Tekninen artikkeli

PDFlibPas JBIG2-puolisävy: HSKIP ja negatiiviset offsetit

PDFlibPas korjasi kaksi toisistaan riippumatonta vikaa natiivissa JBIG2-puolisävyalueen dekooderissaan: versiossa v3.539.37 HSKIP-ohitusmaski indeksoidaan muodossa HSKIP[ng, mg], niin kuin ITU-T T.88 §6.6.5.1 sen määrittelee, ja versiossa v3.539.38 ruudukot, jotka ulottuvat negatiivisiin koordinaatteihin negatiivisen HGX:n tai HGY:n kautta tai kiertymisen takia, sijoitetaan aidolla floor-siirrolla. Ennen kyseisiä julkaisuja vaurioituneet puolisävyalueet tulivat ulos sotkuisina tai siirtyneinä ilman mitään virhettä. Molemmat bugit piiloutuivat testidatan taakse, joka sattui olemaan symmetrinen tai ei-negatiivinen, ja jälkimmäinen laukeaa Delphi ja Free Pascal -ominaisuudesta, joka puree kaukana JBIG2:n ulkopuolellakin: shr etumerkillisellä kokonaisluvulla on looginen siirto, ei standardin olettama aritmeettinen >>

Puolisävyalueet ovat harvinaisin JBIG2-aluetyyppi, joten dekooderi ehtii käsitellä tuhansia skannattuja asiakirjoja ennen kuin se kohtaa rasteroidun valokuvan, joka on koodattu sellaiseksi. Kun se kohtaa, vika on ilkeä: tiedosto jäsennetään, segmenttien pituudet täsmäävät, sivulla on oikea koko, ja alue on roskaa

Mitä JBIG2-puolisävyalue oikeastaan dekoodaa?

JBIG2-puolisävyalue on ruudukko pieniä bittikarttoja, jotka poimitaan kuviotsanakirjasta, ja dekooderin varsinainen työ on laskea indeksi jokaiselle ruudukon solulle ja piksellisijainti, jolle solu laskeutuu. Kuviotsanakirja pitää sisällään HNUMPATS kuviota koko HPW × HPH pikseliä. Puolisävyalueen segmentti kuvaa sitten ruudukon, jossa on HGW saraketta kertaa HGH riviä, ja samankokoisen harmaasävykuvan, koodattuna Gray-koodattuina bittitasoina. Jokainen bittitaso dekoodataan generic region -menettelyllä HGW × HGH-bittikartan yli, merkitsevin taso ensin, ja tasot antavat yhdessä jokaiselle solulle sen kuvioindeksin

Solujen sijoittelu käyttää kiintopistearitmetiikkaa 8-bittisellä murto-osalla. Ruudukon alkukohta HGX, HGY on 32-bittisten arvojen pari, ja ruudukon vektori HRX, HRY kuvaa viereisten solujen välisen askeleen, mikä mahdollistaa kiertyneen ruudukon. Ruudukon riville mg ja sarakkeelle ng T.88 §6.6.5 laskee piksellisijainnin muodossa:

  • x = (HGX + mg × HRY + ng × HRX) >> 8
  • y = (HGY + mg × HRX − ng × HRY) >> 8

Ohitusmaski tulee kuvaan valinnaisen HENABLESKIP-flagin kautta. Kun flagi on asetettu, §6.6.5.1 rakentaa HGW × HGH-bittikartan HSKIP ja asettaa HSKIP[ng, mg] arvoon 1 jokaiselle solulle, jonka kuvio sijaitsee kokonaan alueen ulkopuolella: x + HPW <= 0, x >= HBW, y + HPH <= 0 tai y >= HBH. Harmaasävybittitasot dekoodataan sitten kyseisellä maskilla generic region -ohitusbittikarttana, joten aritmeettinen dekooderi ei lue eikä päivitä kontekstia ohitetulle solulle. Dekooderin ja enkooderin on sovittava jokaisesta HSKIPin bitistä, muuten kaksi aritmeettista kooderia ajautuu tahdista

