Technischer Artikel

HotPDF unter Free Pascal: Deflate, AES und Codec-Grenzen

HotPDF kompiliert und läuft unter Free Pascal 3.2.2 mit Lazarus, und die ehrliche Zusammenfassung dieses Ports sind zwei Sätze. Dokumenterzeugung, Laden, Speichern, Kompression, Dekompression, Verschlüsselung und Entschlüsselung funktionieren alle auf reinen Pascal-Backends, sodass eine Lazarus-Anwendung echtes PDF ohne jegliche C-Abhängigkeit erzeugen und konsumieren kann. Die optionalen nativen Bildcodecs tun es nicht, denn die vorgebauten Win64-Objekte verwenden eine COFF-Variante, die kein Free-Pascal-Linker konsumieren kann, sodass auf dieser Toolchain die Einstiegspunkte zu Stubs aufgelöst werden, die passiv scheitern

HotPDF-Free-Pascal-Fähigkeitskarte: funktionierende Pascal-Deflate-, AES- und Dokument-Backends neben Bildcodec-Stubs, die passiv scheitern
Dokument-, Kompressions- und Verschlüsselungsfeatures laufen auf reinen Pascal-Backends, während native Bildcodecs zu Stubs aufgelöst werden, die passiv scheitern

Von „kompiliert“ bis „funktioniert“ brauchte es einen bestimmten Satz Fixes, und jeder einzelne ist eine Falle, die jede andere Delphi-Codebasis erwischen wird, die zu Free Pascal zieht. Es lohnt sich, sie in der Reihenfolge festzuhalten, in der sie wehtaten

Warum beweist das Kompilieren einer Unit nichts?

Weil eine Pascal-Unit auf ein Symbol verweisen kann, das niemals etwas Nützliches tun wird, und trotzdem den Compiler zufriedenstellt. Als alle 113 Bibliotheks-Units unter Free Pascal sauber bauten, funktionierten die Archivcontainer-Handler tatsächlich, verifiziert durch einen Smoke-Test, der eine CBZ öffnete und sie nach PDF konvertierte. XFA-Formularflattening funktionierte überhaupt nicht, denn Flattening muss den komprimierten /XFA-Paketstrom inflateen, und der Deflate-Einstiegspunkt war noch ein Stub. Nichts im Build-Output unterschied diese beiden Fälle

Die Regel, die daraus entstand, ist kurz. Bevor man in eine Release-Note schreibt, dass ein Feature auf einer neuen Toolchain funktioniert, schreibe man eine Runtime-Sonde, die das Feature auf dieser Toolchain Ende-zu-Ende ausübt. Compile-Abdeckung ist eine Voraussetzung, nie ein Beweis. Das größere Bild dessen, was der Port abdeckt, liefern die Free-Pascal- und Lazarus-Win64-Supportnotizen

Ein Raise in einem cdecl-Stub erreicht den Aufrufer nicht

Das verdient eine eigene Sektion, weil das Symptom so irreführend ist. Die Stub-Units legen C-Einstiegspunkte offen, wie es eine statische Bibliothek täte, also sieht ein Stub so aus

// Sieht vernünftig aus. Ist es nicht.
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
  public name 'inflate';
begin
  raise ENotSupportedException.Create('codec unavailable');
end;

Auf Free Pascal für Win64 propagiert diese Exception nicht zum Aufrufer. Es gibt keinen try..except-Handler, der sie sieht, denn das Entfalten über eine so deklarierte cdecl-Grenze trägt den Pascal-Exceptionframe nicht mit sich; der Prozess terminiert mit Exit-Code 217. Aus Sicht der Anwendung gibt es keinen Fehler, keine Meldung und keine Logzeile, nur ein Programm, das verschwindet. Das ist strikt schlechter als eine falsche Antwort, denn eine falsche Antwort kann behandelt werden

Warum eine in einem cdecl-Stub ausgelöste Exception einen Free-Pascal-Prozess mit Exit-Code 217 beendet und wie das Vorschalten des Pascal-Einstiegspunkts es behebt
Der Pascal-Exceptionframe kann nicht über die cdecl-Grenze entfalten, also stirbt der Prozess still; die Korrektur schaltet vor, bevor der Stub überhaupt erreicht wird

