Articol tehnic

PDFlibPas: grile JBIG2 halftone, HSKIP, offset-uri negative

PDFlibPas a reparat două defecte independente din decoder-ul lui nativ de regiuni halftone JBIG2: în v3.539.37 masca de skip HSKIP e indexată ca HSKIP[ng, mg], cum o definește ITU-T T.88 §6.6.5.1, iar în v3.539.38 grilele care ajung la coordonate negative, printr-un HGX sau HGY negativ sau prin rotație, sunt plasate cu un floor shift adevărat. Înaintea acestor versiuni, regiunile halftone afectate ieșeau deformate sau deplasate, fără nicio eroare ridicată. Ambele bug-uri s-au ascuns după date de test care se întâmplau să fie simetrice sau non-negative, iar al doilea pornește de la o proprietate a Delphi și Free Pascal care mușcă mult dincolo de JBIG2: shr pe un întreg cu semn e un shift logic, nu >> aritmetic pe care standardul îl presupune

Regiunile halftone sunt cel mai rar tip de regiune JBIG2, astfel încât un decoder poate procesa mii de documente scanate înainte să întâlnească o fotografie cu raster ecranată codificată ca una. Când o face, eșecul e urât: fișierul se parsează, lungimile de segment se însumează, pagina are dimensiunea corectă, iar regiunea e gunoi

Ce decodează de fapt o regiune halftone JBIG2?

O regiune halftone JBIG2 e o grilă de bitmap-uri mici alese dintr-un dicționar de pattern-uri, iar munca reală a decoder-ului e calcularea unui index pentru fiecare celulă de grilă și a poziției de pixel acolo unde aterizează celula. Dicționarul de pattern-uri ține HNUMPATS pattern-uri de HPW × HPH pixeli. Segmentul de regiune halftone descrie apoi o grilă de HGW coloane pe HGH rânduri și o imagine în scale de gri de aceeași mărime, codificată ca bitplane-uri Gray. Fiecare bitplane e decodată cu procedura de regiune generică peste un bitmap HGW × HGH, planul cel mai semnificativ primul, iar planele împreună îi dau fiecărei celule indexul de pattern

Plasarea celulelor folosește aritmetică în virgulă fixă cu o fracție pe 8 biți. Originea grilei HGX, HGY e o pereche de valori pe 32 de biți, iar vectorul grilei HRX, HRY descrie pasul dintre celulele vecine, ceea ce permite o grilă rotită. Pentru rândul de grilă mg și coloana de grilă ng, T.88 §6.6.5 calculează poziția de pixel astfel:

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

Masca de skip intră prin flag-ul opțional HENABLESKIP. Când flag-ul e setat, §6.6.5.1 construiește un bitmap HGW × HGH HSKIP și setează HSKIP[ng, mg] la 1 pentru fiecare celulă al cărei pattern stă complet în afara regiunii: x + HPW <= 0, x >= HBW, y + HPH <= 0 sau y >= HBH. Bitplane-urile în scale de gri sunt apoi decodate cu masca aceea drept bitmap de skip al regiunii generice, astfel încât decoder-ul aritmetic nici nu citește, nici nu actualizează contextul pentru o celulă sărită. Decoder-ul și encoder-ul trebuie să fie de acord pe fiecare bit din HSKIP, altfel cei doi codezi aritmetici ies din pas

De ce o mască HSKIP transpusă a stricat doar grilele non-pătrate?

Masca de skip era scrisă cu coordonatele inversate, și doar o grilă non-pătrată o expunea, pentru că o grilă pătrată ține fiecare coordonată inversată în interiorul măștii. PDFlibPas stochează bitmap-uri cu un accesor de pixel (column, row), iar codul care construia masca pasa (mg, ng), rândul primul. Decoder-ul de bitplane-uri în scale de gri citește masca corect ca (ng, mg). Bucla de plasare a pattern-urilor o citea înapoi în ordinea inversată a constructorului, deci cei doi erau de acord, iar o revizuire a singurei logici de plasare ar fi trecut-o. O capcană de denumire înrăutăți situația: în bucla de plasare variabila numită col iterează rânduri de grilă, iar Row iterează coloane de grilă

Luați grila 5 × 3 de pattern-uri 4 × 4 pe o regiune 16 × 8 pe care v3.539.37 o folosește drept caz de regresie. Cu HRX = 1024 și HRY = 0, coloana de grilă 4 aterizează la x = 16, iar rândul de grilă 2 la y = 8, ambele în afara regiunii. Masca corectă marchează șapte celule: toată coloana 4 și tot rândul 2. Scrierile inversate au încercat să seteze pixeli la indici de rând 3 și 4 într-o mască înaltă de doar trei rânduri, iar setter-ul de bitmap a ignorat în tăcere acele scrieri în afara intervalului. Ce a supraviețuit a fost coloana 2, rândurile 0 până la 2. Decoder-ul a sărit deci două celule pe care encoder-ul le codase și a decodat șase celule pe care encoder-ul le sărise

