Műszaki cikk

PDFlibPas JBIG2 halftone rácsok: HSKIP és negatív offszetek

A PDFlibPas két egymástól független hibát javított a natív JBIG2 halftone régió dekódorában: v3.539.37-ben a HSKIP kihagyómaszk úgy indexelődik, mint HSKIP[ng, mg], ahogy az ITU-T T.88 §6.6.5.1-e definiálja, v3.539.38-ban pedig a negatív koordinátákra érő rácsok — negatív HGX vagy HGY, vagy forgatás révén — valódi floor-tolással kerülnek helyre. Azok előtt az érintett halftone régiók zavarosan vagy eltolódva jöttek ki, hiba nélkül. Mindkét hiba szimmetrikusra vagy nemnegatívra sikerült tesztadatok mögé bújt, a második pedig egy olyan Delphi- és Free Pascal-tulajdonságra kapcsol be, ami JBIG2-n túl is harap: az előjeles egészre alkalmazott shr logikus tolás, nem pedig az aritmetikai >>, amivel a szabvány számol

A halftone régiók a legritkább JBIG2 régiótípus, így egy dekódor ezernyi szkenelt dokumentumot dolgozhat fel, mielőtt egy rácsos, ditherelt fényképbe botlana, amit halftone régióként kódoltak. Amikor bebotlik, a hiba kellemetlen: a fájl parzolódik, a szegmenshosszok összeadódnak, az oldal mérete stimmel, és a régió szemét

Mit dekódol valójában egy JBIG2 halftone régió?

Egy JBIG2 halftone régió kis bitmapek rácsa, amiket egy pattern dictionaryből válogatnak, és a dekódor igazi munkája az, hogy minden rácsmezőhöz kiszámoljon egy indexet meg a pixelpozíciót, ahova a mező landol. A pattern dictionary HNUMPATS darab, HPW × HPH pixeles patternt tart. A halftone régió szegmens ezután egy HGW oszlopos, HGH soros rácsot és azonos méretű szürkeárnyalatos képet ír le, Gray-kódolt bitsíkokként kódolva. Minden bitsíkot a generikus régió eljárással dekódolnak egy HGW × HGH bitmapen, a legjelentősebb síkkal kezdve, és a síkok együtt adják meg minden mező pattern indexét

A mezők elhelyezése fixpontos aritmetikát használ 8 bites törtrésszel. A rácshipuspont, az HGX, HGY két 32 bites érték pára, a rácsvektor, az HRX, HRY pedig a szomszédos mezők közti lépést írja le, ami forgatott rácsot is megenged. Az mg rácssorra és ng rácsoszlopra a T.88 §6.6.5-e így számolja a pixelpozíciót:

  • x = (HGX + mg × HRY + ng × HRX) >> 8
  • y = (HGY + mg × HRX − ng × HRY) >> 8

A kihagyómaszk az opcionális HENABLESKIP flagon át jön be. Ha a flag be van állítva, a §6.6.5.1 felépít egy HGW × HGH méretű HSKIP bitmapet, és 1-re állítja az HSKIP[ng, mg]-et minden olyan mezőre, aminek a patternje teljesen a régión kívül esik: x + HPW <= 0, x >= HBW, y + HPH <= 0 vagy y >= HBH. A szürkeárnyalatos bitsíkokat ezután ezzel a maszkkal dekódolják, mint generikus régió kihagyóbitmapet, így az aritmetikai dekódor egy kihagyott mezőhöz sem olvas, sem frissít kontextust. A dekódornak és az enkódernek HSKIP minden bitjén egyet kell értenie, különben a két aritmetikai kóder kicsúszik egymásból

Miért csak a nem négyzetes rácsokat törte meg a transzponált HSKIP maszk?

