Technischer Artikel

EMF- und WMF-Vektoren mit HotPDF in Delphi-PDFs importieren

HotPDF, die native Delphi- und C++Builder-PDF-Komponente, importiert Windows-EMF- und WMF-Metadateien, indem es jeden GDI-Datensatz direkt in PDF-Operatoren übersetzt, statt die Datei zu einer Bitmap zu rastern: Verlaufsfüllungen werden zu PDF-Axialschattierungsmustern, Schraffurpinsel werden zu PDF-Kachelmustern, und ein zentrales Pfadzustands-Gate verhindert, dass fehlerhafte Datensätze die Ausgabe beschädigen. Jedes Diagramm, das ein TChart, eine GDI+-Oberfläche oder ein einfaches TCanvas als erweiterte Metadatei exportieren kann, ist ein Kandidat für diesen Pfad, und der Unterschied zeigt sich in dem Moment, in dem jemand in die Seite hineinzoomt oder sie an einen hochauflösenden Drucker schickt

Die Alternative, zu der die meisten Delphi-Entwickler standardmäßig greifen, besteht darin, die Metadatei vor dem Platzieren auf der Seite zu einer Bitmap zu rastern, und die Kosten zeigen sich erst später: Ein Balkendiagramm, das auf dem Bildschirm scharf aussah, wird sichtbar blockig, sobald das PDF mit 600 DPI gedruckt oder auf eine Konferenzraum-Leinwand projiziert wird, und eine schraffiert gefüllte CAD-Fläche kollabiert zu einem einzigen flachen grauen Rechteck, wenn der Füllstil nicht durchgereicht wird. Die Metadatei als Programm statt als Bild zu lesen, vermeidet beide Probleme, ist aber der schwierigere Weg, ihn korrekt zu implementieren – weshalb es sich lohnt, die folgenden Fallstricke zu kennen, bevor ein Report ausgeliefert wird

Warum eine Metadatei interpretieren statt sie zu einer Bitmap zu rastern?

HotPDF hält den EMF- und WMF-Import auf dem Vektorpfad, weil eine Windows-Metadatei eine aufgezeichnete Folge von GDI-Zeichenaufrufen ist, kein Bild, und das erneute Abspielen dieser Aufrufe als PDF-Pfad-, Text- und Schattierungsoperatoren ist das, was das Ergebnis wie den Rest der Seite skalieren lässt. THPDFPage.ShowMetafile und sein Gegenstück ShowMetafileEx sind die Einstiegspunkte, die eine Anwendung aufruft, und beide übergeben die Metadatei an THPDFWmf, die Klasse, die jeden GDI-Datensatz durchläuft und übersetzt. Die Unterscheidung ist nicht absolut, und HotPDF gibt auch nicht vor, dass sie es sei: Ein Metadatei-Datensatz, der tatsächlich Rasterdaten sind, zum Beispiel ein StretchDIBits-Bitmap-Blit, wird als echtes PDF-Image-XObject über AddImage und ShowImage eingebettet, dasselbe Aufrufpaar, das jedes andere Bild auf der Seite durchläuft, statt es in Pfadoperatoren zu zwingen, die eine Fotografie nicht ausdrücken können. Linien, Füllungen und Text bleiben vektoriell; Pixel, die in der Quelle bereits Pixel waren, bleiben Pixel in der Ausgabe. Der einfachste Aufruf braucht nichts weiter als die geladene Metadatei:

var
  Pdf: THotPDF;
  Chart: TMetafile;
begin
  Pdf := THotPDF.Create(nil);
  Chart := TMetafile.Create;
  try
    Chart.LoadFromFile('quarterly-revenue.emf');  // exported from TChart or GDI+
    Pdf.FileName := 'quarterly-report.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.ShowMetafile(Chart);
    Pdf.EndDoc;
  finally
    Chart.Free;
    Pdf.Free;
  end;
end;

Wie verwandelt der Interpreter GDI-Koordinaten in PDF-Seitenraum?

