Teknisk artikkel

Å dele JBIG2-symbolordbøker på tvers av sider i Delphi

En femti-siders skannet kontrakt gjentar det samme alfabetet på hver side, men en JBIG2-koder som bygger én symbolordbok per bilde, trener opp det alfabetet på nytt femti separate ganger. HotPDF, den native Delphi- og C++Builder-PDF-komponenten, kan i stedet akkumulere én delt symbolordbok gjennom hele dokumentet og forfremme den til én enkelt dokumentnivå-/JBIG2Globals-strøm, slik at hver sides egen JBIG2-strøm bare refererer symbol-ID-er i stedet for å lagre sin egen kopi av alfabetet

Denne artikkelen holder seg bevisst smal og dekker bare hvordan HotPDF bygger den delingen på tvers av sider internt — JBIG2-grunnlaget, CCITT-sammenligningen, og Lossless-versus-LossyLevel-avveiningene hører allerede hjemme i følgeartikkelen om nativ JBIG2-binærnivå-komprimering i Delphi, som denne artikkelen forutsetter at du har lest

Hvorfor gjentar per-side JBIG2-komprimering fortsatt den samme kostnaden?

Svaret er at ingenting bærer tilstand mellom kall. Hver gang HotPDFs koder bygger en symbolordbok for ett bilde, er den ordboken avgrenset til det ene AddImage-kallet: form-matchings-passeringen starter fra null, hver glyf på siden klassifiseres som ny, og de resulterende bitkartene aritmetisk-kodes og lagres på nytt. Mat den samme koderen med femti sider satt i samme skrifttype, og den gjentar villig hele den treningspasseringen femti ganger, fordi fra dens synspunkt er hver side et urelatert bilde som tilfeldigvis ser likt ut. Per-side UseSymbolDictionary slår allerede en flat generisk-region-koding med god margin på én enkelt side, men den flater ut godt før taket et ekte flersides-skann etterlater på bordet

Hvordan deler HotPDF én enkelt symbolordbok på tvers av sider?

Aktiver AccumulateGlobalsAcrossPagesTHPDFJBIG2Options, og HotPDF holder én symbolordbok i live i minnet gjennom hele dokumentets levetid i stedet for å forkaste den etter hvert bilde. Hver etterfølgende sides glyfer sjekkes mot den løpende ordboken før noe blir re-kodet: en form som allerede finnes, gjenbrukes ved sin symbol-ID, og bare en form ingen har sett før, blir lagt til og kodet inn i ordboken. Sammenligningen gjenbruker den samme toleranselogikken LossyLevel anvender på én enkelt side — en litt støyete skanning av den samme bokstaven teller fortsatt som en match — slik at akkumulatoren ikke stille sveller til én ordbokoppføring per piksel-nivå-variasjon av den samme glyfen. Uttrekking skjer først og mater den sammenligningen: HotPDF går gjennom hver sides bitkart og trekker ut sammenhengende former gjennom flood fill mot de svarte pikslene, den samme idéen som å spore blekkflekker for hånd, og det er de uttrukkede formene, ikke rå pikselblokker, som sammenlignes mot den løpende ordboken

HotPDF-diagram som setter per-side JBIG2-koding, der hver skannet side trener det samme glyfalfabetet på nytt inn i sin egen symboldictionary, opp mot kryssside-akkumulering som legger hvert nytt glyph til én gang i én delt løpende dictionary gjenbruket per symbol-ID
Uten akkumulering trener hver side sin egen kopi av alfabetet, mens AccumulateGlobalsAcrossPages lar hver side gjenbruke symbol-ID-er fra én voksende ordbok

Hvordan den delte ordboken sitter inne i en /JBIG2Globals-strøm