A kihagyómaszk felcserélt koordinátákkal íródott, és csak egy nem négyzetes rács leplezte le, mert egy négyzetes rács minden felcserélt koordinátát a maszkban tart. A PDFlibPas a bitmapeket (column, row) pixelhozzáféréssel tárolja, és a maszkot építő kód (mg, ng)-ot adott át, sorral elöl. A szürkeárnyalatos bitsíkok dekódora helyesen, (ng, mg) sorrendben olvassa a maszkot. A pattern elhelyező ciklus viszont az építő felcserélt sorrendjében olvasta vissza, így a kettő egyetértett, és magát az elhelyezési logikát átnézve átmegy. Egy elnevezési csapda tovább rontotta a helyzetet: az elhelyező ciklusban a col nevű változó a rácssorokat járja, a Row pedig a rácsoszlopokat

Vedd az 5 × 3 rácsot 4 × 4-es patternekből egy 16 × 8-as régión, amit a v3.539.37 regressziós esetként használ. HRX = 1024 és HRY = 0 mellett a 4. rácsoszlop x = 16-ra, a 2. rácssor y = 8-ra landol, mindkettő a régión kívülre. A helyes maszk hét mezőt jelöl: a 4. oszlopot egészében és a 2. sort egészében. A felcserélt írások 3-as és 4-es sorindexű pixeleket akartak állítani egy mindössze három soros maszkban, és a bitmap-setter csendben elnyelte ezeket a tartományon kívüli írásokat. Ami megmaradt, az a 2. oszlop, 0-tól 2-ig terjedő sorai. A dekódor ezért két, az enkóder által kódolt mezőt hagyott ki, és hat, az enkóder által kihagyott mezőt dekódolt

PDFlibPas JBIG2 halftone kihagyómaszkok egy 5 × 3-as rácsra, ahol a helyes HSKIP[ng, mg] a 4. oszlopot és a 2. sort jelöli kihagyandóként, miközben a transzponált írások, amik egy háromsoros maszk 3. és 4. sorát célozták, csendben elenyésztek, és csak a 2. oszlop maradt meg, szinkronvesztést okozva az aritmetikai kóderekben
Csak egy nem négyzetes rács leplez le transzponált maszkot, és a keletkező kóder-szinkronvesztés nem hibát dob, hanem összekutyulja a régiót

Az aritmetikai dekódor nem bukik el, amikor ez megtörténik. Extra pixeleket dekódol olyan bitekből, amik későbbi mezőké, a kontextusai rossz szomszédokat olvasnak, és az első eltérés után minden pattern index zaj, ezért volt a tünet egy összekutyult régió, nem néhány félrement mező. Négyzetes rácson ugyanez a hiba gyakran láthatatlan: egyetlen felcserélt koordináta sem hagyja el a maszkot, és ha a régión kívüli mezők szimmetrikusak az átlóra, például egy rács, ami jobb és alsó szélén ugyanannyi mezővel lóg túl, a transzponált maszk bitenként a helyes maszk. A HENABLESKIP ráadásul opcionális, MMR-kódolt szürkeárnyalatos kép esetén 0 kell legyen, és az enkóderek ritkán állítják, így a hibának kevés felszínre útja volt. v3.539.37 óta az építő HSKIP[ng, mg]-et ír, és az elhelyező ciklus ugyanabban a sorrendben olvas

Miért három rétegben bukik el a negatív halftone rács offszet?

Egy olyan halftone rács, ami a régiójától balra vagy föntebb indul, három különböző helyen törte meg a PDFlibPast, és minden hiba elrejtette a következőt. A T.88 szándékosan megengedi ezt a geometriát. Egy enkóder, ami a raszterét az oldalhoz igazítja a régió helyett, vagy forgatott rácsot használ, magától értetődően negatív mezősarkokat produkál, amiket a régió levág. A v3.539.38 mindhárom réteget együtt javította, mert bármelyiket önmagában javítva csak a tünet változott

1. réteg: előjeles mező olvasása előjelentelenként