HotPDF beantwortet das mit einem einzigen Durchlauf über den eigenen Datensatzstrom der Metadatei, statt einer zweiten Implementierung von GDI. THPDFWmf.Analyse liest den Metadatei-Header über den Win32-Aufruf GetEnhMetaFileHeader, setzt seinen internen Zeichenzustand zurück und ruft EnumEnhMetafile auf, dieselbe Enumerations-API, die auch ein Metadatei-Viewer verwenden würde, sodass jeder EMR_*-Datensatz THPDFWmf.ExecuteRecord in der Reihenfolge erreicht, in der er ursprünglich aufgezeichnet wurde. GDI drückt Koordinaten top-down in Geräte- oder logischen Einheiten aus, die vom eigenen Mapping-Modus der Metadatei gewählt werden; eine PDF-Seite ist bottom-up in User-Space-Punkten, dem Koordinatensystem, das in HotPDFs Canvas-Zeichenmodell für Pfade und Füllungen behandelt wird. Jeder Datensatz-Handler löst diese Diskrepanz über ScaleX und ScaleY auf, die ProjectX und ProjectY aufrufen, um GDIs eigene Window-zu-Viewport-Formel für die anisotropen und isotropen Mapping-Modi nachzuvollziehen, sodass eine mit fünf logischen Einheiten Breite aufgezeichnete Form unabhängig davon, welche Window- und Viewport-Ausdehnungen die Quellanwendung gesetzt hat, in der korrekten Breite in PDF-Punkten landet

Wie wird eine GDI-Verlaufsfüllung zu einem PDF-Schattierungsmuster?

Ein EMR_GRADIENTFILL-Datensatz wird immer dann zu einem echten PDF-Type-2-Axialschattierungsmuster (ISO 32000-1 §8.7.4.5), wenn GDI ihn in einem der beiden Rechteck-Modi aufgezeichnet hat. THPDFWmf.VEMRGradientFill liest das eigene Layout des Datensatzes direkt aus dem rohen Byte-Puffer gemäß der MS-EMF-§2.3.1.6-Struktur: ein Vertex-Array aus 16-Bit-RGBA-Eckpunkten, gefolgt von einer Liste von Rechtecken, die jeweils auf zwei dieser Vertices verweisen. Bei GRADIENT_FILL_RECT_H verlaufen die Farben von links nach rechts entlang der horizontalen Mittellinie des Rechtecks; bei GRADIENT_FILL_RECT_V verlaufen sie von oben nach unten entlang der vertikalen Mittellinie. So oder so gehen die beiden Eckfarben und die projizierten Rechteckkoordinaten direkt in THotPDF.RegisterAxialGradient, das einen Musternamen zurückgibt, und die Seite zeichnet das Rechteck und füllt es über dieses Muster (SetFillPattern), statt über einen flachen SetRGBFillColor-Aufruf, sodass ein tabellenartiger Bereichskopf mit Farbbändern oder der Verlaufs-Plotbereich eines Diagramms seinen Übergang behält, statt zu einer einzigen Durchschnittsfarbe zu kollabieren

Der Gouraud-Dreiecksmodus ist die ehrliche Lücke. Wenn das ulMode-Feld des Datensatzes GRADIENT_FILL_TRIANGLE meldet, erkennt VEMRGradientFill das, protokolliert, dass der Dreiecksmodus noch nicht implementiert ist, und überspringt das Rechteck, statt eine Zweifarb-Näherung zu raten. Interpolation pro Vertex und pro Pixel über ein beliebiges Dreiecksnetz lässt sich nicht auf eine axiale oder radiale Schattierung mit zwei Farbstopps reduzieren, und sie korrekt auszudrücken würde bedeuten, eine PDF-Type-4- oder Type-5-Mesh-Schattierung auszugeben – dieselbe Schattierungsfamilie, die auch HotPDFs Seiten-Renderer beim Zurücklesen eines PDFs ungemalt lässt. Zwei voneinander unabhängige Codepfade landen an derselben Grenze: Mesh-Schattierungen sind sowohl auf der Schreib- als auch auf der Leseseite die Lücke, und ein Quelldiagramm, das Gouraud-Dreiecke für einen sanften radialen Glanz verwendet, fällt auf das zurück, was der letzte deckende Pinsel war, nicht auf eine gerenderte Näherung

