Technischer Artikel

JBIG2-Symbolwörterbücher seitenübergreifend teilen in Delphi

Ein fünfzigseitiger gescannter Vertrag wiederholt auf jeder Seite dasselbe Alphabet, aber ein JBIG2-Encoder, der pro Bild ein eigenes Symbolwörterbuch aufbaut, trainiert dieses Alphabet fünfzig separate Male neu. HotPDF, die native Delphi- und C++Builder-PDF-Komponente, kann stattdessen ein gemeinsames Symbolwörterbuch über das gesamte Dokument hinweg anhäufen und es zu einem einzigen dokumentweiten /JBIG2Globals-Stream befördern, sodass der eigene JBIG2-Stream jeder Seite nur noch Symbol-IDs referenziert, statt eine eigene Kopie des Alphabets zu speichern

Dieser Beitrag bleibt absichtlich eng gefasst und behandelt nur, wie HotPDF diese seitenübergreifende Teilung intern aufbaut — JBIG2-Grundlagen, der CCITT-Vergleich und die Abwägungen zwischen Lossless und LossyLevel finden sich bereits in dem begleitenden Artikel zur nativen JBIG2-Bilevel-Kompression in Delphi, den dieser Beitrag als bekannt voraussetzt

Warum wiederholt die JBIG2-Kompression pro Seite immer noch dieselben Kosten?

Die Antwort ist, dass nichts Zustand zwischen Aufrufen trägt. Jedes Mal, wenn HotPDFs Encoder ein Symbolwörterbuch für ein Bild aufbaut, ist dieses Wörterbuch auf genau diesen einen AddImage-Aufruf beschränkt: Der Formabgleich-Durchlauf beginnt bei null, jede Glyphe auf der Seite wird als neu eingestuft, und die entstehenden Bitmaps werden frisch arithmetisch codiert und gespeichert. Füttert man denselben Encoder mit fünfzig Seiten in derselben Schriftart, wiederholt er bereitwillig den gesamten Trainingsdurchlauf fünfzig Mal, weil aus seiner Sicht jede Seite ein unabhängiges Bild ist, das zufällig ähnlich aussieht. Das seitenweise UseSymbolDictionary schlägt eine flache generische Regionscodierung auf einer einzelnen Seite bereits deutlich, aber es endet weit unter der Obergrenze, die ein echter mehrseitiger Scan auf dem Tisch liegen lässt

Wie teilt HotPDF ein einziges Symbolwörterbuch über Seiten hinweg?

AccumulateGlobalsAcrossPages an THPDFJBIG2Options zu aktivieren, hält ein Symbolwörterbuch für die gesamte Lebensdauer des Dokuments im Speicher am Leben, statt es nach jedem Bild zu verwerfen. Die Glyphen jeder nachfolgenden Seite werden gegen dieses laufende Wörterbuch geprüft, bevor überhaupt etwas neu codiert wird: Eine bereits existierende Form wird über ihre Symbol-ID wiederverwendet, und nur eine noch nie gesehene Form wird angehängt und in das Wörterbuch codiert. Der Vergleich verwendet dieselbe Toleranzlogik wieder, die LossyLevel auf einer einzelnen Seite anwendet — ein leicht verrauschter Scan desselben Buchstabens zählt weiterhin als Treffer —, sodass der Akkumulator nicht still zu einem Wörterbucheintrag pro Pixel-Ebenen-Variation derselben Glyphe aufbläht. Die Extraktion findet zuerst statt und speist diesen Vergleich: HotPDF durchläuft die Bitmap jeder Seite und zieht zusammenhängende Formen per Flood Fill gegen die schwarzen Pixel heraus, dieselbe Idee wie das händische Nachzeichnen von Tintenflecken, und es sind diese extrahierten Formen, nicht rohe Pixelblöcke, die gegen das laufende Wörterbuch verglichen werden

Wie das gemeinsame Wörterbuch in einem /JBIG2Globals-Stream sitzt