A T.88 §7.4.5.1.2-e az HGX-et és az HGY-t előjeles 32 bites értékként definiálja, a dekódor viszont ugyanazzal a 32 bites helperrel olvasta őket, amit az előjelentelen mezőkre használt, és az a helper minden negatív eredményt 0-ra szorított le. Egy HGX = -900-ról indulni szánt rács csendben a régió hipuspontjára csúszott. A v3.539.38 regressziós esetében az egész kép két sorral lejjebb jött ki a kelleténél. A leszorítás magyarázza azt is, miért éltek túl sokáig a másik két hiba: ha a hipuspont nemnegatívra van kényszerítve, negatív koordináta csak HRY > 0-s forgatott rácson át jelenhet meg, ahol a y = HGY + mg × HRX − ng × HRY a későbbi rácsoszlopokra nulla alá süllyed

2. réteg: a shr nem >> 8

A T.88 azt írja, >> 8, és aritmetikai tolást jelent, ami mínusz végtelen felé kerekít. A dekódor ezt shr 8-ként fordította le. Delphiben és Free Pascalban az előjeles egészre alkalmazott shr logikus tolás: a sign bit nullaként tolódik be. Egy -512-t tároló Integer esetén a shr 8 -2 helyett 16777214-et ad. Egy pattern, amit y = -2-re kellett volna rajzolni és az alsó felére levágni, 16 millió sorral lejjebb ment, és régión kívüliként eldobásra került. Semmi nem omlott össze; a halftone felső sora egyszerűen eltűnt

3. réteg: fixpont összehasonlítása pixelek helyett

A kihagyóteszt fixpontos értékeket hasonlított össze, nem pixelpozíciókat, és a kettő akkor nem ekvivalens, ha a tört nem nulla. Az eredeti kód a logikus tolást azzal kerülte ki, hogy a tolatlan értéken tesztelte a xx + HPW × 256 <= 0 feltételt, mint a T.88-teszt állítólagos megfelelőjét. HGX = -900 és 4 pixeles pattern mellett ez -900 + 1024 = 124-et ad, ami pozitív, így a mező nem marad ki. A szabvány előbb tol: floor(-900 / 256) = -4, és a -4 + 4 = 0 teljesíti az x + HPW <= 0 feltételt, így a mező teljesen kívül esik, és ki kell hagyni. Az enkóder kihagyta, a dekódor dekódolta, és a szürkeárnyalatos kép pontosan úgy sodródott, mint a transzponált maszkos esetben

PDFlibPas JBIG2 halftone hibák negatív HGX-ű rácson: egy előjeles mező előjelentelen helperen át olvasva nullára szorítódott le, a T.88 jobbtolás logikus shr-ként fordítódott le, ami egy patternt 16 millió sorral lejjebb küldött, és a fixpontos értékeken futó kihagyóteszt megtartott egy, az enkóder által kihagyott mezőt
Minden hiba elrejtette a következőt, ezért javította a v3.539.38 mindhárom réteget együtt, egyetlen közös HalftoneGridPixel helperben, amit a maszképítő és az elhelyező ciklus is használ

A v3.539.38 regressziós esete 4 × 3 rácsot használ 4 × 4-es patternekből HGX = -900, HGY = -512, HRX = 1024 értékekkel egy 12 × 10-es régión. A rácsoszlopok x = -4, 0, 4 és 8 helyre landolnak, így a 0. oszlop teljesen kívül esik, és HSKIP-be tartozik; a rácssorok y = -2, 2 és 6 helyre landolnak, így a 0. sort az alsó két pixelsorra kell levágni, nem eldobni. A rétegek egyenkénti javítása így reprodukálja a halmozódást:

Javított hibákDekódolt régió
Egyik sem (v3.539.38 előtt)Rács a hipuspontjára húzva, egész kép két sorral túl lejjebb
Csak az előjeles HGX / HGY olvasásAz első rácssor hiányzik, a többi a kihagyóteszt sodródásától zavaros
Előjeles olvasás, floor-tolás és pixelteres kihagyótesztPixelenként azonos a T.88 §6.6.5-ből számolt oldallal és két független referenciadekódorral