Schraffurpinsel werden zu Kachelmustern, nicht zu flachem Grau

Ein GDI-Schraffurpinsel behält seine Textur im PDF, weil THPDFWmf.SetBrushColor CurrentBrush.lbStyle auf BS_HATCHED prüft, bevor es überhaupt auf eine deckende Füllung zurückfällt, und diesen Fall stattdessen an SetHatchBrushPattern weiterleitet. Diese Methode schreibt einen 8-mal-8-Einheiten-PDF-Inhaltsstrom aus gestrichenen Linienoperatoren, m, l und S, gewählt nach dem GDI-Schraffurstil: ein einzelner horizontaler oder vertikaler Strich für HS_HORIZONTAL und HS_VERTICAL, drei parallele Diagonalen für HS_FDIAGONAL und HS_BDIAGONAL, sowie die Kombinationen aus horizontal-plus-vertikal bzw. beiden Diagonalen für HS_CROSS und HS_DIAGCROSS. THotPDF.RegisterTilingPattern registriert diesen Inhaltsstrom als farbiges Kachelmuster (PaintType 1, ISO 32000-1 §8.7.3.1) mit einem 8-Einheiten-XStep und YStep, und die Seite füllt über SetFillPattern genauso wie bei einer Axialschattierung. Ein CAD-Grundriss oder eine technische Zeichnung, die sich auf Schraffurfüllungen stützt, um Materialien zu unterscheiden, behält diese visuelle Sprache im PDF, statt jeden Bereich zu identischem Grau zu verlieren

Nicht jeder Pinsel bekommt diese Behandlung, und die Lücke lohnt es sich zu kennen, bevor ein CAD-Import ausgeliefert wird. EMR_CREATEDIBPATTERNBRUSHPT, der Datensatz für einen benutzerdefinierten Bitmap-Bild-Musterpinsel statt einem der sechs Standard-Schraffurstile von GDI, registriert nur sein Handle, damit spätere SELECTOBJECT- und DELETEOBJECT-Datensätze konsistent bleiben; HotPDF stellt noch keine PDF-Pattern-Ressourcen-Pipeline für beliebige Kachelbilder bereit, sodass die Auswahl dieses Pinsels auf eine Volltonfarben-Ersatzlösung statt auf die Quelltextur zurückfällt. Wenn eine Füllung flach dargestellt wird, wo das Original eindeutig eine sich wiederholende Bildtextur verwendet hat, ist der Quellpinsel mit hoher Wahrscheinlichkeit ein benutzerdefiniertes DIB-Muster statt einer Standardschraffur, und das ist der eine Fall, den es sich lohnt, zuerst von Hand zu prüfen. Das Konfigurieren eines Imports für eine solche Zeichnung läuft weiterhin über dasselbe Options-Objekt:

var
  Pdf: THotPDF;
  Drawing: TMetafile;
  Options: THPDFEmfOptions;
begin
  Pdf := THotPDF.Create(nil);
  Drawing := TMetafile.Create;
  Options := THPDFEmfOptions.Create;
  try
    Drawing.LoadFromFile('floor-plan.emf');
    Options.Assign(Pdf.EmfOptions);   // start from the document-wide defaults
    Options.Redraw := False;          // interpret the original EMF bytes, no GDI re-record pass
    Options.ShowNullBrush := True;    // keep explicitly unfilled CAD regions visible
    Options.UseFrame := True;         // clip output to the frame the EMF header declares
    Pdf.FileName := 'floor-plan.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.ShowMetafileEx(Drawing, Options);
    Pdf.EndDoc;
  finally
    Options.Free;
    Drawing.Free;
    Pdf.Free;
  end;
