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) >> 8y = (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
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
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ák | Dekó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ás | Az 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óteszt | Pixelenké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;
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 azHGY-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, neshr 8-ként és nediv 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 = -900mutatja 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