A javítás egyetlen helper, a HalftoneGridPixel, amiben a maszképítő és az elhelyező ciklus osztozik. A koordinátát Int64-ben akkumulálja, hogy egy nagy mg × HRX szorzat ne tudjon körbefordulni, 256-tal oszt mínusz végtelen felé kerekítve, és ±MaxInt div 2-re szorít le, hogy egy sérült rács ne tudja túlcsordultatni a későbbi bitmaparitmetikát. A kihagyóteszt most ezeket a pixelértékeket veti össze az HPW-vel, HPH-val, HBW-vel és HBH-val, pontosan úgy, ahogy a §6.6.5.1 kimondja

Hogyan írj Delphiben aritmetikai jobbtolást?

A Delphinek nincs aritmetikai toló operátora, ezért egy helyes előjeles jobbtolást floor-osztásként kell írni, az egyszerű div pedig nem az. A div nulla felé csonkol. Nemnegatív értékekre a csonkolás és a floor egyetért, és az osztó pontos többszörösei közé tartozó negatív értékekre is, ezért néz ki jól a -512 div 256 = -2 egy gyors tesztben. Minden más helyen eltérnek: a -900 div 256 -3, miközben a floor -4, az -1 div 256 0, miközben a floor -1. Egy nem nulla törtű JBIG2 koordináta pontosan az az eset, ahol a div rossz pixelt ad

A Delphi Win32 és Win64 kompilátorain egy -512-t tároló Integer változó 8-cal jobbra tolva 16777214-et ad, egy -512-t tároló Int64 pedig 72057594037927934-et. A Free Pascal a shr-t szintén logikus tolásként definiálja, és az aritmetikai változathoz szállítja a SarLongint-et és a SarInt64-et a System unitjában, de ezek a függvények Delphiben nincsenek meg, így a két kompilátor közt osztott kódnak saját helper kell:

// Floor osztás: mínusz végtelen felé kerekít A és B bármely előjelére.
// B nem lehet 0, és a FloorDiv(Low(Integer), -1) éppúgy túlcsordul, mint a 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;

// Aritmetikai jobbtolás (a C és a T.88 ">>"-je előjeles értékeken).
// Negatív Value esetén a not Value = -Value - 1 nemnegatív, így ott
// a logikus shr biztonságos, a külső not pedig visszatéríti az eredményt
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;

A not-trükk soha nem tol negatív számot, így nem függ attól, hogyan kezeli a kompilátor a sign bitet, és soha nem túlcsordul, a Low(Integer) esetét is beleértve. Mindkét helper több millió értéken egyezett egy Int64 floor-referenciával, 0-tól 31-ig minden tolással, és a Low(Integer) meg High(Integer) szélekkel Delphi Win32-n, Delphi Win64-en és Free Pascal x86_64-en. Egy józanész-ellenőrzés, amit érdemes tartani minden koordinátát érintő unit tesztben:

var
  V: Integer;
begin
  V := -900;
  Writeln(V shr 8);           // 16777212  logikus tolás, a régi hiba
  Writeln(V div 256);         // -3        csonkolás nulla felé
  Writeln(FloorDiv(V, 256));  // -4        ezt jelenti a T.88 a >> 8 alatt
  Writeln(SarInt32(V, 8));    // -4
end;
PDFlibPas számlineár a -900 koordinátára 8-cal jobbra tolva: a shr 16777212-et ad, a div -3-ra csonkol, a FloorDiv és a SarInt32 viszont egyaránt a floor-értéket, a -4-et találja el, amit az ITU-T T.88 jelent a tolás alatt, és ez csak akkor számít, ha a fixpontos tört nem nulla
A csonkolás és a floor csak pontos többszörösökön egyetért, ezért a -512 div 256 átmegy egy gyors teszten, a -900 div 256 pedig rossz pixelt választ

A Math.Floor(V / 256) is -4-et ad, de a Double-ön átfutó kitérője pontosságot veszít a 253 fölötti Int64 értékeknél, így az egész számokból álló geometriának egész számokban kell maradnia

Melyik PDFlibPas hívás futtatja a halftone dekódot?