Der verführerische Fix ist, den Stub stattdessen einen Fehlercode zurückgeben zu lassen, und bei inflate ist das richtig, weil zlib einen klar definierten Fehler-Rückgabewert hat. Im Allgemeinen ist es falsch: Ein Stub für jpeg_read_header, der null zurückgibt, sagt dem Aufrufer, mit einer Struktur weiterzumachen, die niemand initialisiert hat. Der dauerhafte Fix ist, am Pascal-Einstiegspunkt vorzuschalten statt im C-förmigen Stub, mit der Fehlerkonvention, die diese API bereits hat

function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
  // Ablehnen, bevor der Stub überhaupt erreicht wird, mit der eigenen
  // Fehlerkonvention dieser API statt einer Exception über cdecl
  Bitmap := nil;
  Result := False;
  Exit;
{$ENDIF}
  Result := DecodeJPEGNative(Data, Bitmap);
end;

paszlib ist nicht zlib, und der Unterschied sind zwei Dokumentklassen

Die unter Free Pascal verfügbare Pascal-Deflate-Implementierung beherrscht zwei Framings: den zlib-Wrapper und rohes Deflate. Sie beherrscht kein gzip-Framing, das zlib über windowBits-Werte von 16 bis 31 wählt, und sie beherrscht nicht den automatischen Erkennungsmodus, den Werte 32 bis 47 wählen. HotPDF braucht beides. Der sichere SVG-Import-Pfad fordert 31 an, und der Loader hat eine Fallback-Leiter, die 47 anfordert, wenn das Framing eines Streams mehrdeutig ist. Lässt man eines davon weg, hört eine ganze Dokumentfamilie auf zu öffnen, mit einem Decode-Fehler, der auf den Stream zeigt statt auf das fehlende Framing

windowBits-Abdeckung von paszlib gegen zlib: gzip-Framing und Auto-Detect-Bereiche fehlen für HotPDF-SVG-Import und Loader-Fallback
paszlib beherrscht den zlib-Wrapper und rohes Deflate, aber HotPDF braucht auch windowBits 31 und 47, also muss der Shim das gzip-Framing selbst liefern

Es gibt eine zweite, schärfere Inkompatibilität. Der z_stream-Datensatz, den paszlib deklariert, hat nicht dasselbe Speicherlayout wie das C-Pendant: Sein msg-Feld ist ein Shortstring statt eines Zeigers, und total_in und total_out sind 64 Bit, wo die C-ABI Maschinenworte hat. Ein Aufrufer-Datensatz kann also nicht direkt durchgereicht werden. Die funktionierende Anordnung ist, den paszlib-Zustand hinter dem state-Zeiger zu halten, den der öffentliche Datensatz bereits reserviert, und die öffentlichen Felder um jeden Aufruf herum hinein und heraus zu kopieren. Die gzip-CRC und der acht Byte lange Längstrailer werden in derselben Shim-Schicht verrechnet, was der natürliche Ort dafür ist, da sie ohnehin die Framing-Entscheidung besitzt

Ein dynamisches Array an einen untypisierten var-Parameter übergeben

Das ist der Fehler, der gerade jetzt mit größter Wahrscheinlichkeit in Ihrem Code sitzt. Wenn Sie ein dynamisches Array an einen untypisierten var-Parameter übergeben, erhält der Empfänger die Adresse der Array-Variablen, also die Adresse eines Zeigers, nicht die Adresse der Nutzdaten. Ein Lesevorgang hinein überschreibt also die Variable selbst und was auch immer daneben liegt

var
  FBuffer: TBytes;
begin
  SetLength(FBuffer, 65536);

  // Falsch: übergibt die Adresse der FBuffer-Variablen
  FStream.Read(FBuffer, Length(FBuffer));

  // Richtig: übergibt die Adresse des ersten Nutzdaten-Bytes
  FStream.Read(FBuffer[0], Length(FBuffer));
end;

Auf Delphi erscheint die falsche Form häufig zu funktionieren, weil sie einen benachbarten Stackslot korrumpiert, den danach nichts liest. Auf Free Pascal verursacht dieselbe Zeile einen Segmentierungsfehler bei der ersten Verwendung. Was sie so schwer mit dem Auge zu erkennen macht: Statische Arrays haben dieses Problem nicht, denn eine statische Array-Variable ist ihre eigenen Nutzdaten, also sind beide Schreibweisen in derselben Datei korrekt, je nach Deklaration ein paar hundert Zeilen weiter