Miksi transponoitu HSKIP-maski rikkoi vain ei-neliömuotoiset ruudukot?

Ohitusmaski kirjoitettiin koordinaattinsa vaihtaneena, ja vain ei-neliömuotoinen ruudukko paljasti sen, koska neliöruudukko pitää jokaisen vaihdetun koordinaatin maskin sisällä. PDFlibPas tallettaa bittikartat (column, row)-pikselikäsittelijällä, ja maskin rakentanut koodi antoi muodon (mg, ng), rivi ensin. Harmaasävybittitason dekooderi lukee maskin oikein muodossa (ng, mg). Kuvioiden sijoittelusilmukka luki sen takaisin rakentajan vaihdetussa järjestyksessä, joten kaksi sopi yhteen, ja pelkästään sijoittelulogiikan katselmointi olisi hyväksynyt sen. Nimeämisansa pahensi asiaa: sijoittelusilmukassa muuttuja col iteroi ruudukon rivejä ja Row ruudukon sarakkeita

Ota 5 × 3-ruudukko 4 × 4-kuvioilla 16 × 8-alueella, jonka v3.539.37 käyttää regressiotapauksenaan. Arvoilla HRX = 1024 ja HRY = 0 ruudukon sarake 4 laskeutuu kohtaan x = 16 ja ruudukon rivi 2 kohtaan y = 8, kumpikin alueen ulkopuolella. Oikea maski merkitsee seitsemän solua: koko sarakkeen 4 ja koko rivin 2. Vaihdetut kirjoitukset yrittivät asettaa pikseleitä rivi-indekseihin 3 ja 4 maskissa, jossa on vain kolme riviä, ja bittikartan asettaja ohitti kyseiset alueen ulkopuoliset kirjoitukset hiljaisesti. Jäljelle jäi sarake 2, rivit 0–2. Dekooderi ohitti siis kaksi solua, jotka enkooderi oli koodannut, ja dekoodasi kuusi solua, jotka enkooderi oli ohittanut

PDFlibPasin JBIG2-puolisävyn ohitusmaskit 5 kertaa 3 -ruudukolle, jossa oikea HSKIP[ng, mg] merkitsee sarakkeen 4 ja rivin 2 ohitetuiksi, kun taas vaihdetut kirjoitukset, jotka tähtäsivät kolmerivisen maskin riveihin 3 ja 4, hylättiin hiljaisesti ja vain sarake 2 jäi jäljelle, mistä aritmeettiset kooderit ajautuivat tahdista
Vain ei-neliömuotoinen ruudukko paljastaa transponoidun maskin, ja siitä seuraava kooderien tahdistushäiriö sotkee alueen virhettä nostamatta

Aritmeettinen dekooderi ei epäonnistu, kun näin tapahtuu. Se dekoodaa ylimääräisiä pikseleitä bitteihin, jotka kuuluvat myöhemmille soluille, sen kontekstit lukevat vääriä naapureita, ja jokainen kuvioindeksi ensimmäisen erimielisyyden jälkeen on kohinaa, minkä vuoksi oire oli sotkuinen alue muutaman väärin sijoittuneen solun sijaan. Neliöruudukolla sama bugi on usein näkymätön: mikään vaihdettu koordinaatti ei poistu maskista, ja kun alueen ulkopuoliset solut ovat symmetriset diagonaalin suhteen, esimerkiksi ruudukko, joka ylittää oikean ja alareunan samalla solumäärällä, transponoitu maski on bitti bitiltä oikea. HENABLESKIP on lisäksi valinnainen, sen on oltava 0, kun harmaasävykuva on MMR-koodattu, ja enkooderit asettavat sen harvoin, joten bugilla oli hyvin vähän tapoja nousta pintaan. Versiosta v3.539.37 alkaen rakentaja kirjoittaa muodon HSKIP[ng, mg] ja sijoittelusilmukka lukee saman järjestyksen

Miksi negatiiviset puolisävyruudukon offsetit epäonnistuvat kolmessa kerroksessa?

