Technischer Artikel

EMF-Vektorimport mit Free Pascal in PDFlibPas

PDFlibPas konvertiert Enhanced Metafiles Datensatz für Datensatz in echten PDF-Seiteninhalt, statt sie zu rastern, und genau das hält ein importiertes Diagramm oder eine CAD-Zeichnung bei jedem Zoom gestochen scharf. Dieser Konverter umfasst rund 6500 Zeilen und wurde gegen die VCL geschrieben, also wurde er als nicht portierbar eingestuft und durch einen Stub ersetzt, als die Bibliothek ein Free-Pascal-Ziel bekam. Diese Einstufung war falsch, und die Art, wie sie falsch war, ist eine nützliche Lektion darüber, wie man eine Abhängigkeit prüft, bevor man beschließt, sie per Rewrite zu umgehen

Die tatsächliche VCL-Oberfläche dieser 6500 Zeilen erwies sich als klein: eine Bitmap-Klasse, genutzt für Pixelformat, Stream-Speicherung, Handle, Canvas und Scanlines; eine Metadatei-Klasse, genutzt für Breite, Höhe und Handle; und der Farbtyp mit zwei Konstanten. All das stellte die hauseigene Graphics-Unit der Bibliothek bereits bereit, die genau dazu existiert, damit der Nicht-VCL-Build Äquivalente hat. Der Konverter hing überhaupt nicht an der VCL. Er hing an der Windows-Unit von Free Pascal

Auf der Achse splitten, von der der Code tatsächlich abhängt

Die Änderung war also keine Neuimplementierung. Es war ein einziges Conditional: von „Kompiliere den Stub, wenn ohne die VCL gebaut wird“ zu „Kompiliere den Stub, wenn nicht für Windows gebaut wird“. Das ist die richtige Achse, und die Begründung macht den Unterschied offensichtlich. Eine Enhanced Metafile ist ein Windows-Container. Der Konverter ist von oben bis unten ein Parser für Windows-GDI-Datensätze. Ob die Host-Anwendung die VCL nutzt, ein anderes Widget-Set oder gar keins, hat nichts damit zu tun, ob sich diese Datensätze interpretieren lassen; ob das Ziel Windows ist, hat damit alles zu tun

Die Konsequenzen der richtigen Achse fallen nebenbei ab. C++Builder-Builds, die in dieser Bibliothek das Windows-Plattform-Symbol undefinieren, behalten den auslösenden Stub und verhalten sich exakt wie zuvor. macOS behält den Stub, zu Recht, denn dort gibt es keine GDI-Datensätze zu parsen. Delphi-VCL-Builds bleiben unangetastet. Und ein Windows-Build mit einem Nicht-VCL-Widget-Set bekommt den EMF-Vektorimport als Nebeneffekt geschenkt, den niemand implementieren musste. Ein Conditional, das an der realen Abhängigkeit ausgerichtet ist, macht aus Plattformarbeit eine Ein-Zeilen-Änderung; ein Conditional an der falschen macht daraus einen Rewrite, der nie eingeplant wird

Das EMF-Import-Conditional wird von der VCL-Zugehörigkeit auf die Windows-Plattform umgeaxt, behält Stubs anderswo und verschafft Nicht-VCL-Windows-Builds den Vektorimport
Verlagert man die Stub-Bedingung auf die Windows-Plattform, bleibt jedes bestehende Build-Verhalten erhalten und Nicht-VCL-Windows-Ziele bekommen den EMF-Vektorimport gratis

Die Free-Pascal-Lücke waren Deklarationen, nicht Logik

Was tatsächlich fehlte, waren die Win32-Deklarationen, die die Windows-Unit von Delphi bereitstellt und die von Free Pascal nicht. Sie in einer einzigen Kompatibilitäts-Unit zu sammeln, statt Conditionals durch den Konverter zu verstreuen, hielt den Parser lesbar. Die Liste ist aufschlussreich, weil sie zeigt, wie ungleichmäßig die Header-Abdeckung zwischen den beiden RTLs ist: 113 Konstanten für Metadatei-Datensatztypen, zwei Flags für erweiterte Textausgabe, drei Konstanten für Gradient-Fill-Modi, ein Handle-Table-Zeigertyp, Aliasse für die Gradient-Vertex- und Primitive-Datensätze und drei Datensatztypen, die Free Pascal gar nicht deklariert, für Alpha Blending, transparentes Blitting und den Color-Management-Modus

Keines davon ist einzeln betrachtet spannend. Aber all davon muss stimmen, bevor der Parser kompiliert, und eine Kompatibilitäts-Unit ist das natürliche Zuhause, weil sie sich als Ganzes gegen die Header-Dokumentation diffen lässt

Win32-Deklarationen, die der Windows-Unit von Free Pascal fehlen, gesammelt in einer Kompatibilitäts-Unit für den EMF-nach-PDF-Vektorkonverter
Datensatzkonstanten, Flags, Aliasse und drei fehlende Datensatztypen leben alle in einer Kompatibilitäts-Unit, die sich gegen die Header-Dokumentation diffen lässt

Die Deklaration, die still das falsche Bild zeichnet

Zwei dieser Deklarationen fehlen nicht nur, sie sind vorhanden und für diesen Zweck falsch, und genau dieser Teil ist es wert, gemerkt zu werden, selbst wenn Sie nie eine Metadatei anfassen

