Teknisk artikel

Deling af JBIG2-symbolordbøger på tværs af sider i Delphi

En halvtreds-siders scannet kontrakt gentager det samme alfabet på hver side, men en JBIG2-encoder, der bygger én symbolordbog pr. billede, gentræner det alfabet halvtreds separate gange. HotPDF, den native Delphi- og C++Builder-PDF-komponent, kan i stedet akkumulere én delt symbolordbog på tværs af hele dokumentet og forfremme den til én dokument-niveau /JBIG2Globals-stream, så hver sides egen JBIG2-stream blot refererer symbol-ID'er i stedet for at gemme sin egen kopi af alfabetet

Dette stykke holder sig bevidst snævert og dækker kun, hvordan HotPDF bygger den side-på-tværs-deling internt — JBIG2-fundamentals, CCITT-sammenligningen og Lossless-versus-LossyLevel-afvejningerne findes allerede i følgeartiklen om native JBIG2-bilevel-komprimering i Delphi, som dette stykke antager, du har læst

Hvorfor gentager per-side-JBIG2-komprimering stadig den samme omkostning?

Svaret er, at intet bærer tilstand mellem kald. Hver gang HotPDFs encoder bygger en symbolordbog til ét billede, er den ordbog afgrænset til det ene AddImage-kald: form-matching-passet starter fra nul, hver glyf på siden klassificeres som ny, og de resulterende bitmaps arithmetic-kodes og gemmes friskt. Fodrer man den samme encoder med halvtreds sider sat i den samme skrifttype, gentager den gladeligt hele det trænings-pas halvtreds gange, fordi fra dens synspunkt er hver side et urelateret billede, der tilfældigvis ligner. Per-side UseSymbolDictionary slår allerede en flad generisk-region-kodning med en bred margin på én enkelt side, men den topper langt før det loft, en rigtig flersidet scanning efterlader på bordet

Hvordan deler HotPDF én symbolordbog på tværs af sider?

Aktivér AccumulateGlobalsAcrossPagesTHPDFJBIG2Options, og HotPDF holder én symbolordbog i live i hukommelsen for hele dokumentets levetid i stedet for at kassere den efter hvert billede. Hver efterfølgende sides glyffer tjekkes mod den kørende ordbog, før noget genkodes: en form, der allerede findes, genbruges via sit symbol-ID, og kun en form ingen har set før, tilføjes og kodes ind i ordbogen. Sammenligningen genbruger den samme tolerance-logik, LossyLevel anvender på en enkelt side — en lidt støjfyldt scanning af det samme bogstav tæller stadig som et match — så akkumulatoren balloneres ikke i stilhed til én ordbogspost pr. pixel-niveau-variation af den samme glyf. Udtrækning sker først og fodrer den sammenligning: HotPDF gennemgår hver sides bitmap og trækker forbundne former ud gennem flood fill mod de sorte pixels, den samme idé som at spore blækklatter i hånden, og det er de udtrukne former, ikke rå pixelblokke, der sammenlignes med den kørende ordbog

Hvordan den delte ordbog sidder inden i en /JBIG2Globals-stream

Den akkumulerede ordbog skrives som ét symbolordbog-segment inde i /JBIG2Globals-strømmen, holdt ved et fast segmentnummer, så hver side kan pege på det samme mål. Inden i den indlejrede JBIG2-organisation, ISO 32000-1 §7.4.7 definerer, kan et tekst-region-segment navngive et andet segment som sin symbolkilde gennem referred-to-segment-feltet i segment-headeren, og det er den præcise mekanisme, HotPDF læner sig på: globals-strømmen bærer den ene store symbolordbog, og hver sides egen JBIG2-stream krymper ned til et side-info-segment plus et tekst-region-segment, hvis referred-to-liste peger tilbage på globals-segmentet. Hvad der plejede at være en selvstændig bitstream pr. side, bliver en kort liste af positioner og symbol-ID'er, og hver side bygget på denne måde refererer det identiske indirekte /JBIG2Globals-objekt frem for en kopi af det. HotPDFs egen regressionsdækning tjekker præcis det: kod et kort dokument, hvor hver side har et forskelligt glyf-layout, genindlæs det, og tæl hvor mange distinkte /JBIG2Globals-objektreferencer der dukker op i filen — ét dokument, én objektreference, uanset hvor mange sider der bidrog symboler til den

At slå akkumulering af symbolordbog på tværs af sider til

Kontakten sidder på den samme options-record dækket i følgeartiklen, og den kræver fire indstillinger, der er enige med hinanden, før akkumulering rent faktisk kobler ind

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;