A JBIG2 halftone dekódor akkor fut, amikor a PDFlibPas a beépített rendererrel renderel oldalt, mert a renderelésnek pixelekre van szüksége. A RenderPageToFile és a RenderPageToStream is az oldal JBIG2Decode képfolyain át éri el, így egy halftone oldal újrarendelése a legközvetlenebb módja annak, hogy ellenőrizd, változtat-e a kimenetedet a v3.539.38. Ugyanez a dekódór kezeli a többi JBIG2 régiótípust is, amiket a JBIG2 custom Huffman táblák a tiszta Pascal dekódorban és a random-access JBIG2 fájlok dekódolása Delphiben cikkek fednek le, a renderelt bitmap pedig táplál olyan konverziókat, mint a PDF oldalak renderelése 1 bites monochrome-ba

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
      // A renderelés minden JBIG2 régiót dekódol, a halftone-okat is
      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.

A képkiextrakció normál esetben más utat jár. A GetPageImageList natív formában adja vissza a JBIG2 képeket, a SaveImageListItemDataToFile vagy a GetImageListItemDataToString pedig egy önálló JBIG2 fájlt ad a kezedbe, amit a stream bájtokból épít: a fájlfej, a JBIG2Globals adat és egy end-of-file szegmens veszi körül az oldaladatot. A GetImageListItemIntProperty 400-as tulajdonsága ilyen elemre 6-ot jelent. Ezen az úton semmi nem dekódolódik, így egy kinyert .jb2, ami másik nézegetőben jónak néz ki, miközben a renderelt oldal zajt mutat, a két halftone hiba tipikus jele volt:

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  // önálló JBIG2
        Lib.SaveImageListItemDataToFile(ListID, I, 0,
          Format('page1-image%d.jb2', [I]));
  finally
    Lib.ReleaseImageList(ListID);
  end;
end;

Amikor maszkok vagy színkonverzió renderelt fallbacket kényszerítenek, az elem dekódolt bitmapként jön vissza, és a halftone dekódór tényleg fut. A képlistákról többet a Delphi PDF szöveg-, kép- és fontkiextrakció cikkben találsz

Gyorsreferencia: JBIG2 halftone rácsszabályok

  • A kihagyómaszkot indexeld HSKIP[ng, mg]-ként, rácsoszloppal elöl, és olvasd vissza ugyanabban a sorrendben, ahová csak mezőket helyezel (T.88 §6.6.5.1, javítva a PDFlibPas v3.539.37-ben)
  • Bármilyen halftone vagy rács kódot tesztelj nem négyzetes rácson és aszimmetrikus régión kívüli mezőhalmazon, mert egy négyzetes rács a transzponált indexet teljesen elrejtheti
  • Az HGX-et és az HGY-t olvasd előjeles 32 bites értékként (T.88 §7.4.5.1.2), soha előjelentelen helperen át, ami a negatívokat leszorítja
  • A szabvány >> 8-ját fordítsd 256-tal való floor-osztásként, ne shr 8-ként és ne div 256-ként
  • A kihagyótesztet tolt pixelpozíciókon futtasd; a fixpontos forma minden nem nulla tört esetén eltér, ahogy a HGX = -900 mutatja 4 pixeles patternnel
  • A rácskoordinátákat akkumuláld Int64-ben, és szorítsd le, mielőtt bitmap kódnak adnád, hogy egy sérült rács ne tudjon túlcsordulni
  • Frissíts v3.539.38-ra vagy újabbra, ha a dokumentumaid tartalmaznak HENABLESKIP-es halftone régiókat, negatív rácshipuspontokat vagy forgatott rácsokat

A PDFlibPas Delphiből és C++Builderből renderel, nyer ki és szerkeszt PDF dokumentumokat egy natív Pascal JBIG2 dekódorral, ami mostantól a T.88 által előírt módon kezeli a halftone kihagyómaszkokat, a negatív rácshipuspontokat és a forgatott rácsokat. Funkciókért, kiadásokért és próbaverzióért lásd a PDFlibPas Delphi PDF library oldalt