Puolisävyruudukko, joka alkaa alueensa vasemmalla puolella tai yläpuolella, rikkoi PDFlibPasin kolmessa eri kohdassa, ja jokainen vika kätki seuraavan. T.88 sallii tämän geometrian tahallaan. Enkooderi, joka tasaa rasterinsa sivuun alueen sijaan tai käyttää kiertynyttä ruudukkoa, tuottaa luonnollisesti negatiivisia solukulmia, jotka alue rajaa pois. v3.539.38 korjasi kaikki kolme kerrosta yhdessä, koska minkä tahansa yksin korjaaminen vain vaihtoi oireen

Kerros 1: etumerkillinen kenttä luettiin etumerkittömänä

T.88 §7.4.5.1.2 määrittelee HGXin ja HGYn etumerkillisiksi 32-bittisiksi arvoiksi, mutta dekooderi luki ne samalla 32-bittisellä avustajalla, jota se käytti etumerkittömille kentille, ja kyseinen avustaja puristi jokaisen negatiivisen tuloksen arvoon 0. Ruudukko, jonka piti alkaa kohdasta HGX = -900, siirrettiin hiljaisesti alueen alkukohtaan. v3.539.38:n regressiotapauksessa koko kuva tuli ulos kaksi riviä liian matalalla. Puristus selittää myös, miksi muut kaksi vikaa elivät niin kauan: kun alkukohta pakotettiin ei-negatiiviseksi, negatiivinen koordinaatti saattoi ilmestyä vain kiertyneen ruudukon kautta arvolla HRY > 0, jossa lauseke y = HGY + mg × HRX − ng × HRY putoaa alle nollan myöhemmillä ruudukon sarakkeilla

Kerros 2: shr ei ole >> 8

T.88 kirjoittaa muodon >> 8 ja tarkoittaa aritmeettista siirtoa, joka pyöristää miinus ääretöntä kohti. Dekooderi käänsi sen muodoksi shr 8. Delphissä ja Free Pascalissa shr etumerkillisellä kokonaisluvulla on looginen siirto: etumerkkibitti siirtyy sisään nollana. Integerille, joka pitää arvoa -512, shr 8 tuottaa arvon 16777214 arvon -2 sijaan. Kuvio, jonka olisi pitänyt ilmestyä kohtaan y = -2 rajattuna alapuolikkaakseen, lähetettiin 16 miljoonan rivin alas ja hylättiin alueen ulkopuolisena. Mikään ei kaatunut; puolisävyn ylärivi vain katosi

Kerros 3: kiintopisteiden vertailu pikselien sijaan

Ohitustesti vertaili kiintopistearvoja, ei piksellisijainteja, eikä kaksi ole yhtäpitävää, kun murto-osa on nollasta poikkeava. Alkuperäinen koodi kiersi loogisen siirron testaamalla lausekkeen xx + HPW × 256 <= 0 siirtämättömällä arvolla, väitetyllä T.88-testin vastineella. Arvolla HGX = -900 ja 4 pikselin kuviolla se antaa tuloksen -900 + 1024 = 124, joka on positiivinen, joten solua ei ohiteta. Standardi siirtää ensin: floor(-900 / 256) = -4, ja -4 + 4 = 0 täyttää ehdon x + HPW <= 0, joten solu sijaitsee kokonaan ulkopuolella ja se on ohitettava. Enkooderi ohitti sen, dekooderi dekoodasi sen, ja harmaasävykuva ajautui sivuun täsmälleen kuten transponoidun maskin tapauksessa

PDFlibPasin JBIG2-puolisävyn viat ruudukolle negatiivisella HGX:llä: etumerkillinen kenttä luettiin etumerkittömän avustajan kautta, joka puristi sen arvoon nolla, T.88:n oikea siirto käännettiin loogiseksi shriksi, joka lähetti kuvion 16 miljoonan rivin alas, ja kiintopistearvojen ohitustesti piti solun, jonka enkooderi oli ohittanut
Jokainen vika kätki seuraavan, minkä vuoksi v3.539.38 korjasi kaikki kolme kerrosta yhdessä yhtenä jaettuna HalftoneGridPixel-avustajana, jota maskin rakentaja ja sijoittelusilmukka käyttävät