Das akkumulierte Wörterbuch wird als ein einziges Symbolwörterbuch-Segment innerhalb des /JBIG2Globals-Streams geschrieben, gehalten an einer festen Segmentnummer, sodass jede Seite auf dasselbe Ziel zeigen kann. Innerhalb der eingebetteten JBIG2-Organisation, die ISO 32000-1 §7.4.7 definiert, kann ein Textregion-Segment ein anderes Segment als seine Symbolquelle über das Feld "referred-to segment" im Segment-Header benennen, und genau auf diesen Mechanismus stützt sich HotPDF: Der Globals-Stream trägt das eine große Symbolwörterbuch, und der eigene JBIG2-Stream jeder Seite schrumpft auf ein Seiteninfo-Segment plus ein Textregion-Segment, dessen "referred-to"-Liste zurück auf das Globals-Segment zeigt. Was früher ein in sich geschlossener Bitstream pro Seite war, wird zu einer kurzen Liste von Positionen und Symbol-IDs, und jede so aufgebaute Seite referenziert dasselbe indirekte /JBIG2Globals-Objekt, statt eine Kopie davon zu enthalten. HotPDFs eigene Regressionsabdeckung prüft genau das: ein kurzes Dokument codieren, bei dem jede Seite ein anderes Glyphen-Layout hat, es neu laden und zählen, wie viele unterschiedliche /JBIG2Globals-Objektreferenzen in der Datei auftauchen — ein Dokument, eine Objektreferenz, egal wie viele Seiten Symbole dazu beigetragen haben

Seitenübergreifende Symbolwörterbuch-Akkumulation einschalten

Der Schalter sitzt am selben Options-Record, der im begleitenden Artikel behandelt wird, und er benötigt vier miteinander übereinstimmende Einstellungen, bevor die Akkumulation tatsächlich greift

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;

Diese Paarung ist keine optionale Dekoration. Die im Bilevel-Kompressionsartikel beschriebene External-Encoder-Nahtstelle — die Sie über RegisterJBIG2EncoderBackend für produktionsreife Kompressionsraten registrieren — ist um Pro-Bild-Codierung herum gebaut, und HotPDFs eigene Akkumulations-Demos und Regressionstests paaren AccumulateGlobalsAcrossPages immer mit UseExternalEncoder := False. Behandeln Sie das als harte Anforderung, nicht als Vorschlag: Seitenübergreifende Teilung ist ein Feature des nativen Encoders, und ein registriertes externes Backend ist schlicht nicht Teil des Pfads, der das gemeinsame Wörterbuch aufbaut

Wie viel kleiner wird ein mehrseitiger Scan tatsächlich?

Die ehrliche Antwort beginnt damit, was zunächst nicht half. Eine frühere Version fügte einen inhaltsadressierten Cache für /JBIG2Globals-Streams hinzu — ein Lookup, indiziert über einen 64-Bit-FNV-1a-Hash der Stream-Bytes, sodass zwei Bilder, die zufällig byteidentische Globals-Daten erzeugten, sich ein PDF-Objekt teilen konnten. Gemessen an echter Ausgabe half dieser Cache kaum, weil HotPDFs bestehende Ganzbild-Duplikaterkennung byteidentische Bilder bereits zusammenfasste, bevor der Cache überhaupt zum Zug kam. Die Lehre daraus war, dass Stream-Ebenen-Deduplizierung sich erst auszahlt, sobald zwei tatsächlich unterschiedliche Seitenbilder sich noch ein wachsendes Wörterbuch teilen können, genau das, was echte seitenübergreifende Akkumulation liefert

