Műszaki cikk

JBIG2 szimbólumszótárak megosztása oldalak között Delphiben

Egy ötvenoldalas beszkennelt szerződés minden oldalon ugyanazt az ábécét ismétli, de egy JBIG2-kódoló, amely képenként egy szimbólumszótárat épít, ötvenszer külön betanítja azt az ábécét. A HotPDF, a natív Delphi és C++Builder PDF-komponens, ehelyett egyetlen megosztott szimbólumszótárat halmozhat fel a teljes dokumentumon át, és egyetlen dokumentum-szintű /JBIG2Globals streambe léptetheti elő, így minden oldal saját JBIG2-streamje csupán szimbólum-azonosítókra hivatkozik ahelyett, hogy tárolná az ábécé saját másolatát

Ez az írás szándékosan szűk, és csak azt tárgyalja, hogyan építi fel a HotPDF ezt az oldalok közötti megosztást belsőleg — a JBIG2 alapjai, a CCITT-összehasonlítás, és a Lossless kontra LossyLevel kompromisszumok már a Delphi natív JBIG2 kétszintű tömörítéséről szóló kísérőcikkben élnek, amelyet ez az írás elolvasottnak feltételez

Miért ismétli meg az oldalankénti JBIG2-tömörítés ugyanazt a költséget?

A válasz az, hogy semmi nem hordoz állapotot a hívások között. Valahányszor a HotPDF kódolója felépít egy szimbólumszótárat egyetlen képhez, az a szótár egyetlen AddImage hívásra korlátozódik: az alakillesztő átfutás nulláról indul, minden glyph az oldalon újnak minősül, és a keletkező bitképek frissen aritmetikai-kódolásra és tárolásra kerülnek. Etesd ugyanazt a kódolót ötven, ugyanazon betűtípussal szedett oldallal, és boldogan megismétli a teljes betanítási átfutást ötvenszer, mert a saját szemszögéből minden oldal egy különálló kép, amely véletlenül hasonlóan néz ki. Az oldalankénti UseSymbolDictionary már egyetlen oldalon is nagy különbséggel legyőz egy lapos, generikus régió-kódolást, de messze a plafon alatt tetőzik, amit egy valódi többoldalas szkennelés hagy az asztalon

Hogyan osztja meg a HotPDF egyetlen szimbólumszótárat oldalak között?

Kapcsold be az AccumulateGlobalsAcrossPages-t a THPDFJBIG2Options-on, és a HotPDF egyetlen szimbólumszótárat tart életben memóriában a dokumentum teljes élettartama alatt, ahelyett hogy minden kép után eldobná. Minden azt követő oldal glyph-jei ez ellen a futó szótár ellen kerülnek ellenőrzésre, mielőtt bármi is újrakódolásra kerülne: egy alakzat, amely már létezik, a szimbólum-azonosítója révén kerül újrahasznosításra, és csak egy olyan alakzat kerül hozzáfűzésre és kódolásra a szótárba, amit még senki nem látott. Az összehasonlítás ugyanazt a tolerancia-logikát hasznosítja újra, amelyet a LossyLevel alkalmaz egyetlen oldalon — ugyanannak a betűnek egy kissé zajos szkennelése még mindig egyezésnek számít —, így az akkumulátor nem duzzad csendben ugyanannak a glyphnek minden egyes pixel-szintű variációjára egy szótárbejegyzésre. A kinyerés történik először, és ez táplálja azt az összehasonlítást: a HotPDF bejárja minden oldal bitképét, és kinyeri az összefüggő alakzatokat a fekete pixelek elleni flood fill-lel, ugyanaz az ötlet, mint a tintafoltok kézzel történő nyomon követése, és ezek a kinyert alakzatok, nem a nyers pixeltömbök, kerülnek összehasonlításra a futó szótárral

Hogyan ül a megosztott szótár egy /JBIG2Globals streamen belül

A felhalmozott szótár egyetlen szimbólumszótár-szegmensként kerül megírásra a /JBIG2Globals streamen belül, egy rögzített szegmensszámon tartva, hogy minden oldal ugyanarra a célra mutathasson. Az ISO 32000-1 §7.4.7 által definiált beágyazott JBIG2-szervezésen belül egy szövegrégió-szegmens megnevezhet egy másik szegmenst szimbólumforrásaként a szegmensfejlécben lévő hivatkozott-szegmens mezőn keresztül, és pontosan erre a mechanizmusra támaszkodik a HotPDF: a globals stream hordozza az egy nagy szimbólumszótárat, és minden oldal saját JBIG2-streamje egy oldal-információs szegmensre plusz egy szövegrégió-szegmensre zsugorodik, amelynek hivatkozott-lista mezője visszamutat a globals szegmensre. Ami korábban egy önálló bitfolyam volt oldalanként, most pozíciók és szimbólum-azonosítók rövid listájává válik, és minden így épített oldal ugyanarra az azonos, közvetett /JBIG2Globals objektumra hivatkozik, nem annak egy másolatára. A HotPDF saját regressziós lefedettsége pontosan ezt ellenőrzi: kódolj egy rövid dokumentumot, ahol minden oldalnak más glyph-elrendezése van, töltsd be újra, és számold meg, hány különálló /JBIG2Globals objektumhivatkozás jelenik meg a fájlban — egy dokumentum, egy objektumhivatkozás, függetlenül attól, hány oldal járult hozzá szimbólumokkal

Az oldalak közötti szimbólumszótár-felhalmozás bekapcsolása

A kapcsoló ugyanazon az opcióbejegyzésen ül, amelyet a kísérőcikk tárgyal, és négy beállítás egyetértését igényli, mielőtt a felhalmozás ténylegesen bekapcsolna

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;

