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) >> 8y = (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
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
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 viat | Dekoodattu alue |
|---|---|
| Ei yhtään (ennen v3.539.38) | Ruudukko vedetty alkukohtaan, koko kuva kaksi riviä liian matalalla |
| Vain etumerkillinen HGX/HGY -luku korjattu | Ensimmäinen ruudukkorivi puuttuu, loput sotkuisina ohitustestin ajautumisesta |
| Etumerkillinen luku, floor-siirto ja pikselitilan ohitustesti | Identtinen 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;
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
HGXjaHGYetumerkillisinä 32-bittisinä arvoina (T.88 §7.4.5.1.2), ei koskaan etumerkittömän avustajan kautta, joka puristaa negatiivisia - Käännä standardin
>> 8floor-jakona 256:lla, ei muodonashr 8enkä muodonadiv 256 - Aja ohitustesti siirretyillä piksellisijainneilla; kiintopistemuoto eroaa aina, kun murto-osa on nollasta poikkeava, kuten
HGX = -9004 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