PDFlibPas je popravio dva neovisna kvara u svom nativnom JBIG2 halftone dekoderu regija: u v3.539.37 HSKIP skip maska indeksira se kao HSKIP[ng, mg], kako to definira ITU-T T.88 §6.6.5.1, a u v3.539.38 mreže koje dosežu negativne koordinate, kroz negativni HGX ili HGY ili kroz rotaciju, postavljaju se pravim floor pomakom. Prije tih izdanja zahvaćene halftone regije izlazile su pokvarene ili pomaknute, bez ijedne prijavljene pogreške. Oba su se buga krila iza testnih podataka koji su slučajno bili simetrični ili nenegativni, a drugi se oslanja na svojstvo Delphija i Free Pascala koje grize daleko izvan JBIG2: shr nad predznačenim cijelim brojem jest logički pomak, a ne aritmetički >> koji standard pretpostavlja
Halftone regije najrjeđi su tip JBIG2 regija, pa dekoder može obraditi tisuće skeniranih dokumenata prije nego što naiđe na rasteriranu fotografiju kodiranu kao jedna. Kad joj dođe, kvar je gadan: datoteka se parsira, duljine segmenata se zbroje, stranica ima pravu veličinu, a regija je smeće
Što JBIG2 halftone regija zapravo dekodira?
JBIG2 halftone regija jest mreža malih bitmapa izabranih iz pattern rječnika, a pravi posao dekodera jest izračun indeksa za svaku ćeliju mreže i pikselne pozicije gdje ta ćelija doskoči. Pattern rječnik drži HNUMPATS obrazaca od HPW × HPH piksela. Segment halftone regije zatim opisuje mrežu od HGW stupaca preko HGH redaka i sliku u sivim tonovima iste veličine, kodiranu kao Gray-kodirane bitplaneove. Svaki se bitplane dekodira generičkim postupkom regije nad HGW × HGH bitmapom, najznačajniji plane prvi, a planeovi zajedno daju svakoj ćeliji njezin indeks obrasca
Postavljanje ćelija koristi fixed-point aritmetiku s 8-bitnim razlomkom. Ishodište mreže HGX, HGY jest par 32-bitnih vrijednosti, a vektor mreže HRX, HRY opisuje korak između susjednih ćelija, što dopušta rotiranu mrežu. Za redak mreže mg i stupac mreže ng, T.88 §6.6.5 računa pikselnu poziciju kao:
x = (HGX + mg × HRY + ng × HRX) >> 8y = (HGY + mg × HRX − ng × HRY) >> 8
Skip maska ulazi kroz neobaveznu 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 izvan regije: x + HPW <= 0, x >= HBW, y + HPH <= 0 ili y >= HBH. Sive bitplaneove zatim dekodira ta maska kao skip bitmapu generičke regije, pa aritmetički dekoder ni ne čita ni ne ažurira kontekst za preskočenu ćeliju. Dekoder i enkoder moraju se poklapati u svakom bitu HSKIP-a, ili dva aritmetička kodera ispadnu iz koraka
Zašto je transponirana HSKIP maska pokvarila samo nekvadratne mreže?
Skip maska zapisivala se s zamijenjenim koordinatama, a otkrila ju je samo nekvadratna mreža jer kvadratna mreža drži svaku zamijenjenu koordinatu unutar maske. PDFlibPas sprema bitmape s (column, row) pikselnim pristupnikom, a kod koji je gradio masku proslijedio je (mg, ng), redak prvi. Dekoder sivih bitplaneova čita masku ispravno kao (ng, mg). Petlja postavljanja obrazaca čitala ju je natrag u zamijenjenom redoslijedu graditelja, pa su se ta dva poklapala, i pregled same logike postavljanja prošao bi je. Zamka imena to je pogoršala: u petlji postavljanja varijabla imena col iterira retke mreže, a Row stupce mreže
Uzmite mrežu 5 × 3 od obrazaca 4 × 4 na regiji 16 × 8 koju v3.539.37 koristi kao regresijski slučaj. S HRX = 1024 i HRY = 0, stupac mreže 4 doskoči na x = 16, a redak mreže 2 na y = 8, oboje izvan regije. Ispravna maska označava sedam ćelija: cijeli stupac 4 i cijeli redak 2. Zamijenjeni upisi pokušali su postaviti piksele na indeksima redaka 3 i 4 u maski visokoj samo tri retka, i postavljač bitmape tiho je ignorirao te izvanrasponske upise. Preživio je stupac 2, redci 0 do 2. Dekoder je stoga preskočio dvije ćelije koje je enkoder kodirao, i dekodirao šest ćelija koje je enkoder preskočio
Aritmetički dekoder ne pada kad se to dogodi. Dekodira viškove piksele iz bitova koji pripadaju kasnijim ćelijama, njegovi konteksti čitaju pogrešne susjede, i svaki indeks obrasca nakon prvog neslaganja jest šum, pa je simptom bila pokvarena regija umjesto nekoliko pomaknutih ćelija. Na kvadratnoj mreži isti je bug često nevidljiv: nijedna zamijenjena koordinata ne izlazi iz maske, a kad su ćelije izvan regije simetrične s obzirom na dijagonalu, na primjer mreža koja prelazi desni i donji rub za isti broj ćelija, transponirana maska jest bit po bit ispravna. HENABLESKIP je i neobavezan, mora biti 0 kad je siva slika MMR-kodirana, i retko ga postavljaju enkoderi, pa je bug imao vrlo malo načina da iziđe na vidjelo. Od v3.539.37 graditelj piše HSKIP[ng, mg], a petlja postavljanja čita isti redoslijed
Zašto negativni pomaci halftone mreže zakazuju u tri sloja?
Halftone mreža koja počinje lijevo od ili iznad svoje regije slomila je PDFlibPas na tri odvojena mjesta, i svaki je kvar krio sljedeći. T.88 tu geometriju dopušta namjerno. Enkoder koji poravnava svoj raster sa stranicom, a ne s regijom, ili koristi rotiranu mrežu, prirodno proizvodi negativne uglove ćelija koje regija obreže. v3.539.38 popravio je sva tri sloja zajedno, jer bi popravak bilo kojeg usamljenog samo promijenio simptom
Sloj 1: predznačeno polje čitano kao nepredznačeno
T.88 §7.4.5.1.2 definira HGX i HGY kao predznačene 32-bitne vrijednosti, ali dekoder ih je čitao istim 32-bitnim pomoćnikom kojeg je koristio za nepredznačena polja, a taj je pomoćnik svaki negativan rezultat stezao na 0. Mreža koja je trebala početi na HGX = -900 tiho je premještena na ishodište regije. U regresijskom slučaju v3.539.38 cijela je slika izašla dva retka prenisko. Stezanje objašnjava i zašto su se druga dva kvara toliko dugo održala: s ishodištem natjeranim da bude nenegativno, negativna se koordinata mogla pojaviti samo kroz rotiranu mrežu s HRY > 0, gdje y = HGY + mg × HRX − ng × HRY pada ispod nule za kasnije stupce mreže
Sloj 2: shr nije >> 8
T.88 piše >> 8 i misli na aritmetički pomak, koji zaokružuje prema minus beskonačnosti. Dekoder ga je preveo kao shr 8. U Delphiju i Free Pascalu shr nad predznačenim cijelim brojem jest logički pomak: znakovni bit uspomakne se kao nula. Za Integer koji drži -512, shr 8 daje 16777214 umjesto -2. Obrazac koji se trebao nacrtati na y = -2 i obrezati na donju polovicu poslan je 16 milijuna redaka dolje i otpao je kao izvan regije. Ništa se nije srušilo; gornji redak halftonea jednostavno je nestao
Sloj 3: uspoređivanje fixed-pointa umjesto piksela
Skip test uspoređivao je fixed-point vrijednosti, a ne pikselne pozicije, a te dvije nisu ekvivalentne jednom kad je razlomak ne-nula. Izvorni je kod izbjegavao logički pomak testirajući xx + HPW × 256 <= 0 na nepomaknutoj vrijednosti, navodnoj ekvivalentu T.88 testa. S HGX = -900 i obrascem od 4 piksela to daje -900 + 1024 = 124, što je pozitivno, pa se ćelija ne preskače. Standard prvo pomjera: floor(-900 / 256) = -4, i -4 + 4 = 0 zadovoljava x + HPW <= 0, pa ćelija leži potpuno izvan i mora se preskočiti. Enkoder ju je preskočio, dekoder ju je dekodirao, i slika u sivim tonovima odstupila je točno kao u slučaju transponirane maske
Regresijski slučaj iz v3.539.38 koristi mrežu 4 × 3 od obrazaca 4 × 4 na HGX = -900, HGY = -512, HRX = 1024 na regiji 12 × 10. Stupci mreže doskoče na x = -4, 0, 4 i 8, pa je stupac 0 potpuno izvan i pripada u HSKIP; redci mreže doskoče na y = -2, 2 i 6, pa redak 0 treba obrezati na njegova donja dva pikselna retka, a ne otpisati ga. Popravljanje slojeva jedan po jedan reproducira hrpu:
| Popravljeni kvarovi | Dekodirana regija |
|---|---|
| Nijedan (prije v3.539.38) | Mreža povučena na ishodište, cijela slika dva retka prenisko |
| Samo čitanje predznačenog HGX / HGY | Prvi redak mreže nedostaje, ostalo pokvareno odstupanjem skip testa |
| Predznačeno čitanje, floor pomak i skip test u pikselnom prostoru | Identično, piksel za piksel, stranici izračunatoj iz T.88 §6.6.5 i dva neovisna referentna dekodera |
Popravak je jedan pomoćnik, HalftoneGridPixel, zajednički graditelju skip maske i petlji postavljanja. Koordinatu akumulira u Int64 da veliki mg × HRX umnožak ne može omotati, dijeli s 256 zaokružujući prema minus beskonačnosti, i steže na ±MaxInt div 2 da pokvarena mreža ne može overflowati kasniju bitmapnu aritmetiku. Skip test sada uspoređuje te pikselne vrijednosti s HPW, HPH, HBW i HBH, točno kako §6.6.5.1 kaže
Kako napisati aritmetički pomak udesno u Delphiju?
Delphi nema aritmetički operator pomaka, pa se ispravan predznačeni pomak udesno mora napisati kao floor dijeljenje, a obični div nije to dijeljenje. div reže prema nuli. Za nenegativne vrijednosti rezanje i floor se poklapaju, poklapaju se i za negativne vrijednosti koje su točni višekratnici djelitelja, pa -512 div 256 = -2 izgleda u redu u brzom testu. Razilaze se svugdje drugdje: -900 div 256 jest -3, a floor je -4, i -1 div 256 jest 0, a floor je -1. JBIG2 koordinata s ne-nula razlomkom upravo je slučaj gdje div daje pogrešan piksel
Na Delphi Win32 i Win64 kompajlerima Integer varijabla koja drži -512, pomaknuta udesno za 8, daje 16777214, a Int64 koji drži -512 daje 72057594037927934. Free Pascal također definira shr kao logički pomak i isporučuje SarLongint i SarInt64 u svojoj System jedinici za aritmetičku verziju, ali te funkcije ne postoje u Delphiju, pa kod dijeljen između dva kompajlera treba vlastitog pomoćnika:
// Floor dijeljenje: zaokružuje prema minus beskonačnosti za bilo koji predznak A i B.
// B ne smije biti 0, a FloorDiv(Low(Integer), -1) overflowa 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 pomak udesno (C-ov i T.88 ">>" nad predznačenim vrijednostima).
// Za negativan Value, not Value = -Value - 1 je nenegativan, pa je
// logički shr tamo siguran, a vanjski not vraća rezultat natrag
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 nikad ne pomjera negativan broj, pa ne ovisi o tome kako kompajler tretira znakovni bit, i nikad ne overflowa, uključujući Low(Integer). Oba su pomoćnika poklapala s Int64 floor referencom preko nekoliko milijuna vrijednosti, svaki pomak od 0 do 31 i rubovi Low(Integer) i High(Integer) na Delphi Win32, Delphi Win64 i Free Pascal x86_64. Sanity check koji vrijedi zadržati u svakom unit testu koji dira koordinate:
var
V: Integer;
begin
V := -900;
Writeln(V shr 8); // 16777212 logički pomak, stari bug
Writeln(V div 256); // -3 rezanje prema nuli
Writeln(FloorDiv(V, 256)); // -4 ono što T.88 misli s >> 8
Writeln(SarInt32(V, 8)); // -4
end;
Math.Floor(V / 256) također vraća -4, ali njezin otklon kroz Double gubi preciznost za Int64 vrijednosti iznad 253, pa cijelobrojna geometrija treba ostati u cijelim brojevima
Koji PDFlibPas pozivi pokreću halftone dekoder?
JBIG2 halftone dekoder pokreće se kad PDFlibPas renderira stranicu ugrađenim rendererom, jer renderiranje treba piksele. RenderPageToFile i RenderPageToStream oboje do njega dolaze kroz JBIG2Decode slikovne tokove stranice, pa je ponovno renderiranje halftone stranice izravan način da potvrdite da v3.539.38 mijenja vaš izlaz. Isti dekoder rukuje ostalim JBIG2 tipovima regija, obrađenima u JBIG2 prilagođenim Huffman tablicama u čistom Pascal dekoderu i dekodiranju random-access JBIG2 datoteka u Delphiju, a renderirana bitmapa hrani pretvorbe poput renderiranja PDF stranica u 1-bitni monokrom
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
// Renderiranje dekodira svaku JBIG2 regiju, halftone uključene
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 obično ide drugim putem. GetPageImageList vraća JBIG2 slike u nativnom obliku, a SaveImageListItemDataToFile ili GetImageListItemDataToString predaje vam samostalnu JBIG2 datoteku građenu iz bajtova toka: zaglavlje datoteke, JBIG2Globals podatke i end-of-file segment oko podataka stranice. Svojstvo 400 od GetImageListItemIntProperty javlja 6 za takvu stavku. Ništa se ne dekodira na tom putu, pa je izdvojeni .jb2 koji izgleda ispravno u drugom pregledniku dok renderirana 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 pretvorba boja natjeraju renderirani fallback, stavka se vraća kao dekodirana bitmapa i halftone dekoder se zaista pokreće. Više o slikovnim listama u Delphi PDF izdvajanju teksta, slika i fontova
Brza referenca: pravila JBIG2 halftone mreže
- Indeksirajte skip masku kao
HSKIP[ng, mg], stupac mreže prvi, i čitajte je natrag istim redoslijedom gdje god se ćelije postavljaju (T.88 §6.6.5.1, popravljeno u PDFlibPas v3.539.37) - Testirajte svaki halftone ili mrežni kod nekvadratnom mrežom i asimetričnim skupom ćelija izvan regije, jer kvadratna mreža može potpuno sakriti transponirani indeks
- Čitajte
HGXiHGYkao predznačene 32-bitne vrijednosti (T.88 §7.4.5.1.2), nikad kroz nepredznačeni pomoćnik koji steže negativne - Standardov
>> 8prevodite kao floor dijeljenje s 256, ne kaoshr 8ni kaodiv 256 - Skip test izvodite na pomaknutim pikselnim pozicijama; fixed-point oblik razlikuje se kad god je razlomak ne-nula, kao što pokazuje
HGX = -900s obrascem od 4 piksela - Akumulirajte koordinate mreže u
Int64i stežite ih prije predaje bitmapnom kodu, da pokvarena mreža ne može overflowati - Nadogradite na v3.539.38 ili kasniji ako vaši dokumenti sadrže halftone regije s
HENABLESKIP, negativnim ishodištima mreže ili rotiranim mrežama
PDFlibPas renderira, izdvaja i uređuje PDF dokumente iz Delphija i C++Buildera s nativnim Pascal JBIG2 dekoderom koji sada rukuje halftone skip maskama, negativnim ishodištima mreža i rotiranim mrežama kako T.88 specificira. Pogledajte PDFlibPas Delphi PDF biblioteku za značajke, izdanja i probno preuzimanje