v3.539.38:n regressiotapaus käyttää 4 × 3-ruudukkoa 4 × 4-kuvioilla arvoilla HGX = -900, HGY = -512 ja HRX = 1024 12 × 10-alueella. Ruudukon sarakkeet laskeutuvat kohtiin x = -4, 0, 4 ja 8, joten sarake 0 on kokonaan ulkopuolella ja kuuluu HSKIPiin; ruudukon rivit laskeutuvat kohtiin y = -2, 2 ja 6, joten rivi 0 on rajattava kahteen alimpaan pikseliriviinsä pudottamisen sijaan. Kerrosten korjaaminen yksi kerrallaan toistaa pinon:

Korjatut viatDekoodattu alue
Ei yhtään (ennen v3.539.38)Ruudukko vedetty alkukohtaan, koko kuva kaksi riviä liian matalalla
Vain etumerkillinen HGX/HGY -luku korjattuEnsimmäinen ruudukkorivi puuttuu, loput sotkuisina ohitustestin ajautumisesta
Etumerkillinen luku, floor-siirto ja pikselitilan ohitustestiIdenttinen pikseli pikseliltä T.88 §6.6.5:stä laskettuun sivuun ja kahteen itsenäiseen vertailudekooderiin

Korjaus on yksi avustaja, HalftoneGridPixel, jonka ohitusmaskin rakentaja ja sijoittelusilmukka jakavat. Se kerryttää koordinaatin Int64hin, joten suuri mg × HRX-tulo ei voi kiertyä, jakaa 256:lla miinus ääretöntä kohti pyöristäen ja puristaa arvoon ±MaxInt div 2, joten rikkoutunut ruudukko ei voi ylivuottaa myöhempää bittikarta-aritmetiikkaa. Ohitustesti vertaa nyt kyseisiä pikseliarvoja arvoihin HPW, HPH, HBW ja HBH täsmälleen niin kuin §6.6.5.1 sen toteaa

Miten Delphissä kirjoitetaan aritmeettinen oikea siirto?

Delphissä ei ole aritmeettista siirto-operaattoria, joten oikea etumerkillinen oikea siirto on kirjoitettava floor-jakona, eikä pelkkä div ole kyseinen jako. div katkaisee nollaa kohti. Ei-negatiivisilla arvoilla katkaisu ja floor sopivat yhteen, ja ne sopivat yhteen myös negatiivisilla arvoilla, jotka ovat jakajan tarkkoja kerrannaisia, minkä vuoksi -512 div 256 = -2 näyttää hyvältä pikatestissä. Ne eroavat kaikkialla muualla: -900 div 256 on -3, kun taas floor on -4, ja -1 div 256 on 0, kun taas floor on -1. JBIG2-koordinaatti, jonka murto-osa on nollasta poikkeava, on täsmälleen tapaus, jossa div antaa väärän pikselin

Delphin Win32- ja Win64-kääntäjillä Integer-muuttuja, joka pitää arvoa -512, oikealle siirrettynä 8:lla antaa tuloksen 16777214, ja Int64, joka pitää arvoa -512, antaa tuloksen 72057594037927934. Free Pascal määrittelee myös shrin loogisena siirtona ja toimittaa SarLongintin ja SarInt64in System-yksikössään aritmeettista versiota varten, mutta kyseisiä funktioita ei ole Delphissä, joten kahden kääntäjän välillä jaettava koodi tarvitsee oman avustajansa:

// Floor-jako: pyöristää miinus ääretöntä kohti A:n ja B:n kummalla tahansa etumerkillä.
// B ei saa olla 0, ja FloorDiv(Low(Integer), -1) ylivuotaa aivan kuten div
function FloorDiv(A, B: Integer): Integer;
begin
  Result := A div B;
  if (A mod B <> 0) and ((A < 0) <> (B < 0)) then
    Dec(Result);
