Teknisk artikel

Att dela JBIG2-symbolordböcker mellan sidor i Delphi

Ett femtiosidigt inskannat kontrakt upprepar samma alfabet på varje sida, men en JBIG2-kodare som bygger en symbolordbok per bild tränar om det alfabetet femtio separata gånger. HotPDF, den native PDF-komponenten för Delphi och C++Builder, kan i stället ackumulera en delad symbolordbok över hela dokumentet och befordra den till en enda dokumentnivå-/JBIG2Globals-ström, så att varje sidas egen JBIG2-ström bara refererar till symbol-ID:n i stället för att lagra en egen kopia av alfabetet

Den här artikeln håller sig medvetet smal och täcker bara hur HotPDF bygger den sidöverskridande delningen internt — JBIG2-grunderna, CCITT-jämförelsen, och avvägningarna mellan Lossless och LossyLevel finns redan i följdartikeln om native JBIG2-tvånivåkomprimering i Delphi, som den här artikeln förutsätter att du har läst

Varför upprepar per-sida-JBIG2-komprimering fortfarande samma kostnad?

Svaret är att inget bär tillstånd mellan anrop. Varje gång HotPDF:s kodare bygger en symbolordbok för en bild är den ordboken avgränsad till just det AddImage-anropet: formmatchningspasset börjar från noll, varje glyf på sidan klassificeras som ny, och de resulterande bitmapparna aritmetikkodas och lagras på nytt. Mata samma kodare med femtio sidor satta i samma typsnitt och den upprepar glatt hela träningspasset femtio gånger, eftersom varje sida ur dess synvinkel är en orelaterad bild som råkar se likadan ut. Per-sida-UseSymbolDictionary slår redan en platt generisk-region-kodning med god marginal på en enda sida, men den toppar långt före taket en riktig flersidig skanning lämnar på bordet

Hur delar HotPDF en enda symbolordbok mellan sidor?

Aktivera AccumulateGlobalsAcrossPagesTHPDFJBIG2Options och HotPDF håller en symbolordbok vid liv i minnet under hela dokumentets livstid i stället för att kasta den efter varje bild. Varje efterföljande sidas glyfer kontrolleras mot den löpande ordboken innan något kodas om: en form som redan finns återanvänds via sitt symbol-ID, och bara en form ingen sett tidigare läggs till och kodas in i ordboken. Jämförelsen återanvänder samma toleranslogik som LossyLevel tillämpar på en enda sida — en lite brusig skanning av samma bokstav räknas fortfarande som en matchning — så ackumulatorn sväller inte tyst till en ordbokspost per pixelnivå-variation av samma glyf. Extraktion sker först och matar den jämförelsen: HotPDF går igenom varje sidas bitmapp och plockar ut sammanhängande former genom flödesfyllning mot de svarta pixlarna, samma idé som att spåra bläckfläckar för hand, och det är dessa extraherade former, inte råa pixelblock, som jämförs mot den löpande ordboken

Hur den delade ordboken sitter inuti en /JBIG2Globals-ström

Den ackumulerade ordboken skrivs som ett symbolordbokssegment inuti /JBIG2Globals-strömmen, hållet på ett fast segmentnummer så att varje sida kan peka mot samma mål. Inuti den inbäddade JBIG2-organisationen ISO 32000-1 §7.4.7 definierar kan ett textregionssegment namnge ett annat segment som sin symbolkälla via referens-till-segment-fältet i segmenthuvudet, och det är exakt den mekanismen HotPDF lutar sig mot: globalsströmmen bär den enda stora symbolordboken, och varje sidas egen JBIG2-ström krymper ner till ett sidinfo-segment plus ett textregionssegment vars referens-till-lista pekar tillbaka till globals-segmentet. Vad som brukade vara en självständig bitström per sida blir en kort lista med positioner och symbol-ID:n, och varje sida byggd på det här sättet refererar det identiska indirekta /JBIG2Globals-objektet i stället för en kopia av det. HotPDF:s egen regressionstäckning kontrollerar precis det: koda ett kort dokument där varje sida har en annan glyflayout, ladda om det, och räkna hur många distinkta /JBIG2Globals-objektreferenser som visar sig i filen — ett dokument, en objektreferens, oavsett hur många sidor som bidragit symboler till den

Att slå på ackumulering av sidöverskridande symbolordbok

