Tehnički članak

PDFlibPas JBIG2 halftone mreže: HSKIP i negativni ofseti

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) >> 8
  • y = (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

PDFlibPas JBIG2 halftone skip maske za mrežu 5 sa 3 gde ispravna HSKIP[ng, mg] označava kolonu 4 i red 2 kao preskočene, dok su transponovani upisi ciljani na redove 3 i 4 triredne maske tiho odbačeni i preživela je samo kolona 2, desinhronizujući aritmetičke kodere
Samo ne-kvadratna mreža otkriva transponovanu masku, i nastala desinhronizacija kodera iskrivljuje region umesto da podigne grešku

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

PDFlibPas JBIG2 halftone greške za mrežu na negativnom HGX: označeno polje čitano kroz neoznačeni pomoćnik stezano na nulu, T.88 pomeraj udesno preveden kao logički shr koji je poslao obrazac 16 miliona redova dole, i skip test nad fixed-point vrednostima koji je zadržao ćeliju koju je enkoder preskočio
Svaka greška krila je sledeću, pa je v3.539.38 popravio sva tri sloja zajedno u jednom deljenom HalftoneGridPixel pomoćniku kojeg koriste gradilac maske i petlja postavljanja

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škeDekodirani region
Nijedna (pre v3.539.38)Mreža privučena na poreklo, cela slika dva reda prenisko
Samo čitanje označenog HGX / HGYPrvi red mreže nedostaje, ostatak iskrivljen odstupanjem skip testa
Označeno čitanje, floor pomeraj i skip test u piksel prostoruIdentič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;
PDFlibPas brojevna linija za koordinatu -900 pomerenu udesno za 8: shr daje 16777212, div seče na -3, dok FloorDiv i SarInt32 oba doskoče na floor vrednost -4 koju ITU-T T.88 misli pomerajem, što je bitno samo kad fixed-point razlomak nije nula
Sečenje i floor se slažu samo na tačnim višekratnicima, pa -512 div 256 prolazi brzi test a -900 div 256 bira pogrešan piksel

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 HGX i HGY kao označene 32-bitne vrednosti (T.88 §7.4.5.1.2), nikada kroz neoznačeni pomoćnik koji stezuje negativne
  • Prevodite >> 8 standarda kao floor deljenje sa 256, ne kao shr 8 i ne kao div 256
  • Vodite skip test nad pomeranim piksel pozicijama; fixed-point oblik razlikuje se kad god razlomak nije nula, kao što pokazuje HGX = -900 sa obrascem od 4 piksela
  • Akumulirajte koordinate mreže u Int64 i 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