end;

// Aritmeettinen oikean siirto (C:n ja T.88:n ">>" etumerkillisillä arvoilla).
// Negatiiviselle Valuelle lauseke not Value = -Value - 1 on ei-negatiivinen, joten
// looginen shr on siellä turvallinen, ja ulompi not kääntää tuloksen takaisin
function SarInt32(Value: Integer; Shift: Integer): Integer;  // Shift 0..31
begin
  if Value >= 0 then
    Result := Value shr Shift
  else
    Result := not ((not Value) shr Shift);
end;

function SarInt64(Value: Int64; Shift: Integer): Int64;      // Shift 0..63
begin
  if Value >= 0 then
    Result := Value shr Shift
  else
    Result := not ((not Value) shr Shift);
end;

not-kikka ei koskaan siirrä negatiivista lukua, joten se ei riipu siitä, miten kääntäjä käsittelee etumerkkibittiä, eikä se koskaan ylivuoda, mukaan lukien Low(Integer). Molemmat avustajat vastasivat Int64-floor-viitettä usean miljoonan arvon yli, jokaisella siirrolla 0–31 sekä Low(Integer)n ja High(Integer)n reuna-arvoilla Delphi Win32:lla, Delphi Win64:llä ja Free Pascal x86_64:llä. Järkevyystarkistus, joka kannattaa pitää missä tahansa koordinaatteihin koskevassa yksikkötestissä:

var
  V: Integer;
begin
  V := -900;
  Writeln(V shr 8);           // 16777212  looginen siirto, vanha bugi
  Writeln(V div 256);         // -3        katkaisu nollaa kohti
  Writeln(FloorDiv(V, 256));  // -4        mitä T.88 tarkoittaa ilmauksella >> 8
  Writeln(SarInt32(V, 8));    // -4
end;
PDFlibPasin lukusuora koordinaatille -900 oikealle siirrettynä 8:lla: shr antaa 16777212, div katkaisee arvoon -3, kun taas FloorDiv ja SarInt32 laskeutuvat kumpikin floor-arvoon -4, jonka ITU-T T.88 tarkoittaa siirrolla, mikä merkitsee vain silloin, kun kiintopisteen murto-osa on nollasta poikkeava
Katkaisu ja floor sopivat yhteen vain tarkoilla kerrannaisilla, joten -512 div 256 läpäisee pikatestin ja -900 div 256 poimii väärän pikselin

Math.Floor(V / 256) palauttaa myös arvon -4, mutta sen koukku Doublen kautta menettää tarkkuutta Int64-arvoilla yli 253, joten kokonaislukugeometrian kannattaa pysyä kokonaisluvuissa

Mitkä PDFlibPas-kutsut ajavat puolisävydekooderin?

JBIG2-puolisävydekooderi ajautuu käyntiin, kun PDFlibPas renderöi sivun sisäänrakennetulla rendererillä, koska renderöinti tarvitsee pikseleitä. Sekä RenderPageToFile että RenderPageToStream ulottuvat siihen sivun JBIG2Decode-kuvastreamien kautta, joten puolisävysivun uudelleenrenderöinti on suora tapa varmistaa, että v3.539.38 muuttaa tuotostasi. Sama dekooderi hoitaa muut JBIG2-aluetyypit, jotka käsitellään artikkeleissa JBIG2:n custom Huffman -taulut puhtaassa Pascal-dekooderissa ja random access -JBIG2-tiedostojen dekoodaus Delphissä, ja renderöity bittikartta ruokkii muunnoksia kuten artikkelin PDF-sivujen renderöinti 1-bittiseksi monokromiksi kuvaama

uses
  SysUtils, PDFlibrary;

var
  Lib: TPDFlib;
  Page: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('scanned-halftone.pdf', '') <> 1 then
      raise Exception.CreateFmt('Load failed, error %d', [Lib.LastErrorCode]);
    for Page := 1 to Lib.PageCount do
      // Renderöinti dekoodaa jokaisen JBIG2-alueen, puolisävyalueet mukaan lukien
      if Lib.RenderPageToFile(150, Page, PDF_RENDER_PNG,
        Format('page-%.3d.png', [Page])) <> 1 then
        Writeln('Page ', Page, ' was not rendered');
  finally
    Lib.Free;
  end;