Măști de skip halftone JBIG2 în PDFlibPas pentru o grilă 5 pe 3, unde HSKIP[ng, mg] corect marchează coloana 4 și rândul 2 ca sărite, în timp ce scrierile transpuse țintite pe rândurile 3 și 4 ale unei măști de trei rânduri au fost aruncate în tăcere și doar coloana 2 a supraviețuit, desincronizând codezii aritmetici
Doar o grilă non-pătrată expune o mască transpusă, iar desincronizarea de codezi rezultată strică regiunea în loc să ridice o eroare

Decoder-ul aritmetic nu eșuează când se întâmplă asta. El decodează pixeli în plus din biți care aparțin celulelor ulterioare, contextele lui citesc vecini greșiți, iar fiecare index de pattern după prima dezacord e zgomot, motiv pentru care simptomul a fost o regiune stricată în loc de câteva celule deplasate. Pe o grilă pătrată același bug e adesea invizibil: nicio coordonată inversată nu iese din mască, iar când celulele din afara regiunii sunt simetrice față de diagonală, o grilă care atârnă peste laturile din dreapta și de jos cu același număr de celule, de pildă, masca transpusă e bit cu bit cea corectă. HENABLESKIP e și opțional, trebuie să fie 0 când imaginea în scale de gri e codificată MMR și e rar setat de encodezi, deci bug-ul avea foarte puține căi de a ieși la suprafață. Din v3.539.37 constructorul scrie HSKIP[ng, mg], iar bucla de plasare citește aceeași ordine

De ce eșuează offset-urile negative de grilă halftone în trei straturi?

O grilă halftone care pornește la stânga sau deasupra regiunii ei a stricat PDFlibPas în trei locuri separate, iar fiecare defect ascundea următorul. T.88 permite această geometrie în mod deliberat. Un encoder care își aliniază raster-ul la pagină, nu la regiune, sau folosește o grilă rotită, produce în mod natural colțuri de celule negative pe care regiunea le decupează. v3.539.38 a reparat toate trei straturile împreună, pentru că repararea oricăruia singur ar fi schimbat doar simptomul

Stratul 1: un câmp cu semn citit ca fără semn

T.88 §7.4.5.1.2 definește HGX și HGY ca valori pe 32 de biți cu semn, dar decoder-ul le citea cu același helper pe 32 de biți folosit pentru câmpurile fără semn, iar helper-ul acela limita fiecare rezultat negativ la 0. O grilă menită să pornească la HGX = -900 era mutată pe furiș pe originea regiunii. În cazul de regresie din v3.539.38 toată imaginea ieșea cu două rânduri mai jos. Limitarea explică și de ce celelalte două defecte au supraviețuit atât: cu originea forțată non-negativă, o coordonată negativă putea apărea doar printr-o grilă rotită cu HRY > 0, unde y = HGY + mg × HRX − ng × HRY coboară sub zero pentru coloanele de grilă ulterioare

Stratul 2: shr nu e >> 8

T.88 scrie >> 8 și înțelege un shift aritmetic, care rotunjește spre minus infinit. Decoder-ul l-a tradus ca shr 8. În Delphi și Free Pascal, shr pe un întreg cu semn e un shift logic: bitul de semn este adus în ca zero. Pentru un Integer care ține -512, shr 8 dă 16777214 în loc de -2. Un pattern care ar fi trebuit desenat la y = -2 și decupat la jumătatea lui de jos a fost trimis cu 16 milioane de rânduri în jos și aruncat ca în afara regiunii. Nimic nu s-a blocat; rândul de sus al halftone-ului a dispărut pur și simplu

Stratul 3: comparare în virgulă fixă în loc de pixeli

Testul de skip compara valori în virgulă fixă, nu poziții de pixel, iar cele două nu sunt echivalente odată ce fracția e non-zero. Codul original ocoli shift-ul logic testând xx + HPW × 256 <= 0 pe valoarea neshifată, un presupus echivalent al testului T.88. Cu HGX = -900 și un pattern de 4 pixeli, asta dă -900 + 1024 = 124, care e pozitiv, deci celula nu e sărită. Standardul shiftează întâi: floor(-900 / 256) = -4, iar -4 + 4 = 0 satisface x + HPW <= 0, deci celula stă complet în afara și trebuie sărită. Encoder-ul a sărit-o, decoder-ul a decodat-o, iar imaginea în scale de gri a derivat exact ca în cazul măștii transpuse