ZIP-Container ohne System.Zip

Free Pascal hat kein Äquivalent der RTL-Zip-Unit, und die verfügbare Alternative hat sowohl eine andere API-Oberfläche als auch keine Unterstützung für die Legacy-Verschlüsselung, die ältere Containerformate noch verwenden, also erwies sich ein kleiner hauseigener Reader als kürzer als die Anpassung an sie. Zwei Formatdetails kosteten Zeit und sind leicht falsch zu machen

Das erste ist das Prüfbyte des Verschlüsselungsheaders. Sein zwölftes Byte ist normalerweise das hohe Byte der CRC, aber wenn das allgemeine Flag-Bit 3 gesetzt ist, was bedeutet, dass die Größen in einem nachgelagerten Data Descriptor leben und die CRC noch nicht bekannt ist, stammt das Prüfbyte stattdessen aus dem hohen Byte der Änderungszeit. Implementiert man nur die CRC-Form, weist jedes im Streaming-Modus geschriebene Archiv ein korrektes Passwort zurück. Das zweite ist das ZIP64-Extra-Feld: Seine drei 64-Bit-Felder erscheinen in fester Reihenfolge, werden aber nur geschrieben, wenn das entsprechende 32-Bit-Feld gesättigt ist, also funktioniert das Lesen an festen Offsets bei den Archiven, die Sie getestet haben, und scheitert am nächsten. Parsen Sie sie positional danach, welche 32-Bit-Felder gesättigt sind

Eine Annehmlichkeit, die man kennen sollte: Der Free-Pascal-Dekompressionsstream nimmt ein zweites Konstruktorargument, das den zlib-Header überspringt, was genau das ist, was ZIP-Einträge brauchen, denn sie speichern rohes Deflate. Dieser Pfad berührt den zlib-Shim der Bibliothek überhaupt nicht, also ist er vom fehlenden C-Backend unberührt

Farbglyph-Transparenz unter der LCL

Das Auslesen des Alphakanals eines gerasterten Farbglyphs ist das eine Grafikdetail ohne direkte Übersetzung. Die LCL-PNG-Klasse hat keinen Scanline-Accessor, der Alpha freigibt, und die Zuweisung eines PNG an ein Bitmap verwirft ihn, sodass ein Farb-Emoji vollständig opak ankommt und sich mit einer schwarzen Box dahinter komponiert. Der funktionierende Weg ist das Interface-Bild: aus dem PNG erzeugen, dann Pixel über den Farb-Accessor lesen, im Kopf behaltend, dass seine Komponenten 16 Bit haben und um acht nach unten geschoben werden müssen, um Bytes zu werden. Diese Oberfläche verwendet außerdem die natürliche Zeilenreihenfolge von oben nach unten, also muss die Height - 1 - Y-Inversion, die VCL-Scanline-Code braucht, entfernt statt portiert werden

Zwei Build-System-Notizen, bevor Sie einen Bug melden

Ein vollständiger Rebuild scheitert gelegentlich mit einem undefinierten Symbol, dessen Name auf ein $crc-Suffix und einen Hexwert endet. Dieses Suffix wird aus den Parametertypen berechnet, und es passt nicht, wenn ein Build eine Unit im selben Durchlauf gegen zwei unterschiedliche Interface-Versionen kompiliert. Ein erneuter Lauf des Builds räumt es weg; die Signatur ist nicht falsch

Zweitens hat Free Pascal 3.2.2 keine anonymen Methoden, also greift der Free-Pascal-Build überall dort, wo die Bibliothek Closures verwendete, um eine parallele Pipeline zu verdrahten, stattdessen auf einen deterministischen seriellen Fallback zurück. Die Ausgabe ist identisch, der Durchsatz nicht; wenn Sie auf paralleles Seitenrendering angewiesen sind, ist das ein Grund, vorerst bei Delphi zu bleiben, und das Pipeline-Design beschreibt der Artikel zur parallelen Render-Pipeline. Die Bildcodec-Situation ist der andere Ort, an dem die Toolchain-Wahl die Fähigkeiten verändert und nicht nur die Geschwindigkeit, also sollte ein Lazarus-Deployment seine Bildformate entsprechend planen; die aktuelle Matrix je Toolchain steht auf der Produktseite der HotPDF Delphi PDF-Komponente