Technischer Artikel

JBIG2-Encoder-Backends und der Free-Pascal-Linker

PDFlibPas kann Binärbilder über zwei verschiedene Backends als JBIG2 kodieren. Das eine ist ein nativer Object-Pascal-MMR-Encoder, der immer vorhanden ist. Das andere ist ein externer Symbolwörterbuch-Encoder, der bei gescanntem Text deutlich kleinere Ausgaben erzeugt, und er ist optional: Ein Projekt muss die Backend-Unit einbinden, damit er überhaupt existiert. Dieser Unterschied ist die Quelle der häufigsten Überraschung bei dieser Funktion, deshalb vorab: DefaultJBIG2EncodeOptions fordert standardmäßig den externen Encoder an, und wenn die Backend-Unit nicht eingebunden ist, fällt die Anfrage still auf den Pascal-MMR-Weg zurück

Auf Delphi und C++Builder ist das externe Backend ein Satz vorgebauter statischer Objekte. Unter Free Pascal musste es zu einer DLL werden, und der Weg zu diesem Schluss ist eine Linker-Geschichte, die jedem nützlich ist, der schon einmal versucht hat, C++-Objekte in ein Free-Pascal-Programm einzubinden

Die Registrierung ist der Vertrag

Die Backend-Unit registriert sich selbst aus ihrer Initialisierungssektion heraus per Aufruf von RegisterJBIG2EncoderBackend. Aufrufer fordern ihn an, entweder über das Options-Bit PDF_JBIG2_OPTION_EXTERNAL_ENCODER mit dem Wert 4 oder über den Parameter UseExternalEncoder der erweiterten Bildeinstiegspunkte. Der Bibliotheks-Umbrella zieht die Backend-Unit bewusst nicht herein, denn der Transport eines großen Objektsatzes sollte die Entscheidung jedes Projekts sein; im C++Builder-Baum wird er beispielsweise von den Projekten, die ihn wollen, explizit eingebunden

Die Konsequenz für Aufrufer: Das Anfordern des externen Encoders ist eine Präferenz, keine Garantie, und ein Build, der die Unit vergisst, erzeugt größere Dateien statt eines Fehlers. Wenn die Ausgabegröße wichtig genug ist, um den besseren Encoder anzufordern, ist sie wichtig genug, um zu prüfen, ob Sie ihn bekommen haben

Der JBIG2-Kodier-Anfragefluss in PDFlibPas: Ohne Backend-Unit fällt die Präferenz für den externen Encoder still auf den nativen Pascal-MMR-Weg zurück
Das Anfordern des externen Symbolwörterbuch-Encoders ist eine Präferenz: Eingebunden schrumpft die Ausgabe, nicht eingebunden läuft der Pascal-MMR-Weg still mit größeren Dateien
uses
  PDFlibrary,
{$IFDEF FPC}
  PDFlibJBIG2EncDLL;    // dynamisches Backend für Free Pascal
{$ELSE}
  PDFlibJBIG2EncC;      // statischer Objektsatz für Delphi / C++Builder
{$ENDIF}

var
  Pdf: TPDFlib;
  ImageId: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.NewDocument;
    Pdf.NewPage;
    // Interpolate, SymbolExtract, UseExternalEncoder, SkipBlackDots,
    // BlackDotSize, LossyLevel
    ImageId := Pdf.AddImageJBIG2FromFileEx('scan-page-1.tif',
      0, 1, 1, 0, 0, 0);
    if ImageId = 0 then
      raise Exception.Create('JBIG2 encoding failed');
    Pdf.SaveToFile('archive.pdf');
  finally
    Pdf.Free;
  end;
end;

Die Unit zu kompilieren war zweizeilig. Die Symbole waren die Arbeit

Die Backend-Unit selbst unter Free Pascal zum Kompilieren zu bringen, kostete genau zwei Änderungen: den Assembler-Dialekt zu setzen und einen datensatzbasierten Konstruktor für Formateinstellungen durch die globale Default-Variable zu ersetzen. Das bildet ziemlich genau ab, wie portabel geradliniges Pascal zwischen den beiden Compilern ist