Defecte halftone JBIG2 în PDFlibPas pentru o grilă la HGX negativ: un câmp cu semn citit printr-un helper fără semn limitat la zero, shift-ul dreapta T.88 tradus ca un shr logic care a trimis un pattern cu 16 milioane de rânduri în jos și un test de skip pe valori în virgulă fixă care a păstrat o celulă sărită de encoder
Fiecare defect ascundea următorul, motiv pentru care v3.539.38 a reparat toate trei straturile împreună într-un singur helper partajat HalftoneGridPixel folosit de constructorul măștii și de bucla de plasare

Cazul de regresie din v3.539.38 folosește o grilă 4 × 3 de pattern-uri 4 × 4 la HGX = -900, HGY = -512, HRX = 1024 pe o regiune 12 × 10. Coloanele de grilă aterizează la x = -4, 0, 4 și 8, deci coloana 0 stă complet în afara și intră în HSKIP; rândurile de grilă aterizează la y = -2, 2 și 6, deci rândul 0 trebuie decupat la cele două rânduri de pixeli de jos, nu aruncat. Repararea straturilor câte unul reproduce stiva:

Defecte reparateRegiunea decodată
Niciunul (înainte de v3.539.38)Grilă trasă la origine, toată imaginea cu două rânduri mai jos
Doar citirea cu semn a HGX / HGYPrimul rând de grilă lipsă, restul stricat de derivarea testului de skip
Citire cu semn, floor shift și test de skip în spațiul de pixeliIdentică, pixel cu pixel, cu pagina calculată din T.88 §6.6.5 și cu doi decoderi de referință independenți

Repararea e un singur helper, HalftoneGridPixel, partajat de constructorul măștii de skip și de bucla de plasare. El acumulează coordonata în Int64, astfel încât un produs mare mg × HRX să nu poată da wrap, împarte la 256 rotunjit spre minus infinit și limitează la ±MaxInt div 2, astfel încât o grilă coruptă să nu poată da overflow în aritmetica de bitmap de mai târziu. Testul de skip compară acum acele valori de pixel cu HPW, HPH, HBW și HBH, exact cum îl enunță §6.6.5.1

Cum scrii un shift dreapta aritmetic în Delphi?

Delphi nu are operator de shift aritmetic, deci un shift dreapta corect cu semn trebuie scris ca o diviziune floor, iar div simplu nu e diviziunea aceea. div trunchiază spre zero. Pentru valori non-negative trunchierea și floor-ul sunt de acord, și sunt de acord și pentru valorile negative care sunt multipli exacți ai divizorului, motiv pentru care -512 div 256 = -2 pare în regulă într-un test rapid. Nu sunt de acord oriunde altundeva: -900 div 256 e -3, în timp ce floor-ul e -4, iar -1 div 256 e 0, în timp ce floor-ul e -1. O coordonată JBIG2 cu fracție non-zero e exact cazul în care div dă pixelul greșit

Pe compilatoarele Delphi Win32 și Win64, o variabilă Integer care ține -512 shifată la dreapta cu 8 dă 16777214, iar un Int64 care ține -512 dă 72057594037927934. Free Pascal definește și el shr ca shift logic și livrează SarLongint și SarInt64 în unitatea lui System pentru varianta aritmetică, dar funcțiile acelea nu există în Delphi, deci codul partajat între cele două compilatoare are nevoie de propriul helper:

// Diviziune floor: rotunjește spre minus infinit pentru orice semn al lui A și B.
// B nu trebuie să fie 0, iar FloorDiv(Low(Integer), -1) dă overflow exact ca 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;

// Shift dreapta aritmetic (">>" din C și T.88 pe valori cu semn).
// Pentru Value negativ, not Value = -Value - 1 e non-negativ, deci
// shr-ul logic e sigur acolo, iar not-ul exterior mapează rezultatul înapoi
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;

Trucul cu not nu shiftează niciodată un număr negativ, deci nu depinde de felul în care un compilator tratează bitul de semn și nu dă niciodată overflow, inclusiv pentru Low(Integer). Ambele helper-e s-au potrivit cu o referință floor Int64 pe câteva milioane de valori, fiecare shift de la 0 la 31 și marginile Low(Integer) și High(Integer) pe Delphi Win32, Delphi Win64 și Free Pascal x86_64. O verificare de bun simț, de păstrat în orice test de unitate care atinge coordonate:

var
  V: Integer;
begin
  V := -900;
  Writeln(V shr 8);           // 16777212  shift logic, vechiul bug
  Writeln(V div 256);         // -3        trunchiere spre zero
  Writeln(FloorDiv(V, 256));  // -4        ce înseamnă T.88 prin >> 8
  Writeln(SarInt32(V, 8));    // -4
end;
Dreaptă de numere PDFlibPas pentru coordonata -900 shifată la dreapta cu 8: shr dă 16777212, div trunchiază la -3, în timp ce FloorDiv și SarInt32 aterizează ambele pe valoarea floor -4 pe care înseamnă-o ITU-T T.88 prin shift, ceea ce contează doar când fracția în virgulă fixă e non-zero
Trunchierea și floor-ul sunt de acord doar pe multiplii exacți, astfel încât -512 div 256 trece un test rapid, iar -900 div 256 alege pixelul greșit

Math.Floor(V / 256) întoarce și el -4, dar ocolul lui prin Double pierde precizie pentru valori Int64 peste 253, deci geometria în întregi ar trebui să rămână în întregi

Ce apeluri PDFlibPas rulează decoder-ul de halftone?

Decoder-ul de halftone JBIG2 rulează când PDFlibPas randează o pagină cu renderer-ul integrat, pentru că randarea are nevoie de pixeli. RenderPageToFile și RenderPageToStream ajung ambele la el prin stream-urile de imagini JBIG2Decode ale paginii, deci re-randarea unei pagini halftone e calea directă de a confirma că v3.539.38 îți schimbă output-ul. Același decoder se ocupă și de celelalte tipuri de regiuni JBIG2, acoperite în tabelele Huffman personalizate JBIG2 din decoder-ul Pascal pur și decodarea fișierelor JBIG2 cu acces aleator în Delphi, iar bitmap-ul randat alimentează conversii precum randarea paginilor PDF în monocrom pe 1 bit

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
      // Randarea decodează fiecare regiune JBIG2, halftone-urile incluse
      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.

Extragerea de imagini merge de obicei pe altă cale. GetPageImageList întoarce imaginile JBIG2 în formă nativă, iar SaveImageListItemDataToFile sau GetImageListItemDataToString vă pune în mână un fișier JBIG2 de sine stătător construit din octeții stream-ului: header-ul de fișier, datele JBIG2Globals și un segment de end-of-file în jurul datelor paginii. Proprietatea 400 a lui GetImageListItemIntProperty raportează 6 pentru un asemenea element. Nimic nu e decodat pe calea aceea, deci un .jb2 extras care arată corect într-un alt viewer în timp ce pagina randată arată zgomot era semnul tipic al acestor două bug-uri de halftone:

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;

Când măștile sau conversia de culoare forțează un fallback randat, elementul revine ca bitmap decodată, iar decoder-ul de halftone chiar rulează. Mai multe despre listele de imagini în extragerea de text, imagini și fonturi din PDF în Delphi

Referință rapidă: regulile grilei halftone JBIG2

  • Indexați masca de skip ca HSKIP[ng, mg], coloana de grilă prima, și citiți-o înapoi în aceeași ordine oriunde sunt plasate celule (T.88 §6.6.5.1, reparat în PDFlibPas v3.539.37)
  • Testați orice cod de halftone sau de grilă cu o grilă non-pătrată și o mulțime asimetrică de celule din afara regiunii, pentru că o grilă pătrată poate ascunde complet un index transpus
  • Citiți HGX și HGY ca valori pe 32 de biți cu semn (T.88 §7.4.5.1.2), niciodată printr-un helper fără semn care limitează negativele
  • Traduceți >> 8 din standard ca o diviziune floor la 256, nu ca shr 8 și nu ca div 256
  • Rulați testul de skip pe poziții de pixel shifate; forma în virgulă fixă diferă ori de câte ori fracția e non-zero, cum arată HGX = -900 cu un pattern de 4 pixeli
  • Acumulați coordonatele de grilă în Int64 și limitați înainte de a le da codului de bitmap, astfel încât o grilă coruptă să nu poată da overflow
  • Faceți upgrade la v3.539.38 sau mai nou dacă documentele conțin regiuni halftone cu HENABLESKIP, origini de grilă negative sau grile rotite

PDFlibPas randează, extrage și editează documente PDF din Delphi și C++Builder cu un decoder JBIG2 Pascal nativ care se ocupă acum de măști de skip halftone, origini de grilă negative și grile rotite așa cum specifică T.88. Veziți PDFlibPas Delphi PDF library pentru funcții, ediții și o descărcare de probă