end;

Was verhindert, dass eine fehlerhafte Metadatei die Seite beschädigt?

HotPDFs Antwort ist ein einziges Gate am Anfang von ExecuteRecord, statt einer defensiven Prüfung, die in jedem der etwa achtzig Datensatz-Handler wiederholt wird. Eine GDI-Pfadklammer, geöffnet durch EMR_BEGINPATH und geschlossen durch EMR_ENDPATH oder EMR_ABORTPATH, wird von einer privaten PathContinue-Eigenschaft verfolgt, die vom Feld FPathContinue gestützt wird. Solange diese Klammer offen ist, lässt ExecuteRecord nur Pfadkonstruktions-Datensätze durch: die Varianten Move, Line, Polyline, Polygon, Polybezier und Polydraw, plus CLOSEFIGURE und eine kleine Menge an Transformations- und DC-Zustands-Datensätzen wie SETWORLDTRANSFORM, SAVEDC und RESTOREDC. Jeder andere Datensatztyp, der ExecuteRecord erreicht, während die Klammer offen ist, etwa ein verirrter EXTTEXTOUT oder ein Bitmap-Blit, wird zentral verworfen, mit einem einzigen Exit in dem Moment, in dem er ankommt

Dieses Gate existiert, weil bei einer Pfadklammer in einer handgeschriebenen, werkzeuggenerierten oder schlicht beschädigten Metadatei nicht garantiert ist, dass sie nur das enthält, was eine wohlgeformte Datei zwischen ihren Öffnungs- und Schließdatensätzen platzieren würde. Ein Textausgabe-Datensatz, der zwischen EMR_BEGINPATH und EMR_ENDPATH landet, würde ohne Gate entweder die im Aufbau befindliche Pfadgeometrie verunreinigen oder einen PDF-Textausgabeoperator mitten in einer Sequenz ausgeben, die reine Pfadkonstruktion sein soll, und beide Fehlerarten sind genau die Sorte, die bei einer fehlerhaften Eingabe von einem Drittanbieter-Tool zutage tritt, nicht bei irgendetwas, das eine normale Testsuite zufällig abdeckt. Die Prüfung in ExecuteRecord zu zentralisieren bedeutet, dass die einzelnen VEMR*-Handler sich nicht jeweils dagegen wappnen müssen, zum falschen Zeitpunkt aufgerufen zu werden; das Gate entscheidet das einmal, vor dem Dispatch, statt achtzigmal danach

Ein Vektordiagramm neben Text und Bildern auf einer Seite platzieren

Eine Report-Seite enthält selten nur ein Diagramm, und ShowMetafile lässt sich mit HotPDFs anderen Seitenoperatoren genau so kombinieren wie jeder andere Zeichenaufruf. Eine mit TextOut gezeichnete Überschrift, ein als EMF importiertes, schraffiert gefülltes Balkendiagramm und ein mit ShowImage platziertes Logo können alle auf derselben Seite im selben Inhaltsstrom landen, jedes mit seiner nativen Wiedergabetreue, das Kompositionsmuster, das in HotPDFs Leitfaden zum Layout von Text, Schriften und Bildern in einem Report behandelt wird:

Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Q2 Regional Sales');
Pdf.CurrentPage.ShowMetafile(RegionChart);   // hatch-filled bars, still vector
Pdf.CurrentPage.ShowImage(LogoIndex, 450, 760, 90, 30, 0);

Der EMF- und WMF-Interpreter, die von ihm für Verlaufsfüllungen registrierten Axialschattierungsmuster und die hier beschriebene Kachelmuster-Zuordnung für Schraffurpinsel sind alle Teil der Standard-HotPDF-Komponente für Delphi und C++Builder, einer nativen VCL-Bibliothek ohne externe DLL-Abhängigkeit für all das