Für diesen härteren Fall beziffert HotPDFs eigene technische Schätzung die zusätzliche Ersparnis auf etwa 30 bis 60 Prozent kleiner, als reine Stream-Ebenen-Deduplizierung allein erreicht, für einen typischen mehrseitigen Scan, der aus einer wiederkehrenden Schriftart aufgebaut ist — die Spanne verschiebt sich danach, wie viel vom visuellen Vokabular des Dokuments tatsächlich wiederkehrt, da eine Seite voller einzigartiger Diagramme dem Wörterbuch nichts zum Wiederverwenden gibt. Behandeln Sie das als Designziel, nicht als Garantie für eine bestimmte Eingabe, und messen Sie Ihre eigenen Dokumente, statt einer einzelnen Zahl zu vertrauen. Die mit HotPDF ausgelieferte Demo JBIG2Benchmark existiert genau zu diesem Zweck: Sie codiert denselben mehrseitigen Scan auf vier verschiedene Arten und gibt die resultierende Dateigröße für jede Konfiguration aus, sodass der Vergleich gegen Ihre eigene Scan-Mischung läuft statt gegen eine synthetische

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.

Wo seitenübergreifende Akkumulation an ihre Grenzen stößt

Das akkumulierte Wörterbuch ist auf 4096 Symbole begrenzt, dieselbe Obergrenze, die der Pro-Bild-native-Encoder bereits auf einer einzelnen Seite erzwingt. Wird diese Grenze mitten im Dokument überschritten, wirft HotPDF keine Exception und bricht den Lauf nicht ab: Der Akkumulator lehnt die neue Glyphe ab, und die Seite, die sie eingeführt hat, fällt automatisch auf unabhängige Pro-Bild-Codierung zurück, sodass das Dokument weiterhin korrekt herauskommt — Sie verlieren nur die seitenübergreifende Ersparnis für die Seiten, die über die Obergrenze hinausgestoßen sind. Eine zweite Absicherung überwacht die Gesamtgröße statt der Symbolanzahl: Sobald die kombinierte Symbolbreite des akkumulierten Wörterbuchs 131071 Pixel überschreitet, schreibt HotPDF die aktuelle Charge automatisch auf die Festplatte und beginnt eine neue Globals-Gruppe, statt eine Struktur im Speicher unbegrenzt wachsen zu lassen. Keine der beiden Grenzen benötigt Code auf Ihrer Seite, da beide automatische Rückfalllösungen sind, keine Exceptions, die Sie abfangen müssten

PDF/A-Konformität ist die eine Einstellung, die den gesamten Mechanismus komplett abschaltet, statt ihn nur zu begrenzen. HotPDF ersetzt JBIG2 in dem Moment still durch CCITT Group 4, in dem PDFACompliance nicht leer ist, auf jeder Seite, unabhängig von AccumulateGlobalsAcrossPages oder irgendetwas anderem an JBIG2Options — eine bewusste Konformitätsentscheidung, kein Bug, aber das bedeutet, dass ein archivarisches Profil und seitenübergreifende Symbolteilung sich heute gegenseitig ausschließen. Für welche Konfiguration Sie sich auch entscheiden, dekodieren Sie, was Sie geschrieben haben, bevor Sie ihm vertrauen: Laden Sie die Datei mit LoadFromFile zurück und ziehen Sie jede Seite über ExtractLoadedImage, was die gemeinsamen Globals für Sie auflöst, genauso wie es jeder konforme Reader tun würde, und vergleichen Sie das Ergebnis mit Ihren Quell-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;

Seitenübergreifende Wörterbuchteilung betrifft nur die Bilevel-Bildseite eines Dokuments. Falls dieselbe Pipeline neben den Scans auch generierte Textseiten ausgibt — Deckblätter, Indexseiten, eine OCR-Textebene —, greifen Objektströme und Xref-Ströme die andere Hälfte des Dateigrößenbudgets an, indem sie die Dokumentstruktur komprimieren, die diese Seiten hinzufügen. Seitenübergreifende JBIG2-Globals ist Teil der HotPDF-Komponente für Delphi und C++Builder, zusammen mit den Pro-Bild-JBIG2-Optionen und dem Rest der Kompressions-Pipeline