Free Pascal deklariert den Brush-Erzeugungs-Datensatz mit der eingebetteten Laufzeit-Brush-Struktur und den Extended-Pen-Datensatz mit der eingebetteten Laufzeit-Pen-Struktur. Beide Laufzeitstrukturen deklarieren ihr Hatch-Member als zeigergroßen Integer, denn in einem echten GDI-Aufruf kann dieses Member ein Handle tragen. Eine Metadatei speichert jedoch immer die 32-Bit-Form, denn das Datensatz-Layout ist Teil des serialisierten Dateiformats und ändert sich nicht mit der Prozess-Bitbreite

Bei 32-Bit-Builds stimmen beide überein und nichts passiert. Auf Win64 ist das zeigergroße Member acht Bytes groß, wo die Datei vier hat, also wird jedes Feld nach dem Hatch-Member vom falschen Offset gelesen. Keine Exception, kein Parse-Fehler, keine Warnung. Die Metadatei rendiert nur falsch: Farben aus den falschen Bytes, Pen-Breiten aus den falschen Bytes, und ein Bild, das wie ein Rendering-Fehler aussieht statt wie ein Struct-Layout-Fehler. Delphi liefert aus genau diesem Grund explizite 32-Bit-Varianten beider Strukturen mit, und die Kompatibilitäts-Unit deklariert sie auf dieselbe Weise neu

Byte-Layout des EMF-Brush-Datensatzes: Ein zeigergroßes Hatch-Feld verschiebt auf Win64 die nachfolgenden Felder um vier Bytes gegenüber dem festen 32-Bit-Layout
Der serialisierte Datensatz speichert immer einen 4-Byte-Hatch, deshalb liest die zeigergroße Laufzeitstruktur auf Win64 still jedes spätere Feld falsch
// Falsch auf Win64: Hatch ist zeigergroß, die Datei speichert 32 Bit,
// und jedes nachfolgende Feld verschiebt sich um vier Bytes, ohne Fehler
type
  TLogBrushRuntime = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: ULONG_PTR;      // 8 Bytes in einem 64-Bit-Prozess
  end;

// Richtig: das serialisierte Layout, feste Breite unabhängig von der Bitbreite
type
  TLogBrush32 = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: DWORD;          // immer 4 Bytes, wie in der Metadatei gespeichert
  end;

Die allgemeine Regel: Jede Struktur, die sowohl als Laufzeit-API-Argument als auch als serialisiertes Feldlayout auftaucht, braucht zwei Deklarationen, und die serialisierte muss durchweg Typen fester Breite verwenden. Zeigergroße Member in einem Dateiformat sind immer ein Fehler, der auf einen 64-Bit-Build wartet

Signaturunterschiede gehören in einen Wrapper, nicht an jede Aufrufstelle

Die übrigen Unterschiede waren gewöhnliche Signaturabweichungen, und sie absorbiert man mit einem Weiterleitungs-Wrapper statt mit einem Conditional an jeder Aufrufstelle. Die Transform-Kombinationsfunktion nimmt unter Free Pascal Zeiger, wo Delphi Referenzparameter nimmt, also nimmt der Wrapper Referenzen und übergibt Adressen. Er kopiert zudem beide Quellargumente zuerst in lokale Variablen, denn der Konverter hat Aufrufstellen, an denen die Zielmatrix zugleich eine der Quellen ist, und dieselbe Adresse zweimal an eine Funktion zu übergeben, die schreibt, während sie liest, erzeugt eine Transformation, die subtil falsch ist, und zwar so, dass es erst bei rotiertem Inhalt auffällt

function CombineTransformCompat(var Dest: TXForm;
  const A, B: TXForm): BOOL;
var
  SrcA, SrcB: TXForm;
begin
  // Erst kopieren: Aufrufer dürfen Dest legitim als A oder B übergeben
  SrcA := A;
  SrcB := B;
{$IFDEF FPC}
  Result := Windows.CombineTransform(@Dest, @SrcA, @SrcB);
{$ELSE}
  Result := Windows.CombineTransform(Dest, SrcA, SrcB);
{$ENDIF}
end;

Die Rechteck- und Punkttypen sind der andere Fall. Free Pascal behandelt die Rechteck- und Punkt-Datensätze der Metadatei als eigene Typen, getrennt von den allgemeinen Grafiktypen, also brauchten acht Zuweisungsstellen ein explizites Casting zwischen Datensätzen mit identischem Layout. Beide Compiler akzeptieren die Cast-Form, deshalb tragen diese Stellen überhaupt kein Conditional, was eine kleine Hässlichkeit wert ist

Was das für einen Free-Pascal-Einsatz ändert

Der EMF-Vektorimport funktioniert auf Windows unter Free Pascal und erzeugt denselben Seiteninhalt wie der Delphi-Build: Pfade als Pfade, Gradients als Pattern-Inhalt, Text als Text. Außerhalb von Windows bleibt der Rasterweg die Antwort, und das ist eine Grenze des Formats, nicht des Ports. Den Koordinaten- und Clipping-Zustand, in den der Konverter einspeist, beschreibt der Artikel zum Content-Stream-CTM- und Clipping-Tracker, und die von ihm ausgegebenen Vektorprimitive behandelt Vektorgrafiken, Shaders und Gradients

Wer die eigene Codebasis auf dieselbe Gelegenheit hin prüft, dem nützt die Übung, mit der alles begann: Listen Sie die Member auf, die Sie tatsächlich aus dem Framework verwenden, von dem Sie glauben, abzuhängen. Die Antwort fällt oft viel kürzer aus, als die Importliste vermuten lässt, und die echte Beschränkung liegt meist ganz woanders. Gerätekontext-basierte Importwege beschreibt allgemein der Artikel zu Druckvorschau und Gerätekontext, und die Plattform- und Toolchain-Abdeckung listet die Produktseite der losLab PDF Developer Library