Ez a párosítás nem opcionális díszítés. A bilevel-tömörítésről szóló cikkben leírt külső-kódolós illesztés — az, amit a RegisterJBIG2EncoderBackend-en keresztül regisztrálsz termelési szintű arányokért — a képenkénti kódolás köré épül, és a HotPDF saját felhalmozási demói és regressziós tesztjei mindig párosítják az AccumulateGlobalsAcrossPages-t a UseExternalEncoder := False-szal. Kezeld ezt kemény követelményként, nem javaslatként: az oldalak közötti megosztás natív kódoló-funkció, és egy regisztrált külső háttérrendszer egyszerűen nem része annak az útvonalnak, amely a megosztott szótárat építi

Mennyivel lesz ténylegesen kisebb egy többoldalas szkennelés?

Az őszinte válasz azzal kezdődik, ami elsőre nem mozdította a mutatót. Egy korábbi kiadás egy tartalom-címzett gyorsítótárat adott hozzá a /JBIG2Globals streamekhez — egy keresést, amelyet a stream bájtjainak 64 bites FNV-1a hash-e kulcsol, így két kép, amely véletlenül bájtazonos globals-adatot állított elő, megoszthatott egy PDF-objektumot. A valódi kimenettel szemben mérve ez a gyorsítótár alig segített, mert a HotPDF meglévő, teljes-kép duplikátumfelismerése már összeomlasztotta a bájtazonos képeket, mielőtt a gyorsítótárnak esélye lett volna futni. A tanulság az volt, hogy a stream-szintű deduplikáció csak akkor térül meg, ha két valóban különböző oldalkép még mindig meg tud osztani egy növekvő szótárat, és pontosan ezt szállítja a valódi oldalak közötti felhalmozás

Erre a nehezebb esetre a HotPDF saját mérnöki becslése az additív megtakarítást nagyjából 30-60 százalékkal kisebbre teszi, mint amit a stream-szintű deduplikáció önmagában elér, egy tipikus, egy visszatérő betűtípusból épült többoldalas szkenneléshez — a tartomány azzal mozog, mennyire ismétlődik ténylegesen a dokumentum vizuális szókészlete, mivel egy egyedi diagramokkal teli oldal semmit nem ad a szótárnak, amit újra tudna hasznosítani. Kezeld ezt tervezési célként, nem garanciaként bármely konkrét bemenetre, és mérd meg a saját dokumentumaidat ahelyett, hogy egyetlen számban bíznál. A HotPDF-fel szállított JBIG2Benchmark demó pontosan erre a célra létezik: ugyanazt a többoldalas szkennelést négy különböző módon kódolja, és kinyomtatja a keletkező fájlméretet minden konfigurációhoz, így az összehasonlítás a saját szkennelési keveréked ellen fut, nem egy szintetikus ellen

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.

Hol éri el az oldalak közötti felhalmozás a korlátait

A felhalmozott szótár 4096 szimbólumra van korlátozva, ugyanaz a plafon, amit a képenkénti natív kódoló már kikényszerít egyetlen oldalon. Lépd át ezt a korlátot dokumentum közben, és a HotPDF nem dob kivételt, és nem szakítja meg a futást: az akkumulátor elutasítja az új glyph-et, és az az oldal, amely azt bevezette, automatikusan visszaesik a független, képenkénti kódolásra, így a dokumentum még mindig helyesen jön ki — csak leállsz az oldalak közötti megtakarítás kapásáról azokra az oldalakra, amelyek átlépték a plafont. Egy második biztonsági intézkedés a teljes méretet figyeli, nem a szimbólumszámot: amint a felhalmozott szótár együttes szimbólumszélessége átlépi a 131071 pixelt, a HotPDF kiönti az aktuális köteget lemezre, és automatikusan új globals-csoportot indít, ahelyett hogy hagyná egy memóriabeli struktúrát korlátlanul növekedni. Egyik korlát sem igényel semmilyen kódot a te oldaladon, mivel mindkettő automatikus visszaesés, nem olyan kivétel, amit el kellene kapnod

A PDF/A-megfelelőség az egyetlen beállítás, amely kikapcsolja az egész mechanizmust, ahelyett hogy csak korlátozná azt. A HotPDF csendben helyettesíti a JBIG2-t CCITT Group 4-gyel abban a pillanatban, amint a PDFACompliance nem üres, minden oldalon, függetlenül az AccumulateGlobalsAcrossPages-től vagy bármi mástól a JBIG2Options-on — ez egy szándékos megfelelőségi döntés, nem hiba, de azt jelenti, hogy egy archiválási profil és az oldalak közötti szimbólummegosztás ma kölcsönösen kizárják egymást. Bármelyik konfigurációnál is landolsz, dekódold, amit írtál, mielőtt megbíznál benne: töltsd be újra a fájlt a LoadFromFile-lal, és húzd át minden oldalt az ExtractLoadedImage-en, amely feloldja neked a megosztott globals-t ugyanúgy, ahogyan bármely szabványkövető olvasó tenné, és hasonlítsd össze az eredményt a forrás bitképeiddel

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;

Az oldalak közötti szótármegosztás csak a dokumentum kétszintű kép-oldalát érinti. Ha ugyanaz a csővezeték generált szövegoldalakat is kibocsát a szkennelések mellett — borítólapokat, indexoldalakat, egy OCR szövegréteget —, az objektumfolyamok és az xref-folyamok a fájlméret-költségvetés másik felét támadják meg azáltal, hogy tömörítik azt a dokumentumszerkezetet, amelyet ezek az oldalak hozzáadnak. Az oldalak közötti JBIG2-globals a Delphihez és C++Builderhez készült HotPDF komponens részeként érkezik, a képenkénti JBIG2-opciók és a tömörítési csővezeték többi része mellett