Die Symbolseite war die eigentliche Arbeit. Der Objektsatz referenziert 176 C-Symbole. Davon hatten 128 bereits Pascal-Implementierungen innerhalb der Unit und brauchten nur exportierte Namen angehängt, denn Delphi nutzt den Funktionsnamen als Symbolnamen, während Free Pascal eine explizite public-name-Deklaration verlangt. Siebenundzwanzig teilte der JPEG-2000-Codec und mussten von genau einer Stelle exportiert werden, denn eine doppelte Definition bricht jedes Programm, das beides einbindet. Die übrigen 21 waren Plattform- und C-Runtime-Einträge, sechzehn Win32-Dateifunktionen plus eine Handvoll Standardbibliotheksaufrufe, und sie wanderten in eine neue Kompatibilitäts-Unit

Keines davon ist konzeptionell schwer, und all davon ist nötig, bevor der Linker es überhaupt versucht. Am Linker scheiterte es dann

Drei Linking-Routen, drei Sackgassen

Der interne Free-Pascal-Linker kann die Objektdateien nicht lesen, denn sie stammen von einem Compiler, der assoziative COMDAT-Sektionen erzeugt, und der interne Linker meldet, dass er sie nicht unterstützt. Das ist eine kategorische Weigerung, keine Warnung

Der Wechsel zu einem externen Linker schien die Antwort. Der mit Free Pascal gebündelte binutils-Linker stürzt kategorisch ab, während er Section Garbage Collection auf dieses Archiv anwendet, und dieses Flag ist Teil des festen Parametersatzes, den Free Pascal für das 64-Bit-Windows-Ziel übergibt, also lässt es sich nicht von der Kommandozeile entfernen; die dokumentierten Schalter zu seiner Unterdrückung werden auf diesem Weg ignoriert. Eine viel neuere binutils-Version scheitert anders: Sie kann das Free-Pascal-Linkskript überhaupt nicht verarbeiten und erzeugt ohne das Skript eine leere Ausgabe und mit dem Skript eine Wand von Relocation-Fehlern

Eine unterwegs entdeckte Grenze ist es wert zu kennen, selbst wenn Sie das Linker-Problem nie treffen. Der externe Linker löst Objektdateipfade relativ zum Ausgabeverzeichnis der ausführbaren Datei auf, nicht relativ zum Quellbaum, also funktioniert eine relative Include-Object-Direktive nur, wenn das Ausgabeverzeichnis zufällig dem Arbeitsverzeichnis zur Compilezeit gleicht. Das kann eine Bibliothek über das Projekt eines Nutzers nicht annehmen, was für sich schon ein Grund ist, eine gelinkte Bibliothek losen Objekten vorzuziehen

Drei gescheiterte Linker-Routen für C++-JBIG2-Encoder-Objekte unter Free Pascal und die DLL mit zwei flachen C-Einstiegspunkten, die das Problem löste
COMDAT-Sektionen besiegen den internen Linker, beide externen Linker scheitern, also wird der C++-Encoder als eine DLL ausgeliefert, die die Backend-Unit dynamisch bindet

Warum ein anderer C++-Compiler nicht hilft

Der naheliegende nächste Gedanke: die C++-Seite mit einem Compiler neu zu bauen, dessen Objekte Free Pascal lesen kann. Das funktioniert ebenfalls nicht, und der Grund ist fundamental statt eine Frage von Schaltern. Eine minimale C++-Übersetzungseinheit mit einem Template, kompiliert mit allen Codegenerierungs-Features ausgeschaltet, emittiert trotzdem weak external symbols, denn die Template- und Inline-Instantiierung erzeugt sie konstruktionsbedingt. Free Pascal weist diese Symbolklasse kategorisch zurück. Die Gegenrichtung scheitert ebenso: Ein gängiger C++-Linker kann die Objekte des anderen Compilers wegen derselben COMDAT-Sektionsbehandlung nicht konsumieren

