Tehnički članak

PDFlibPas JBIG2 halftone mreže: HSKIP i negativni pomaci

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

PDFlibPas JBIG2 halftone skip maske za mrežu 5 puta 3 gdje ispravna HSKIP[ng, mg] označava stupac 4 i redak 2 kao preskočene, dok su transponirani upisi ciljani na retke 3 i 4 troredne maske tiho odbačeni i preživio je samo stupac 2, desinkronizirajući aritmetičke kodere
Samo nekvadratna mreža otkriva transponiranu masku, a posljedica desinkronizacije kodera pokvari regiju umjesto da prijavi pogrešku

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

PDFlibPas JBIG2 halftone kvarovi za mrežu na negativnom HGX: predznačeno polje čitano kroz nepredznačeni pomoćnik stezano na nulu, T.88 pomak udesno preveden kao logički shr koji je poslao obrazac 16 milijuna redaka dolje, i skip test nad fixed-point vrijednostima koji je zadržao ćeliju koju je enkoder preskočio
Svaki je kvar krio sljedeći, pa je v3.539.38 popravio sva tri sloja zajedno u jednom zajedničkom HalftoneGridPixel pomoćniku koji koriste graditelj maske i petlja postavljanja

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 kvaroviDekodirana regija
Nijedan (prije v3.539.38)Mreža povučena na ishodište, cijela slika dva retka prenisko
Samo čitanje predznačenog HGX / HGYPrvi redak mreže nedostaje, ostalo pokvareno odstupanjem skip testa
Predznačeno čitanje, floor pomak i skip test u pikselnom prostoruIdentič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;
PDFlibPas brojevna linija za koordinatu -900 pomaknutu udesno za 8: shr daje 16777212, div reže na -3, dok FloorDiv i SarInt32 oba doskoče na floor vrijednost -4 koju ITU-T T.88 misli pomakom, što je važno samo kad je fixed-point razlomak ne-nula
Rezanje i floor poklapaju se samo na točnim višekratnicima, pa -512 div 256 prolazi brzi test, a -900 div 256 bira pogrešan piksel

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 HGX i HGY kao predznačene 32-bitne vrijednosti (T.88 §7.4.5.1.2), nikad kroz nepredznačeni pomoćnik koji steže negativne
  • Standardov >> 8 prevodite kao floor dijeljenje s 256, ne kao shr 8 ni kao div 256
  • Skip test izvodite na pomaknutim pikselnim pozicijama; fixed-point oblik razlikuje se kad god je razlomak ne-nula, kao što pokazuje HGX = -900 s obrascem od 4 piksela
  • Akumulirajte koordinate mreže u Int64 i 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