PDFlibPas je popravio dve nezavisne greške u svom nativnom JBIG2 halftone region dekoderu: u v3.539.37 HSKIP skip maska indeksira se kao HSKIP[ng, mg] kako ITU-T T.88 §6.6.5.1 definiše, a u v3.539.38 mreže koje dopiru do negativnih koordinata, kroz negativan HGX ili HGY ili kroz rotaciju, postavljaju se pravim floor pomerajem. Pre tih izdanja, pogođeni halftone regioni izlazili su iskrivljeni ili pomerani, bez podignute greške. Oba buga krila su se iza test podataka koji su slučajno bili simetrični ili nenegativni, a drugi se oslanja na osobinu Delphi-ja i Free Pascala koja grize daleko van JBIG2: shr nad označenim celim brojem je logički pomeraj, a ne aritmetički >> koji standard pretpostavlja
Halftone regioni su najređi tip JBIG2 regiona, pa dekoder može obraditi hiljade skeniranih dokumenata pre nego što sretne rastriranu fotografiju kodiranu kao jedan. Kad je sretne, kvar je gadan: fajl se parsira, dužine segmenata se sabiraju, stranica ima pravu veličinu, a region je đubre
Šta JBIG2 halftone region zapravo dekodira?
JBIG2 halftone region je mreža malih bitmapa uzetih iz pattern rečnika, i pravi posao dekodera je računanje indeksa za svaku ćeliju mreže i piksel pozicije na koju ta ćelija doskoči. Pattern rečnik drži HNUMPATS obrazaca od HPW × HPH piksela. Segment halftone regiona zatim opisuje mrežu od HGW kolona sa HGH redova i sliku u sivoj istih dimenzija, kodiranu kao Gray-kodirane bitplane. Svaki bitplane dekodira se generic region procedurom nad HGW × HGH bitmapom, najznačajnija ravnina prva, i ravnine zajedno daju svakoj ćeliji njen indeks obrasca
Postavljanje ćelija koristi fixed-point aritmetiku sa 8-bitnim razlomkom. Poreklo mreže HGX, HGY je par 32-bitnih vrednosti, a vektor mreže HRX, HRY opisuje korak između susednih ćelija, što dopušta rotiranu mrežu. Za red mreže mg i kolonu mreže ng, T.88 §6.6.5 računa piksel poziciju kao:
x = (HGX + mg × HRY + ng × HRX) >> 8y = (HGY + mg × HRX − ng × HRY) >> 8
Skip maska ulazi kroz opcionu HENABLESKIP zastavicu. Kad je zastavica postavljena, §6.6.5.1 gradi HGW × HGH bitmapu HSKIP i postavlja HSKIP[ng, mg] na 1 za svaku ćeliju čiji obrazac leži potpuno van regiona: x + HPW <= 0, x >= HBW, y + HPH <= 0 ili y >= HBH. Sive bitplane dekodiraju se zatim sa tom maskom kao generic region skip bitmapa, pa aritmetički dekoder ni ne čita ni ne ažurira kontekst za preskočenu ćeliju. Dekoder i enkoder moraju se slagati o svakom bitu HSKIP, ili ta dva aritmetička kodera padaju iz koraka
Zašto je transponovana HSKIP maska lomila samo ne-kvadratne mreže?
Skip maska upisivana je sa zamenjenim koordinatama, i samo ne-kvadratna mreža to je otkrila, jer kvadratna mreža drži svaku zamenjenu koordinatu unutar maske. PDFlibPas čuva bitmape sa (column, row) piksel pristupnikom, a kod koji je gradio masku prosleđivao je (mg, ng), red prvi. Dekoder sivih bitplane čita masku ispravno kao (ng, mg). Petlja postavljanja obrazaca čitala ju je nazad u zamenjenom redosledu gradioca, pa su se ta dva slagala, i pregled same logike postavljanja prošao bi je. Zamka imenovanja to je pogoršala: u petlji postavljanja varijabla nazvana col iterira redove mreže a Row iterira kolone mreže
Uzmite mrežu 5 × 3 od obrazaca 4 × 4 na regionu 16 × 8 koju v3.539.37 koristi kao regresioni slučaj. Sa HRX = 1024 i HRY = 0, kolona mreže 4 doskoči na x = 16 a red mreže 2 na y = 8, obe van regiona. Ispravna maska označava sedam ćelija: celu kolonu 4 i ceo red 2. Zamenjeni upisi pokušali su da postave piksele na indeksima redova 3 i 4 u maski visokoj samo tri reda, i bitmap postavljač tiho je ignorišao te van-opseg upise. Što je preživelo bila je kolona 2, redovi 0 do 2. Dekoder je zato preskočio dve ćelije koje je enkoder kodirao, i dekodirao šest ćelija koje je enkoder preskočio
Aritmetički dekoder ne pada kad se to desi. Dekodira dodatne piksele iz bitova koji pripadaju kasnijim ćelijama, njegovi konteksti čitaju pogrešne susede, i svaki indeks obrasca posle prvog neslaganja je šum, pa je simptom bio iskrivljen region umesto nekoliko pomeranih ćelija. Na kvadratnoj mreži isti bug je često nevidljiv: nijedna zamenjena koordinata ne izlazi iz maske, i kad su van-regionske ćelije simetrične oko dijagonale, mreža koja previšava desnu i donju ivicu istim brojem ćelija na primer, transponovana maska je bit po bit ispravna. HENABLESKIP je takođe opciona, mora biti 0 kad je siva slika MMR-kodirana, i retko je postavljaju enkoderi, pa je bug imao vrlo malo načina da se pokaže. Od v3.539.37 gradilac upisuje HSKIP[ng, mg] i petlja postavljanja čita isti redosled
Zašto negativni ofseti halftone mreže padaju u tri sloja?
Halftone mreža koja počinje levo od ili iznad svog regiona lomila je PDFlibPas na tri odvojena mesta, i svaka greška krila je sledeću. T.88 dopušta tu geometriju namerno. Enkoder koji poravnava svoj rastr sa stranicom a ne sa regionom, ili koristi rotiranu mrežu, prirodno proizvodi negativne uglove ćelija koje region iseče. v3.539.38 popravio je sva tri sloja zajedno, jer popravka bilo koga sama menjala je samo simptom
Sloj 1: označeno polje čitano kao neoznačeno
T.88 §7.4.5.1.2 definiše HGX i HGY kao označene 32-bitne vrednosti, ali dekoder ih je čitao istim 32-bitnim pomoćnikom kojim je čitao neoznačena polja, i taj pomoćnik je svaki negativan rezultat stezao na 0. Mreža koja je trebalo da počne na HGX = -900 tiho je pomerena na poreklo regiona. U regresionom slučaju v3.539.38 cela slika izašla je dva reda prenisko. Stezanje objašnjava i zašto su druge dve greške preživele toliko dugo: sa poreklom nasilno nenegativnim, negativna koordinata mogla se pojaviti samo kroz rotiranu mrežu sa HRY > 0, gde y = HGY + mg × HRX − ng × HRY pada ispod nule za kasnije kolone mreže
Sloj 2: shr nije >> 8
T.88 piše >> 8 i misli na aritmetički pomeraj, koji zaokružuje ka minus beskonačnosti. Dekoder ga je preveo kao shr 8. U Delphi-ju i Free Pascalu, shr nad označenim celim brojem je logički pomeraj: znakovni bit usmerava se kao nula. Za Integer koji drži -512, shr 8 daje 16777214 umesto -2. Obrazac koji je trebalo da bude nacrtan na y = -2 i isečen na donju polovinu poslat je 16 miliona redova dole i bačen kao van-regiona. Ništa nije palo; gornji red halftone-a jednostavno je nestao
Sloj 3: poređenje fixed-point umesto piksela
Skip test poredio je fixed-point vrednosti, a ne piksel pozicije, i dve nisu ekvivalentne kad razlomak nije nula. Originalni kod izbegao je logički pomeraj testirajući xx + HPW × 256 <= 0 na nepomerenoj vrednosti, navodni ekvivalent T.88 testa. Sa HGX = -900 i obrascem od 4 piksela, to daje -900 + 1024 = 124, što je pozitivno, pa ćelija nije preskočena. Standard pomera prvo: floor(-900 / 256) = -4, i -4 + 4 = 0 zadovoljava x + HPW <= 0, pa ćelija leži potpuno van i mora se preskočiti. Enkoder ju je preskočio, dekoder ju je dekodirao, i siva slika odstupala je baš kao u slučaju transponovane maske
Regresioni slučaj iz v3.539.38 koristi mrežu 4 × 3 obrazaca 4 × 4 na HGX = -900, HGY = -512, HRX = 1024 na regionu 12 × 10. Kolone mreže doskoče na x = -4, 0, 4 i 8, pa je kolona 0 potpuno van i pripada u HSKIP; redovi mreže doskoče na y = -2, 2 i 6, pa red 0 mora biti isečen na donja dva piksel reda umesto bačen. Popravka slojeva jedan po jedan reprodukuje stek:
| Popravljene greške | Dekodirani region |
|---|---|
| Nijedna (pre v3.539.38) | Mreža privučena na poreklo, cela slika dva reda prenisko |
| Samo čitanje označenog HGX / HGY | Prvi red mreže nedostaje, ostatak iskrivljen odstupanjem skip testa |
| Označeno čitanje, floor pomeraj i skip test u piksel prostoru | Identično, piksel po piksel, stranici računatoj iz T.88 §6.6.5 i dva nezavisna referentna dekodera |
Popravka je jedan pomoćnik, HalftoneGridPixel, deljen između graditelja skip maske i petlje postavljanja. Akumulira koordinatu u Int64 da veliki proizvod mg × HRX ne može da se prelije, deli sa 256 zaokružujući ka minus beskonačnosti, i stezuje na ±MaxInt div 2 da oštećena mreža ne može preliti kasniju bitmap aritmetiku. Skip test sada poredi te piksel vrednosti sa HPW, HPH, HBW i HBH, tačno kako §6.6.5.1 kaže
Kako napisati aritmetički pomeraj udesno u Delphi-ju?
Delphi nema operator aritmetičkog pomeraja, pa se ispravan označeni pomeraj udesno mora napisati kao floor deljenje, i običan div nije to deljenje. div seče ka nuli. Za nenegativne vrednosti sečenje i floor se slažu, i slažu se i za negativne vrednosti koje su tačni višekratnici delioca, pa -512 div 256 = -2 izgleda u redu u brzom testu. Ne slažu se svuda drugde: -900 div 256 je -3, dok je floor -4, i -1 div 256 je 0, dok je floor -1. JBIG2 koordinata sa ne-nula razlomkom je baš slučaj gde div daje pogrešan piksel
Na Delphi Win32 i Win64 kompajlerima, Integer promenljiva koja drži -512 pomerena udesno za 8 daje 16777214, a Int64 koji drži -512 daje 72057594037927934. Free Pascal takođe definiše shr kao logički pomeraj i isporučuje SarLongint i SarInt64 u svojoj System jedinici za aritmetičku verziju, ali te funkcije ne postoje u Delphi-ju, pa kod deljen između dva kompajlera treba sopstveni pomoćnik:
// Floor deljenje: zaokružuje ka minus beskonačnosti za bilo koji znak A i B.
// B ne sme biti 0, i FloorDiv(Low(Integer), -1) se prelije baš kao 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;
// Aritmetički pomeraj udesno (C i T.88 ">>" nad označenim vrednostima).
// Za negativan Value, not Value = -Value - 1 je nenegativan, pa je
// logički shr tamo bezbedan, i spoljašnji not vraća rezultat nazad
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 trik nikada ne pomera negativan broj, pa ne zavisi od toga kako kompajler tretira znakovni bit, i nikada se ne prelije, uključujući Low(Integer). Oba pomoćnika poklopila su se sa Int64 floor referencom preko nekoliko miliona vrednosti, svaki pomeraj od 0 do 31 i Low(Integer) i High(Integer) ivice na Delphi Win32, Delphi Win64 i Free Pascal x86_64. Sanitarna provera vredna da se zadrži u svakom unit testu koji dodiruje koordinate:
var
V: Integer;
begin
V := -900;
Writeln(V shr 8); // 16777212 logički pomeraj, stari bug
Writeln(V div 256); // -3 sečenje ka nuli
Writeln(FloorDiv(V, 256)); // -4 ono što T.88 misli sa >> 8
Writeln(SarInt32(V, 8)); // -4
end;
Math.Floor(V / 256) takođe vraća -4, ali njen obilazak kroz Double gubi preciznost za Int64 vrednosti iznad 253, pa celobrojna geometrija treba da ostane u celim brojevima
Koji PDFlibPas pozivi pokreću halftone dekoder?
JBIG2 halftone dekoder radi kad PDFlibPas renderuje stranicu ugrađenim renderer-om, jer renderovanje traži piksele. RenderPageToFile i RenderPageToStream oba dolaze do njega kroz JBIG2Decode image tokove stranice, pa ponovno renderovanje halftone stranice direktni je način da potvrdite da v3.539.38 menja vaš izlaz. Isti dekoder rukuje ostalim JBIG2 tipovima regiona, pokrivenim u JBIG2 custom Huffman tabelama u čistom Pascal dekoderu i dekodiranju random-access JBIG2 fajlova u Delphi-ju, a renderovana bitmapa hrani konverzije poput renderovanja PDF stranica u 1-bit monohrom
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
// Renderovanje dekodira svaki JBIG2 region, halftone uključeno
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.
Izdvajanje slika normalno ide drugom putanjom. GetPageImageList vraća JBIG2 slike u nativnom obliku, a SaveImageListItemDataToFile ili GetImageListItemDataToString uručuje vam samostalan JBIG2 fajl građen iz bajtova toka: fajl header, JBIG2Globals podaci i end-of-file segment oko podataka stranice. Svojstvo 400 od GetImageListItemIntProperty prijavljuje 6 za takvu stavku. Ništa se ne dekodira na toj putanji, pa je izdvojeni .jb2 koji izgleda ispravno u drugom pregledniku dok renderovana stranica pokazuje šum bio tipičan znak ova dva halftone buga:
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;
Kad maske ili konverzija boja nateraju renderovani fallback, stavka se vraća kao dekodirana bitmapa i halftone dekoder zaista radi. Više o image listama je u Delphi PDF izdvajanju teksta, slika i fontova
Brzi pregled: pravila JBIG2 halftone mreže
- Indeksirajte skip masku kao
HSKIP[ng, mg], kolona mreže prva, i čitajte je nazad istim redosledom gde god se ćelije postavljaju (T.88 §6.6.5.1, popravljeno u PDFlibPas v3.539.37) - Testirajte svaki halftone ili mrežni kod ne-kvadratnom mrežom i asimetričnim skupom van-regionskih ćelija, jer kvadratna mreža može potpuno sakriti transponovani indeks
- Čitajte
HGXiHGYkao označene 32-bitne vrednosti (T.88 §7.4.5.1.2), nikada kroz neoznačeni pomoćnik koji stezuje negativne - Prevodite
>> 8standarda kao floor deljenje sa 256, ne kaoshr 8i ne kaodiv 256 - Vodite skip test nad pomeranim piksel pozicijama; fixed-point oblik razlikuje se kad god razlomak nije nula, kao što pokazuje
HGX = -900sa obrascem od 4 piksela - Akumulirajte koordinate mreže u
Int64i stezujte pre nego što ih predate bitmap kodu, da oštećena mreža ne može da se prelije - Nadogradite na v3.539.38 ili noviji ako vaši dokumenti sadrže halftone regione sa
HENABLESKIP, negativnim poreklom mreže ili rotiranim mrežama
PDFlibPas renderuje, izdvaja i uređuje PDF dokumente iz Delphi-ja i C++Builder-a sa nativnim Pascal JBIG2 dekoderom koji sada rukuje halftone skip maskama, negativnim poreklom mreže i rotiranim mrežama kako T.88 specificira. Pogledajte PDFlibPas Delphi PDF biblioteku za funkcije, izdanja i trial preuzimanje