Un contract scanat de cincizeci de pagini repetă același alfabet pe fiecare pagină, dar un encoder JBIG2 care construiește câte un dicționar de simboluri pentru fiecare imagine re-antrenează acel alfabet de cincizeci de ori separate. HotPDF, componenta PDF nativă pentru Delphi și C++Builder, poate în schimb acumula un singur dicționar de simboluri partajat pe tot parcursul documentului și îl poate promova la un singur flux /JBIG2Globals la nivel de document, astfel încât propriul flux JBIG2 al fiecărei pagini doar referă ID-uri de simboluri, în loc să stocheze propria copie a alfabetului
Acest articol rămâne deliberat restrâns și acoperă doar modul în care HotPDF construiește intern această partajare între pagini — fundamentele JBIG2, comparația cu CCITT și compromisurile Lossless versus LossyLevel trăiesc deja în articolul complementar despre compresia bilevel JBIG2 nativă în Delphi, pe care acest articol presupune că l-ați citit
De ce compresia JBIG2 per-pagină tot repetă același cost?
Răspunsul este că nimic nu poartă starea între apeluri. De fiecare dată când encoder-ul HotPDF construiește un dicționar de simboluri pentru o imagine, acel dicționar este limitat la acel unic apel AddImage: trecerea de potrivire a formelor pornește de la zero, fiecare glifă de pe pagină este clasificată ca nouă, iar bitmap-urile rezultate sunt codificate aritmetic și stocate din nou. Alimentați același encoder cu cincizeci de pagini scrise cu același font, și el repetă fericit întreaga acea trecere de antrenare de cincizeci de ori, pentru că din punctul său de vedere fiecare pagină este o imagine fără legătură care se întâmplă să arate similar. UseSymbolDictionary per-pagină deja depășește cu mult o codificare simplă cu regiune generică pe o singură pagină, dar se plafonează cu mult sub tavanul pe care o scanare reală multi-pagină îl lasă neexploatat
Cum partajează HotPDF un singur dicționar de simboluri între pagini?
Activați AccumulateGlobalsAcrossPages pe THPDFJBIG2Options, iar HotPDF păstrează un singur dicționar de simboluri viu în memorie pe toată durata documentului, în loc să îl arunce după fiecare imagine. Glifele fiecărei pagini ulterioare sunt verificate față de acel dicționar în curs de rulare înainte ca orice să fie re-codificat: o formă care există deja este refolosită prin ID-ul său de simbol, și doar o formă pe care nimeni nu a mai văzut-o este adăugată și codificată în dicționar. Comparația refolosește aceeași logică de toleranță pe care LossyLevel o aplică pe o singură pagină — o scanare ușor zgomotoasă a aceleiași litere tot contează ca o potrivire — astfel încât acumulatorul nu se umflă silențios într-o intrare de dicționar per fiecare variație la nivel de pixel a aceleiași glife. Extragerea se întâmplă prima și alimentează acea comparație: HotPDF parcurge bitmap-ul fiecărei pagini și extrage forme conectate prin umplere prin inundare (flood fill) față de pixelii negri, aceeași idee ca trasarea manuală a petelor de cerneală, și acele forme extrase, nu blocuri brute de pixeli, sunt cele comparate față de dicționarul în curs de rulare
Cum stă dicționarul partajat în interiorul unui flux /JBIG2Globals
Dicționarul acumulat este scris ca un singur segment de dicționar de simboluri în interiorul fluxului /JBIG2Globals, ținut la un număr de segment fix, astfel încât fiecare pagină poate indica spre aceeași țintă. În interiorul organizării JBIG2 încorporate pe care o definește ISO 32000-1 §7.4.7, un segment de regiune de text poate numi un alt segment ca sursă a sa de simboluri prin câmpul de segment-referit din antetul segmentului, iar acesta este exact mecanismul pe care se sprijină HotPDF: fluxul globals poartă marele dicționar unic de simboluri, iar propriul flux JBIG2 al fiecărei pagini se reduce la un segment de informații de pagină plus un segment de regiune de text a cărui listă de referiri indică înapoi spre segmentul globals. Ce era anterior un flux binar auto-conținut per pagină devine o listă scurtă de poziții și ID-uri de simboluri, iar fiecare pagină construită astfel referă obiectul indirect identic /JBIG2Globals, nu o copie a lui. Propria acoperire de regresie a HotPDF verifică exact asta: codifică un document scurt unde fiecare pagină are un aspect diferit de glife, îl reîncarcă și numără câte referințe distincte la obiectul /JBIG2Globals apar în fișier — un document, o singură referință de obiect, indiferent de câte pagini au contribuit simboluri la el
Activarea acumulării dicționarului de simboluri între pagini
Comutatorul stă pe aceeași înregistrare de opțiuni acoperită în articolul complementar, și necesită patru setări în acord unele cu altele înainte ca acumularea să se activeze efectiv
var
Pdf: THotPDF;
Bmp: TBitmap;
PageIdx, ImgIdx: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.JBIG2Options.Lossless := True;
Pdf.JBIG2Options.UseSymbolDictionary := True;
Pdf.JBIG2Options.UseGlobalSegments := True;
Pdf.JBIG2Options.AccumulateGlobalsAcrossPages := True; // opt-in, default False
Pdf.JBIG2Options.UseExternalEncoder := False; // accumulation needs the native path
Pdf.JBIG2Options.UseNativeArithmeticFallback := True;
Pdf.BeginDoc;
for PageIdx := 0 to ScannedPages.Count - 1 do
begin
if PageIdx > 0 then
Pdf.AddPage;
Bmp := ScannedPages[PageIdx]; // 1-bit TBitmap for this page
ImgIdx := Pdf.AddImage(Bmp, icJBIG2);
Pdf.CurrentPage.ShowImage(ImgIdx, 0, 0, Bmp.Width, Bmp.Height, 0);
end;
Pdf.EndDoc; // the shared /JBIG2Globals stream is finalized here
finally
Pdf.Free;
end;
end;
Această asociere nu este o decorație opțională. Interfața de encoder extern descrisă în articolul despre compresia bilevel — cea pe care o înregistrați prin RegisterJBIG2EncoderBackend pentru rate de compresie de nivel de producție — este construită în jurul codificării per-imagine, iar propriile demonstrații de acumulare și teste de regresie ale HotPDF asociază întotdeauna AccumulateGlobalsAcrossPages cu UseExternalEncoder := False. Tratați asta ca pe o cerință dură, nu ca pe o sugestie: partajarea între pagini este o capabilitate a encoder-ului nativ, iar un backend extern înregistrat pur și simplu nu face parte din calea care construiește dicționarul partajat
Cât de mult mai mică devine efectiv o scanare multi-pagină?
Răspunsul onest începe cu ce nu a mișcat acul mai întâi. O lansare anterioară a adăugat un cache adresat după conținut pentru fluxurile /JBIG2Globals — o căutare cheiată de un hash FNV-1a pe 64 de biți al octeților fluxului, astfel încât două imagini care s-au întâmplat să producă date globals identice octet-cu-octet ar putea partaja un singur obiect PDF. Măsurat față de rezultatul real, acel cache a ajutat abia sesizabil, pentru că detecția existentă a HotPDF de imagini duplicate întregi colapsa deja imaginile identice octet-cu-octet înainte ca cache-ul să apuce vreodată să ruleze. Lecția a fost că deduplicarea la nivel de flux se plătește doar odată ce două imagini de pagină cu adevărat diferite pot totuși partaja un dicționar în creștere, ceea ce oferă acumularea reală între pagini
Pentru acel caz mai dificil, propria estimare de inginerie a HotPDF plasează economia suplimentară la aproximativ 30 până la 60 la sută mai mică decât obține doar deduplicarea la nivel de flux, pentru o scanare tipică multi-pagină construită dintr-un font recurent — intervalul se mișcă în funcție de cât de mult din vocabularul vizual al documentului chiar se repetă, întrucât o pagină plină de diagrame unice nu oferă dicționarului nimic de refolosit. Tratați asta ca pe o țintă de design, nu ca pe o garanție pentru orice intrare specifică, și măsurați-vă propriile documente, în loc să vă bazați pe un singur număr. Demonstrația JBIG2Benchmark livrată cu HotPDF există exact pentru acest scop: codifică aceeași scanare multi-pagină în patru moduri diferite și tipărește dimensiunea rezultată a fișierului pentru fiecare configurație, astfel încât comparația rulează pe propriul dvs. mix de scanări, nu pe unul sintetic
procedure RunScenario(const Title: string; AccumulateGlobals: Boolean);
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.JBIG2Options.Lossless := True;
Pdf.JBIG2Options.UseSymbolDictionary := True;
Pdf.JBIG2Options.UseGlobalSegments := True;
Pdf.JBIG2Options.AccumulateGlobalsAcrossPages := AccumulateGlobals;
Pdf.JBIG2Options.UseExternalEncoder := not AccumulateGlobals;
// ... encode the same three-page scan here, then compare file sizes.
finally
Pdf.Free;
end;
end;
begin
RunScenario('Per-image lossless baseline', False);
RunScenario('Cross-page accumulated globals', True);
end.
Unde întâlnește acumularea între pagini limitele sale
Dicționarul acumulat este plafonat la 4096 de simboluri, același prag pe care encoder-ul nativ per-imagine îl impune deja pe o singură pagină. Depășiți acea limită la mijlocul documentului, iar HotPDF nu ridică o excepție și nici nu abandonează rularea: acumulatorul refuză noua glifă, iar pagina care a introdus-o revine automat la codificare independentă per-imagine, astfel încât documentul tot iese corect — doar că încetați să mai obțineți economia între pagini pentru oricare din paginile care au depășit pragul. O a doua măsură de siguranță urmărește dimensiunea totală, nu numărul de simboluri: odată ce lățimea combinată a simbolurilor din dicționarul acumulat depășește 131071 de pixeli, HotPDF varsă automat lotul curent pe disc și pornește un grup globals nou, în loc să lase o structură unică din memorie să crească fără limită. Niciuna din limite nu necesită vreun cod din partea dvs., întrucât ambele sunt reveniri automate, nu excepții pe care trebuie să le prindeți
Conformitatea PDF/A este singura setare care oprește întregul mecanism, în loc doar să îl plafoneze. HotPDF substituie discret CCITT Group 4 pentru JBIG2 din momentul în care PDFACompliance este ne-gol, pe fiecare pagină, independent de AccumulateGlobalsAcrossPages sau orice altceva din JBIG2Options — o alegere de conformitate deliberată, nu un bug, dar înseamnă că un profil de arhivare și partajarea de simboluri între pagini se exclud reciproc astăzi. Indiferent de configurația la care ajungeți, decodificați ce ați scris înainte de a avea încredere în asta: încărcați fișierul înapoi cu LoadFromFile și extrageți fiecare pagină prin ExtractLoadedImage, care rezolvă globals-ul partajat pentru dvs. la fel cum ar face-o orice cititor conform, și comparați rezultatul cu bitmap-urile dvs. sursă
var
Loaded: THotPDF;
PageBmp: TBitmap;
PageIdx: Integer;
begin
Loaded := THotPDF.Create(nil);
try
Loaded.LoadFromFile('scanned-contract.pdf');
for PageIdx := 0 to Loaded.PagesCount - 1 do
begin
PageBmp := Loaded.ExtractLoadedImage(PageIdx); // resolves the shared globals for you
try
// Compare PageBmp against the source bitmap for this page.
finally
PageBmp.Free;
end;
end;
finally
Loaded.Free;
end;
end;
Partajarea dicționarului între pagini atinge doar partea de imagine bilevel a unui document. Dacă aceeași conductă emite de asemenea pagini de text generate alături de scanări — coperte, pagini de index, un strat de text OCR — fluxurile de obiecte și fluxurile xref atacă cealaltă jumătate a bugetului de dimensiune a fișierului comprimând structura documentului pe care acele pagini o adaugă. Partajarea globals JBIG2 între pagini este livrată ca parte a componentei HotPDF pentru Delphi și C++Builder, alături de opțiunile JBIG2 per-imagine și restul conductei de compresie