Omkopplaren sitter på samma alternativpost som täcks i följdartikeln, och den kräver fyra inställningar överens med varandra innan ackumulering faktiskt kopplas in

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 ihopparningen är ingen valfri detalj. Den externa kodarsömmen som beskrivs i tvånivåkomprimeringsartikeln — den du registrerar via RegisterJBIG2EncoderBackend för produktionsklassade kompressionsförhållanden — är byggd kring per-bild-kodning, och HotPDF:s egna ackumuleringsdemos och regressionstester parar alltid AccumulateGlobalsAcrossPages med UseExternalEncoder := False. Behandla det som ett hårt krav snarare än ett förslag: sidöverskridande delning är en native-kodarfunktion, och en registrerad extern backend är helt enkelt inte en del av vägen som bygger den delade ordboken

Hur mycket mindre blir en flersidig skanning faktiskt?

Det ärliga svaret börjar med vad som inte flyttade nålen först. En tidigare version lade till en innehållsadresserad cache för /JBIG2Globals-strömmar — en uppslagning nyckad på en 64-bitars FNV-1a-hash av strömbytesen, så att två bilder som råkade producera byte-identisk globals-data kunde dela ett PDF-objekt. Mätt mot verklig utdata hjälpte den cachen knappt, eftersom HotPDF:s befintliga upptäckt av heldubbletter av bilder redan slog samman byte-identiska bilder innan cachen någonsin fick chansen att köras. Lärdomen var att strömnivå-avduplicering bara betalar sig när två genuint olika sidbilder fortfarande kan dela en växande ordbok, vilket är precis vad äkta sidöverskridande ackumulering ger

För det svårare fallet sätter HotPDF:s egen ingenjörsuppskattning den ytterligare besparingen till ungefär 30 till 60 procent mindre än vad strömnivå-avduplicering ensam åstadkommer, för en typisk flersidig skanning byggd från ett återkommande typsnitt — intervallet rör sig med hur mycket av dokumentets visuella vokabulär som faktiskt upprepas, eftersom en sida full av unika diagram inte ger ordboken något att återanvända. Behandla det som ett designmål snarare än en garanti för någon specifik indata, och mät dina egna dokument i stället för att lita på ett enda tal. Demot JBIG2Benchmark som levereras med HotPDF finns för exakt det syftet: den kodar samma flersidiga skanning på fyra olika sätt och skriver ut den resulterande filstorleken för varje konfiguration, så jämförelsen körs mot din egen skanningsmix i stället för 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.

Var sidöverskridande ackumulering når sina gränser

Den ackumulerade ordboken är begränsad till 4096 symboler, samma tak den native per-bild-kodaren redan upprätthåller på en enda sida. Passera den gränsen mitt i ett dokument och HotPDF kastar inget undantag eller avbryter körningen: ackumulatorn avvisar den nya glyfen, och sidan som introducerade den faller automatiskt tillbaka till oberoende per-bild-kodning, så dokumentet kommer fortfarande ut korrekt — du slutar bara få den sidöverskridande besparingen för de sidor som körde förbi taket. Ett andra skyddsvärn bevakar total storlek snarare än symbolantal: så fort den ackumulerade ordbokens kombinerade symbolbredd passerar 131071 pixlar spiller HotPDF ut den aktuella batchen till disk och startar en ny globals-grupp automatiskt, i stället för att låta en struktur i minnet växa utan gräns. Ingen av gränserna kräver någon kod på din sida, eftersom båda är automatiska reservlösningar snarare än undantag du behöver fånga

PDF/A-konformans är den enda inställningen som stänger av hela mekanismen i stället för att bara begränsa den. HotPDF ersätter tyst JBIG2 med CCITT Group 4 i samma stund PDFACompliance är icke-tom, på varje sida, oberoende av AccumulateGlobalsAcrossPages eller något annat på JBIG2Options — ett medvetet konformansval, inte en bugg, men det innebär att en arkivprofil och sidöverskridande symboldelning är ömsesidigt uteslutande idag. Vilken konfiguration du än landar på, avkoda vad du skrivit innan du litar på det: ladda tillbaka filen med LoadFromFile och dra varje sida genom ExtractLoadedImage, som löser den delade globals-datan åt dig på samma sätt som varje regelrätt läsare skulle göra, och jämför resultatet mot dina källbitmappar

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;

Sidöverskridande ordboksdelning rör bara den tvånivå-bilddelen av ett dokument. Om samma pipeline också genererar textsidor bredvid skanningarna — omslag, indexsidor, ett OCR-textlager — angriper objektströmmar och xref-strömmar den andra halvan av filstorleksbudgeten genom att komprimera dokumentstrukturen dessa sidor lägger till. Sidöverskridande JBIG2-globals levereras som en del av HotPDF-komponenten för Delphi och C++Builder, tillsammans med per-bild-JBIG2-alternativen och resten av komprimeringspipelinen