Odborný článok

Zdieľanie slovníkov symbolov JBIG2 naprieč stránkami v Delphi

Päťdesiatstranová naskenovaná zmluva opakuje tú istú abecedu na každej stránke, no enkodér JBIG2, ktorý stavia jeden slovník symbolov na obrázok, túto abecedu znovu natrénuje päťdesiatkrát oddelene. HotPDF, natívny PDF komponent pre Delphi a C++Builder, dokáže namiesto toho hromadiť jeden zdieľaný slovník symbolov naprieč celým dokumentom a povýšiť ho na jediný prúd /JBIG2Globals na úrovni dokumentu, takže vlastný prúd JBIG2 každej stránky iba odkazuje na ID symbolov namiesto toho, aby ukladal vlastnú kópiu abecedy

Tento text je zámerne úzko zameraný a pokrýva iba to, ako HotPDF interne buduje toto zdieľanie naprieč stránkami — základy JBIG2, porovnanie s CCITT a kompromisy medzi Lossless a LossyLevel už žijú v sprievodnom článku o natívnej dvojúrovňovej kompresii JBIG2 v Delphi, ktorý tento text predpokladá, že ste už čítali

Prečo kompresia JBIG2 po stránkach stále opakuje rovnaké náklady?

Odpoveďou je, že nič neprenáša stav medzi volaniami. Zakaždým, keď enkodér HotPDF postaví slovník symbolov pre jeden obrázok, je tento slovník obmedzený na jedno volanie AddImage: prechod porovnávania tvarov začína od nuly, každý glyf na stránke sa klasifikuje ako nový, a výsledné bitmapy sa arithmeticky zakódujú a uložia nanovo. Nakŕmte tým istým enkodérom päťdesiat stránok sadzaných tým istým písmom a ochotne zopakuje celý tento trénovací prechod päťdesiatkrát, pretože z jeho pohľadu je každá stránka nesúvisiaci obrázok, ktorý sa náhodou podobá. Slovník symbolov na stránku UseSymbolDictionary už na jednej stránke poráža plochý generický zápis regiónu s výrazným náskokom, no vrcholí ďaleko pred stropom, ktorý skutočný viacstranový sken necháva na stole nevyužitý

Ako HotPDF zdieľa jeden slovník symbolov naprieč stránkami?

Zapnite AccumulateGlobalsAcrossPages na THPDFJBIG2Options a HotPDF udrží jeden slovník symbolov nažive v pamäti počas celej životnosti dokumentu namiesto toho, aby ho po každom obrázku zahodil. Glyfy každej nasledujúcej stránky sa skontrolujú voči tomuto bežiacemu slovníku ešte predtým, než sa čokoľvek znova zakóduje: tvar, ktorý už existuje, sa znova použije podľa svojho ID symbolu, a iba tvar, ktorý ešte nikto nevidel, sa pripojí a zakóduje do slovníka. Porovnanie znova používa rovnakú logiku tolerancie, akú LossyLevel aplikuje na jednej stránke — mierne zašumený sken toho istého písmena sa stále počíta ako zhoda — takže akumulátor sa ticho nenafúkne na jednu položku slovníka za pixelovú variáciu toho istého glyfu. Extrakcia prebieha ako prvá a napája toto porovnanie: HotPDF prejde bitmapu každej stránky a vytiahne spojené tvary cez flood fill voči čiernym pixelom, rovnaká myšlienka ako ručné obkresľovanie atramentových škvŕn, a práve tieto extrahované tvary, nie surové pixelové bloky, sa porovnávajú voči bežiacemu slovníku

Ako zdieľaný slovník sedí vnútri prúdu /JBIG2Globals

Akumulovaný slovník sa zapíše ako jeden segment slovníka symbolov vnútri prúdu /JBIG2Globals, uložený na pevnom čísle segmentu, aby naň mohla ukazovať každá stránka. Vnútri vloženej organizácie JBIG2, ktorú definuje ISO 32000-1 §7.4.7, môže segment textového regiónu pomenovať iný segment ako svoj zdroj symbolov cez pole odkazovaného segmentu v hlavičke segmentu, a práve o tento mechanizmus sa HotPDF opiera: prúd globals nesie ten jeden veľký slovník symbolov, a vlastný prúd JBIG2 každej stránky sa scvrkne na segment informácií o stránke plus segment textového regiónu, ktorého zoznam odkazovaných segmentov ukazuje späť na segment globals. To, čo bývalo samostatným bitovým prúdom na stránku, sa stáva krátkym zoznamom pozícií a ID symbolov, a každá stránka postavená týmto spôsobom odkazuje na identický nepriamy objekt /JBIG2Globals namiesto jeho kópie. Vlastné regresné pokrytie HotPDF kontroluje presne toto: zakóduje krátky dokument, kde má každá stránka odlišné rozloženie glyfov, znova ho načíta a spočíta, koľko odlišných odkazov na objekt /JBIG2Globals sa v súbore objaví — jeden dokument, jeden odkaz na objekt, bez ohľadu na to, koľko stránok doň prispelo symbolmi

Zapnutie akumulácie slovníka symbolov naprieč stránkami

Prepínač sedí na tom istom zázname volieb opísanom v sprievodnom článku a vyžaduje, aby sa štyri nastavenia navzájom zhodli skôr, než sa akumulácia skutočne zapne

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;