Den akkumulerte ordboken skrives som ett symbolordbok-segment inne i /JBIG2Globals-strømmen, holdt ved et fast segmentnummer slik at hver side kan peke på det samme målet. Inne i den innebygde JBIG2-organiseringen ISO 32000-1 §7.4.7 definerer, kan et tekstregion-segment navngi et annet segment som sin symbolkilde gjennom referert-til-segment-feltet i segmentheaderen, og det er den nøyaktige mekanismen HotPDF lener seg på: globals-strømmen bærer den ene store symbolordboken, og hver sides egen JBIG2-strøm krymper ned til et side-info-segment pluss et tekstregion-segment hvis referert-til-liste peker tilbake på globals-segmentet. Det som pleide å være en selvstendig bitstrøm per side, blir en kort liste med posisjoner og symbol-ID-er, og hver side bygget på denne måten refererer til det identiske indirekte /JBIG2Globals-objektet i stedet for en kopi av det. HotPDFs egen regresjonsdekning sjekker nøyaktig det: kod et kort dokument der hver side har et forskjellig glyf-layout, last det inn på nytt, og tell hvor mange distinkte /JBIG2Globals-objektreferanser som dukker opp i filen — ett dokument, én objektreferanse, uansett hvor mange sider som bidro med symboler til den

Å skru på akkumulering av symbolordbok på tvers av sider

Bryteren sitter på den samme opsjons-recorden dekket i følgeartikkelen, og den krever at fire innstillinger er enige med hverandre før akkumulering faktisk trer i kraft

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;  // valgfritt på, standard False
    Pdf.JBIG2Options.UseExternalEncoder := False;            // akkumulering krever den native stien
    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-bits TBitmap for denne siden
      ImgIdx := Pdf.AddImage(Bmp, icJBIG2);
      Pdf.CurrentPage.ShowImage(ImgIdx, 0, 0, Bmp.Width, Bmp.Height, 0);
    end;
    Pdf.EndDoc;                                  // den delte /JBIG2Globals-strømmen ferdigstilles her
  finally
    Pdf.Free;
  end;
end;

Den koblingen er ikke valgfri pynt. Den eksterne koder-sømmen beskrevet i binærnivå-komprimeringsartikkelen — den man registrerer gjennom RegisterJBIG2EncoderBackend for produksjonsklasse-komprimeringsforhold — er bygget rundt per-bilde-koding, og HotPDFs egne akkumulerings-demoer og regresjonstester parer alltid AccumulateGlobalsAcrossPages med UseExternalEncoder := False. Behandle det som et hardt krav snarere enn et forslag: deling på tvers av sider er en nativ-koder-funksjon, og en registrert ekstern backend er rett og slett ikke en del av veien som bygger den delte ordboken

Hvor mye mindre blir et flersides-skann faktisk?

Det ærlige svaret starter med hva som ikke flyttet nålen først. En tidligere utgivelse la til en innholdsadressert buffer for /JBIG2Globals-strømmer — et oppslag nøkkelsatt på en 64-bit FNV-1a-hash av strømbytene, slik at to bilder som tilfeldigvis produserte byte-identiske globals-data, kunne dele ett PDF-objekt. Målt mot ekte utdata hjalp den bufferen knapt, fordi HotPDFs eksisterende hele-bilde-duplikatdeteksjon allerede kollapset byte-identiske bilder før bufferen noensinne fikk sjansen til å kjøre. Lærdommen var at strøm-nivå-deduplisering bare lønner seg når to genuint forskjellige sidebilder likevel kan dele én voksende ordbok, noe ekte akkumulering på tvers av sider leverer

For det vanskeligere tilfellet setter HotPDFs egen ingeniør-anslag den ekstra besparelsen til omtrent 30 til 60 prosent mindre enn strøm-nivå-deduplisering alene oppnår, for et typisk flersides-skann bygget fra én gjentakende skrifttype — området beveger seg med hvor mye av dokumentets visuelle ordforråd som faktisk gjentar seg, ettersom en side full av unike diagrammer ikke gir ordboken noe å gjenbruke. Behandle det som et designmål snarere enn en garanti for noe spesifikt inndata, og mål dine egne dokumenter i stedet for å stole på ett enkelt tall. JBIG2Benchmark-demoen som følger med HotPDF, finnes nettopp for det formålet: den koder det samme flersides-skannet på fire forskjellige måter og skriver ut den resulterende filstørrelsen for hver konfigurasjon, slik at sammenligningen kjører mot din egen skann-miks i stedet for en syntetisk en

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;
    // ... kod den samme tresiders skanningen her, sammenlign deretter filstørrelser.
  finally
    Pdf.Free;
  end;