Der C++-Code lässt sich also auf keiner verfügbaren Route als Objekte an Free Pascal liefern. Als DLL lässt er sich liefern, und genau das geschah: Der Encoder und seine Bildverarbeitungs-Abhängigkeit sind in einer Bibliothek mit zwei flachen C-Einstiegspunkten gebaut, und die Free-Pascal-Backend-Unit bindet diese dynamisch und registriert sich exakt so wie das statische Backend. Der Delphi- und C++Builder-Weg wurde überhaupt nicht angefasst, was das richtige Ergebnis ist; ein Portabilitätsproblem auf einer Toolchain sollte die Toolchain, die bereits funktioniert, nicht stören

Die Polarität ist die eine Falle

Zwischen einer Windows-Binärbitmap und einem JBIG2-Encoder klafft eine Konventionsdiskrepanz, die kein Typsystem abfängt. Eine Scanline eines geräteunabhängigen Bitmap mit einem Bit pro Pixel behandelt ein gesetztes Bit als weiß. Der Encoder behandelt ein gesetztes Bit als schwarz. Geben Sie die Scanlines unverändert weiter, erhalten Sie einen völlig gültigen JBIG2-Stream des fotografischen Negativs Ihrer Seite

Die Polaritätskonventionen von Ein-Bit-DIB und JBIG2: Ein gesetztes Bit ist in der Scanline weiß und im Encoder schwarz, gelöst durch Invertieren jedes Bytes
Dieselben Bytes, entgegengesetzte Bedeutung: Ohne das Invertieren jedes Bytes erzeugt der Encoder einen gültigen JBIG2-Stream des fotografischen Negativs
// Ein-Bit-DIB: gesetztes Bit heißt weiß. JBIG2-Encoder: gesetztes Bit
// heißt schwarz. Auf dem Weg hinein jedes Byte invertieren
for I := 0 to RowBytes - 1 do
  Row[I] := Row[I] xor $FF;

Die Verifikationsmethode ist so wichtig wie der Fix. Komprimierte Streamlängen zu vergleichen sagt nichts, denn ein Negativbild komprimiert auf ähnliche Größe. Die Seite anzusehen beweist nur, dass sie nicht offensichtlich invertiert ist. Die zuverlässige Prüfung ist, die Ausgabe beider Kodierwege, nativ Pascal und extern, nach PNG zu rendern und byteweise zu vergleichen: Beide Encoder sind auf demselben Quellbild verlustfrei, also ist alles außer einer exakten Übereinstimmung ein Fehler in einem von ihnen. Dieser Vergleich ist inzwischen ein permanenter Regressionstest, und es ist die Art von Assertion, die sich zu bauen lohnt, wann immer zwei Implementierungen exakt übereinstimmen sollen

Welches Backend zum Einsatz kommen soll

Bei allgemeinem Binärinhalt, geditherten Halbtönen, Line Art, gemischten Grafiken ist der native Pascal-MMR-Encoder ausreichend und hat keine Deployment-Kosten. Bei gescanntem Text, dem Fall, für den JBIG2 entworfen wurde, sitzt die Größenreduktion beim externen Symbolwörterbuch-Encoder, denn er faktorisiert wiederholte Glyphenformen in ein Wörterbuch, statt jedes Vorkommen neu zu kodieren. Wer Archive gescannter Dokumente erzeugt, für den ist dieser Unterschied groß genug, um die Speicherplanung zu verändern

Die vorgelagerte Frage, wie das Binärbild überhaupt entsteht, ist für die Ausgabegröße genauso wichtig; das regionenbasierte Monochrom-Rendering behandelt der Artikel zum Monochrome-Region-Rendering, und die dokumentweite Größenstrategie PDF-Dateigrößenoptimierung und Font-Subsetting. Bei Scansätzen mit wiederholten Seiten schlägt Deduplizierung oft die bessere Kompression, das Thema von perzeptueller Bild-Deduplizierung. Toolchain- und Backend-Verfügbarkeit je Plattform listet die Produktseite der losLab PDF Developer Library