Toto párovanie nie je voliteľná ozdoba. Rozhranie externého enkodéra opísané v článku o dvojúrovňovej kompresii — to, ktoré registrujete cez RegisterJBIG2EncoderBackend kvôli produkčným kompresným pomerom — je postavené okolo kódovania po jednotlivých obrázkoch, a vlastné demá akumulácie aj regresné testy HotPDF vždy párujú AccumulateGlobalsAcrossPages s UseExternalEncoder := False. Berte to ako tvrdú požiadavku, nie ako odporúčanie: zdieľanie naprieč stránkami je funkcia natívneho enkodéra, a registrovaný externý backend jednoducho nie je súčasťou cesty, ktorá stavia zdieľaný slovník

O koľko sa viacstranový sken v skutočnosti zmenší?

Čestná odpoveď začína tým, čo najprv vôbec nepohlo ihlou. Skoršie vydanie pridalo pre prúdy /JBIG2Globals vyrovnávaciu pamäť adresovanú podľa obsahu — vyhľadávanie kľúčované 64-bitovým hashom FNV-1a bajtov prúdu, takže dva obrázky, ktoré náhodou vyprodukovali bajtovo identické dáta globals, mohli zdieľať jeden objekt PDF. Pri meraní voči skutočnému výstupu táto vyrovnávacia pamäť pomohla sotva, pretože existujúca detekcia celoobrázkových duplicít v HotPDF už zvyčajne zbalila bajtovo identické obrázky skôr, než vyrovnávacia pamäť vôbec dostala šancu zabehnúť. Poučenie bolo, že deduplikácia na úrovni prúdu sa oplatí až vtedy, keď si dva skutočne odlišné obrázky stránok stále môžu deliť jeden rastúci slovník, čo je presne to, čo prináša skutočná akumulácia naprieč stránkami

Pre tento náročnejší prípad vlastný inžiniersky odhad HotPDF kladie dodatočnú úsporu na približne 30 až 60 percent menšiu, než dosahuje samotná deduplikácia na úrovni prúdu, pri typickom viacstranovom skene postavenom z jedného opakujúceho sa písma — rozsah sa mení podľa toho, koľko zo vizuálneho slovníka dokumentu sa skutočne opakuje, keďže stránka plná unikátnych diagramov nedá slovníku nič na opätovné použitie. Berte to ako návrhový cieľ, nie ako záruku pre akýkoľvek konkrétny vstup, a merajte vlastné dokumenty namiesto toho, aby ste dôverovali jedinému číslu. Demo JBIG2Benchmark, ktoré sa dodáva s HotPDF, existuje presne na tento účel: zakóduje ten istý viacstranový sken štyrmi rôznymi spôsobmi a vypíše výslednú veľkosť súboru pre každú konfiguráciu, takže porovnanie beží voči vašej vlastnej zmesi skenov, nie syntetickej

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.

Kde akumulácia naprieč stránkami naráža na svoje limity

Akumulovaný slovník je obmedzený na 4096 symbolov, rovnaký strop, aký na jednej stránke už presadzuje natívny enkodér po jednotlivých obrázkoch. Prekročte tento limit uprostred dokumentu a HotPDF nevyvolá výnimku ani nepreruší beh: akumulátor nový glyf odmietne a stránka, ktorá ho zaviedla, automaticky spadne späť na nezávislé kódovanie po jednotlivých obrázkoch, takže dokument stále vyjde správne — iba prestanete dostávať úsporu naprieč stránkami pre tie stránky, ktoré prekročili strop. Druhá poistka sleduje skôr celkovú veľkosť než počet symbolov: keď kombinovaná šírka symbolov akumulovaného slovníka prekročí 131071 pixelov, HotPDF automaticky preleje aktuálnu dávku na disk a začne novú skupinu globals, namiesto toho, aby nechal jednu štruktúru v pamäti rásť bez obmedzenia. Ani jeden z týchto limitov nevyžaduje na vašej strane žiadny kód, keďže oba sú automatické záložné mechanizmy, nie výnimky, ktoré by ste museli odchytávať

Zhoda s PDF/A je jediné nastavenie, ktoré celý mechanizmus vypne namiesto toho, aby ho iba obmedzilo. HotPDF ticho nahradí JBIG2 za CCITT Group 4 vo chvíli, keď je PDFACompliance neprázdne, na každej stránke, nezávisle od AccumulateGlobalsAcrossPages alebo čohokoľvek iného na JBIG2Options — zámerná voľba kvôli zhode s normou, nie chyba, no znamená to, že archívny profil a zdieľanie symbolov naprieč stránkami sa dnes navzájom vylučujú. Nech skončíte pri akejkoľvek konfigurácii, dekódujte to, čo ste zapísali, ešte predtým, než tomu začnete dôverovať: načítajte súbor späť pomocou LoadFromFile a pretiahnite každú stránku cez ExtractLoadedImage, ktorá za vás vyrieši zdieľané globals rovnako, ako by to urobila ktorákoľvek čítačka rešpektujúca normu, a porovnajte výsledok s vašimi zdrojovými bitmapami

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;

Zdieľanie slovníka naprieč stránkami sa dotýka iba dvojúrovňovej obrazovej strany dokumentu. Ak rovnaká pipeline popri skenoch vytvára aj generované textové stránky — titulné listy, indexové stránky, textovú vrstvu OCR — prúdy objektov a xref prúdy útočia na druhú polovicu rozpočtu veľkosti súboru tým, že komprimujú štruktúru dokumentu, ktorú tieto stránky pridávajú. Zdieľanie globals JBIG2 naprieč stránkami sa dodáva ako súčasť komponentu HotPDF pre Delphi a C++Builder, spolu s voľbami JBIG2 po jednotlivých obrázkoch a zvyškom kompresnej pipeline