end;

begin
  RunScenario('Per-image lossless baseline', False);
  RunScenario('Cross-page accumulated globals', True);
end.

Hvor akkumulering på tvers av sider når sine grenser

Den akkumulerte ordboken er begrenset til 4096 symboler, det samme taket den per-bilde-native koderen allerede håndhever på én enkelt side. Krysser man den grensen midt i dokumentet, kaster ikke HotPDF et unntak eller avbryter kjøringen: akkumulatoren avviser den nye glyfen, og siden som introduserte den, faller automatisk tilbake til uavhengig per-bilde-koding, slik at dokumentet fortsatt kommer ut korrekt — man slutter bare å få besparelsen på tvers av sider for hvilke som helst sider som presset forbi taket. En annen sikringsmekanisme følger med på total størrelse i stedet for symbolantall: så snart den akkumulerte ordbokens kombinerte symbolbredde krysser 131071 piksler, søler HotPDF automatisk den gjeldende batchen til disk og starter en ny globals-gruppe, i stedet for å la én struktur i minnet vokse uten grense. Ingen av grensene trenger noen kode på din side, ettersom begge er automatiske reserveløsninger snarere enn unntak man må fange

Anatomidiagram over en Delphi-generert PDF der én dokumentnivå /JBIG2Globals-strøm holder ett enkelt fastnummer symboldictionary-segment, og hver sidestrøm krymper til et sideinfo-segment pluss et tekstregion-segment hvis referred-to-liste lagrer bare posisjoner og symbol-ID-er
Hver sidestrøm beholder bare et page-info-segment og et kort tekstregionsegment hvis referred-to-liste peker på den ene dokumentnivå-/JBIG2Globals-ordboken

PDF/A-konformitet er den ene innstillingen som skrur av hele mekanismen i stedet for bare å begrense den. HotPDF erstatter stille CCITT Group 4 for JBIG2 i det øyeblikket PDFACompliance er ikke-tom, på hver eneste side, uavhengig av AccumulateGlobalsAcrossPages eller noe annet på JBIG2Options — et bevisst konformitetsvalg, ikke en bug, men det betyr at en arkivprofil og deling av symboler på tvers av sider er gjensidig utelukkende i dag. Uansett hvilken konfigurasjon man lander på, avkod det man skrev før man stoler på det: last filen inn igjen med LoadFromFile og trekk hver side gjennom ExtractLoadedImage, som løser den delte globals-en for deg på samme måte som enhver konform leser ville gjort, og sammenlign resultatet mot kildebitkartene

HotPDF-flytskjema over de to automatiske JBIG2-akkumuleringsrekkverkene: 4096-symboltakket som faller tilbake til uavhengig per-bilde-koding, og 131071-pikslers totalbreddsbudsjett som renner over til disk og åpner en fersk globals-gruppe, pluss PDF/A-bryteren som erstatter JBIG2 med CCITT Group 4 på hver side
Å krysse en av de automatiske grensene degraderer delingen på en elegant måte i stedet for å få dokumentet til å feile, og en PDF/A-profil slår hele mekanismen av og erstatter med CCITT Group 4
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);   // løser de delte globalene for deg
      try
        // Sammenlign PageBmp mot kildebitmappet for denne siden.
      finally
        PageBmp.Free;
      end;
    end;
  finally
    Loaded.Free;
  end;
end;

Deling av ordbok på tvers av sider rører bare den binærnivå-bilde-siden av et dokument. Hvis den samme pipelinen også genererer tekstsider ved siden av skannene — omslagsark, indekssider, et OCR-tekstlag — angriper objektstrømmer og xref-strømmer den andre halvparten av filstørrelsesbudsjettet ved å komprimere dokumentstrukturen de sidene legger til. JBIG2-globals på tvers av sider følger med som en del av HotPDF-Delphi-komponenten for Delphi og C++Builder, sammen med per-bilde JBIG2-opsjonene og resten av komprimeringspipelinen