Den kombination er ikke valgfri pynt. Den eksterne encoder-grænseflade beskrevet i bilevel-komprimeringsartiklen — den man registrerer gennem RegisterJBIG2EncoderBackend til produktionsklasse-forhold — er bygget omkring per-billede-kodning, og HotPDFs egne akkumuleringsdemoer og regressionstests parrer altid AccumulateGlobalsAcrossPages med UseExternalEncoder := False. Behandl det som et hårdt krav frem for et forslag: side-på-tværs-deling er en native-encoder-funktion, og en registreret ekstern backend er simpelthen ikke en del af den vej, der bygger den delte ordbog

Hvor meget mindre bliver en flersidet scanning rent faktisk?

Det ærlige svar begynder med, hvad der ikke flyttede nålen først. En tidligere udgivelse tilføjede en indholds-adresseret cache til /JBIG2Globals-streams — et opslag nøglet af en 64-bit FNV-1a-hash af stream-bytene, så to billeder, der tilfældigvis producerede byte-identiske globals-data, kunne dele ét PDF-objekt. Målt mod rigtigt output hjalp den cache knap nok, fordi HotPDFs eksisterende hele-billede-dublet-detektion allerede kollapsede byte-identiske billeder, før cachen nogensinde fik chancen for at køre. Lektien var, at stream-niveau-deduplikering kun betaler sig, når to genuint forskellige side-billeder stadig kan dele én voksende ordbog, hvilket er, hvad ægte side-på-tværs-akkumulering leverer

For det sværere tilfælde sætter HotPDFs eget tekniske skøn den ekstra besparelse til omtrent 30 til 60 procent mindre, end stream-niveau-deduplikering alene opnår, for en typisk flersidet scanning bygget fra én gentagende skrifttype — intervallet bevæger sig med, hvor meget af dokumentets visuelle vokabular der rent faktisk gentager sig, da en side fuld af unikke diagrammer ikke giver ordbogen noget at genbruge. Behandl det som et designmål frem for en garanti for et specifikt input, og mål dine egne dokumenter frem for at stole på ét tal. JBIG2Benchmark-demoen, der følger med HotPDF, findes netop til det formål: den koder den samme flersidede scanning på fire forskellige måder og udskriver den resulterende filstørrelse for hver konfiguration, så sammenligningen kører mod din egen scan-mix 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;
    // ... 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.

Hvor side-på-tværs-akkumulering rammer sine grænser

Den akkumulerede ordbog er begrænset til 4096 symboler, det samme loft den per-billede native encoder allerede håndhæver på en enkelt side. Krydser man den grænse midt i dokumentet, kaster HotPDF ikke en undtagelse eller afbryder kørslen: akkumulatoren afviser den nye glyf, og den side, der introducerede den, falder automatisk tilbage til uafhængig per-billede-kodning, så dokumentet stadig kommer korrekt ud — man mister bare side-på-tværs-besparelsen for de sider, der skubbede forbi loftet. En anden sikring holder øje med den totale størrelse frem for symbolantal: så snart den akkumulerede ordbogs kombinerede symbolbredde krydser 131071 pixels, spilder HotPDF den nuværende batch til disk og starter en frisk globals-gruppe automatisk, i stedet for at lade én struktur i hukommelsen vokse uden grænse. Ingen af grænserne kræver nogen kode på din side, da begge er automatiske fallbacks frem for undtagelser, man skal fange

PDF/A-konformitet er den ene indstilling, der slår hele mekanismen fra frem for blot at begrænse den. HotPDF erstatter i stilhed CCITT Group 4 med JBIG2, i det øjeblik PDFACompliance er ikke-tom, på hver side, uafhængigt af AccumulateGlobalsAcrossPages eller noget andet på JBIG2Options — et bevidst konformitetsvalg, ikke en fejl, men det betyder, at en arkiv-profil og side-på-tværs-symbol-deling gensidigt udelukker hinanden i dag. Uanset hvilken konfiguration man lander på, dekod hvad man skrev, før man stoler på det: indlæs filen tilbage med LoadFromFile og træk hver side gennem ExtractLoadedImage, som løser de delte globals for dig på samme måde som enhver konform læser ville, og sammenlign resultatet med dine kilde-bitmaps

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;

Side-på-tværs-ordbogsdeling rører kun bilevel-billedsiden af et dokument. Hvis den samme pipeline også udsender genererede tekstsider ved siden af scanningerne — forsider, indeks-sider, et OCR-tekstlag — angriber objektstrømme og xref-strømme den anden halvdel af filstørrelsesbudgettet ved at komprimere den dokumentstruktur, de sider tilføjer. Side-på-tværs JBIG2-globals leveres som en del af HotPDF-komponenten til Delphi og C++Builder, sammen med per-billede JBIG2-indstillingerne og resten af komprimeringspipelinen