end.

Kuvien poiminta kulkee yleensä toista polkua. GetPageImageList palauttaa JBIG2-kuvat natiivissa muodossaan, ja SaveImageListItemDataToFile tai GetImageListItemDataToString ojentaa sinulle itsenäisen JBIG2-tiedoston, joka on rakennettu streamin tavuista: tiedosto-otsikko, JBIG2Globals-data ja end-of-file-segmentti sivudatan ympärillä. GetImageListItemIntPropertyn ominaisuus 400 raportoi arvon 6 tällaiselle alkiolle. Mitään ei dekoodata kyseisellä polulla, joten poimittu .jb2, joka näyttää oikealta toisessa katseluohjelmassa, kun renderöity sivu näyttää kohinaa, oli tyypillinen merkki näistä kahdesta puolisävybugista:

var
  ListID, I: Integer;
begin
  Lib.SelectPage(1);
  ListID := Lib.GetPageImageList(0);
  if ListID = 0 then
    Exit;
  try
    for I := 1 to Lib.GetImageListCount(ListID) do
      if Lib.GetImageListItemIntProperty(ListID, I, 400) = 6 then  // standalone JBIG2
        Lib.SaveImageListItemDataToFile(ListID, I, 0,
          Format('page1-image%d.jb2', [I]));
  finally
    Lib.ReleaseImageList(ListID);
  end;
end;

Kun maskit tai värimuunnos pakottavat renderöidyn varapolun, alkio palautuu dekoodattuna bittikarttana ja puolisävydekooderi kyllä ajautuu käyntiin. Lisää kuvaluetteloista artikkelissa Delphi PDF -tekstin, kuvien ja fonttien poiminta

Pikaopas: JBIG2-puolisävyruudukon säännöt

  • Indeksoi ohitusmaski muodossa HSKIP[ng, mg], ruudukon sarake ensin, ja lue se takaisin samassa järjestyksessä missä tahansa sijoittelussa (T.88 §6.6.5.1, korjattu PDFlibPas v3.539.37)
  • Testaa mitä tahansa puolisävy- tai ruudukkoodia ei-neliömuotoisella ruudukolla ja epäsymmetrisellä joukolla alueen ulkopuolisia soluja, koska neliöruudukko voi kätkeä transponoidun indeksin täysin
  • Lue HGX ja HGY etumerkillisinä 32-bittisinä arvoina (T.88 §7.4.5.1.2), ei koskaan etumerkittömän avustajan kautta, joka puristaa negatiivisia
  • Käännä standardin >> 8 floor-jakona 256:lla, ei muodona shr 8 enkä muodona div 256
  • Aja ohitustesti siirretyillä piksellisijainneilla; kiintopistemuoto eroaa aina, kun murto-osa on nollasta poikkeava, kuten HGX = -900 4 pikselin kuviolla osoittaa
  • Kerrytä ruudukon koordinaatit Int64hin ja purista ennen kuin annat ne bittikarttakoodille, joten rikkoutunut ruudukko ei voi ylivuottaa
  • Päivitys versioon v3.539.38 tai uudempaan, jos asiakirjasi sisältävät puolisävyalueita, joilla on HENABLESKIP, negatiivisia ruudukon alkukohtia tai kiertyneitä ruudukkoja

PDFlibPas renderöi, poimii ja muokkaa PDF-asiakirjoja Delphistä ja C++Builderista natiivilla Pascal-JBIG2-dekooderilla, joka hoitaa nyt puolisävyn ohitusmaskit, negatiiviset ruudukon alkukohdat ja kiertyneet ruudukot niin kuin T.88 määrittelee. Katso PDFlibPas Delphi PDF library -sivulta ominaisuudet, versiot ja kokeiluversion lataus