HotPDF konvertiert XPS- und OpenXPS-Pakete in Delphi und C++Builder zu PDF, ohne einen Druckertreiber, indem es jede 96-DPI-Fixed-Page-Koordinate durch eine einzige 0.75-skalierte, Y-spigelnde Seitenmatrix mappt, jeden VisualBrush als geteiltes Form XObject veröffentlicht und ImageBrush-Tile-Modi in native PDF-Tiling-Patterns überführt statt in wiederholte Bildzeichnungen
Das Szenario, das die meisten Windows-Werkstätten hierher zieht, ist fad und unausweichlich. Etwas druckt bereits in den Microsoft XPS Document Writer — ein Legacy-ERP-Bericht, ein signiertes Formular, ein Stapel Abrechnungen — und die Archivrichtlinie sagt PDF. XPS ist ein völlig ordentliches Erfassungsformat und ein schreckliches, um es einem Records-System zehn Jahre später zu übergeben. Also muss die Spool-Datei ein seitengetreues PDF werden, und in dem Moment, in dem man diesen Konverter schreibt, entdeckt man, dass der interessante Teil nicht das XML ist. Sondern dass XPS und PDF uneins sind darüber, wo der Ursprung liegt, was eine Einheit wert ist und was ein Brush sein darf
Vom Paket zum PDF in einem Durchlauf
Der Einsprungpunkt ist die Document-Handler-Registry, keine spezielle XPS-Klasse. THPDFDocumentHandlerRegistry.RegisterStandardHandlers installiert die XPS-, EPUB- und CBZ-Handler; die Erkennung basiert auf Inhalten, sodass ein Paket mit [Content_Types].xml plus mindestens einem .fpage-Part 95 Punkte erzielt, selbst wenn die Dateiendung lügt, während eine bloße .xps- oder .oxps-Endung nur 10 Punkte erzielt. Diese Reihenfolge zählt, wenn Sie Uploads annehmen, denn ein Angreifer, der ein EPUB in .xps umbenennt, soll die Pipeline nicht steuern
var
Handled: IHPDFHandledDocument;
Info: THPDFDocumentHandlerInfo;
Options: THPDFDocumentHandlerOptions;
Registry: THPDFDocumentHandlerRegistry;
Output: TFileStream;
begin
Registry := THPDFDocumentHandlerRegistry.Create;
Output := TFileStream.Create('spool.pdf', fmCreate);
try
Registry.RegisterStandardHandlers;
Options := THPDFDocumentHandlerOptions.Default;
if not Registry.OpenFromFile('spool.xps', '', Options, Handled, Info) then
raise Exception.Create('no registered handler recognised the package');
if Handled.Format = hdfXPS then
Handled.WritePDF(Output, Options, Info);
// Info.UnsupportedFeatureCount ist der ehrliche Punktestand dieser Konvertierung
finally
Output.Free;
Registry.Free;
end;
end;
Alles an der Konvertierung wird budgetiert, bevor es unternommen wird. THPDFDocumentHandlerOptions.Default begrenzt Archiv-Einträge auf 10.000, entpackte Archiv-Bytes auf 1 GiB, die Kompressionsrate auf 200, Ressourcen auf 4.096 und Seiten auf 10.000, und es trägt ein optionales CancellationToken, damit ein serverseitiger Job mitten im Paket gestoppt werden kann. Lesen Sie danach Info.UnsupportedFeatureCount und behandeln Sie einen Wert ungleich null als echten Befund: HotPDF zählt bewusst, was es nicht mappen konnte, statt eine Annäherung zu zeichnen und darüber zu schweigen
Warum braucht eine XPS-Seite eine Matrix statt umgeschriebener Koordinaten?
Weil Koordinaten umzuschreiben den Transformationsstapel verliert. Eine XPS-FixedPage ist in 96-DPI-Einheiten spezifiziert, mit dem Ursprung oben links und Y nach unten wachsend; der PDF-Benutzerraum ist 72-DPI mit dem Ursprung unten links und Y nach oben wachsend. Die naive Reparatur: jede Zahl mit 0.75 multiplizieren und jedes Y beim Ausgeben von der Seitenhöhe abziehen. Das funktioniert für einen flachen Pfad und fällt auseinander, sobald eine RenderTransform, ein verschachteltes Canvas oder eine Brush-lokale Matrix dazukommt, denn diese Transformationen sind im XPS-Raum definiert, und Ihre Koordinate-für-Koordinate-Umschreibung hat ihn bereits verlassen. HotPDF hält die Projektion deshalb als Matrix und komponiert sie. HPDFXPSPageMatrix liefert die festen Konstanten einmal pro Seite, HPDFMultiplyXPSMatrix verkettet sie mit der akkumulierten Pfadtransformation, und das Ergebnis wird als ein einziger cm-Operator vor der Geometrie ausgegeben. Pfaddaten werden dann in unveränderten XPS-Zahlen geschrieben, weshalb auch die abgekürzte Geometriesyntax denselben begrenzten Parser teilen kann wie SVG-Pfaddaten — nur das führende F0- oder F1-Fill-Rule-Token behandelt der XPS-Adapter. Wenn Sie dieselbe Überlegung beim EMF- und WMF-Vektorimport verfolgt haben, ist die Form des Arguments vertraut: Importformate werden per Matrix konvertiert, nie durch Arithmetik auf Blattkoordinaten
function HPDFXPSPageMatrix(PageHeight: Double): THPDFXPSMatrix;
begin
Result.A := 0.75; // 96-DPI-XPS-Einheit zu 72-DPI-PDF-Punkt
Result.B := 0;
Result.C := 0;
Result.D := -0.75; // XPS-Y wächst nach unten, PDF-Y nach oben
Result.E := 0;
Result.F := PageHeight; // PDF-Seitenhöhe, in Punkten
end;
// Eine komponierte CTM pro Visual, ausgegeben vor jedem Pfadoperator
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));
Wie nutzt man einen VisualBrush wieder, ohne ihn zweimal zu zeichnen?
Ein VisualBrush malt einen beliebigen Visual-Baum — Canvas-, Path- und Glyphs-Kinder — in eine Region, evtl. über sie wiederholt. HotPDF kompiliert diesen Visual einmalig in ein PDF-Form XObject und platziert es dann, was dieselbe Ressourcenstrategie ist wie im Artikel zum SVG-Import über Form XObjects beschrieben. Zwei Details entscheiden, ob es funktioniert. Erstens muss der Visual als direkte XML-Kinder durchlaufen werden: Ein flacher Scan nach tile-würdigen Elementen zerrt verschachtelte Visuals auf die Seitenspitze und zerstört Ressourcen-Scoping wie Malreihenfolge. Zweitens wird der Inhalt mit bereits angewandter XPS-zu-PDF-Seitenmatrix erfasst, sodass das Veröffentlichen des Forms die Multiplikation mit der Inversen dieser Matrix verlangt, sonst wendet jede Platzierung die 0.75-Skalierung und den Y-Spiegel erneut an. Das Form muss auch seine Ressourcen besitzen: HotPDF kopiert nur die Fonts, XObjects, Patterns, ExtGStates und Farbräume, die der erfasste Content-Stream wirklich referenziert; das gesamte Seitenressourcen-Dictionary zu klonen zerrte das gerade registrierte Form in seinen eigenen Ressourcengraph und baute einen Zyklus. Fonts bleiben auf gewöhnlichen Seiten in einem direkten Dictionary und werden nur dann in ein geteiltes indirektes Dictionary befördert, wenn erfasster Inhalt wirklich ein Tf enthält, sodass ein Dokument ohne wiederverwendbare Visuals nicht für die Maschinerie zahlt. Eine Spezifikationsgrenze lohnt das Wissen, bevor Sie einen Bug melden: ECMA-388 Abschnitt 13.4 verlangt, dass sowohl ViewboxUnits als auch ViewportUnits an einem VisualBrush Absolute sind, also sind relative Einheiten kein fehlendes Feature — sie sind nicht konforme Eingabe, und HotPDF weigert sich, Koordinatensemantik für sie zu erfinden
ImageBrush-Tiling: vier Modi, vier Zellgrößen
XPS-Tile-Modi mapen auf PDF-Tiling-Patterns aus ISO 32000-1 Abschnitt 8.7.3, statt zu wiederholten Bildplatzierungen über die bedeckte Fläche aufgebläht zu werden, was Ausgabegröße und Konvertierungszeit unabhängig davon hält, wie viel der Brush von der Seite bedeckt. Das Mapping ist mechanisch, sobald man es sieht: Die Spiegelung wird ausgedrückt, indem gespiegelte Platzierungen in eine Pattern-Zelle gelegt und die Zelle passend vergrößert wird
Tile— eine Platzierung, Zelle bleibt 1×1-ViewportFlipX— zwei Platzierungen, Zelle auf 2×1 verbreitertFlipY— zwei Platzierungen, Zelle auf 1×2 erhöhtFlipXY— vier Platzierungen, Zelle auf 2×2 erweitert
Jede Platzierung trägt ihr eigenes Clip-Rechteck, denn ein Viewbox-Mapping, das seine Sub-Zelle überläuft, würde in die Nachbarspiegelung hineinbluten. Die Pattern-/Matrix ist der Teil, der Leute erwischt. Ein Tiling-Pattern ist an den Standardbenutzerraum seines Eltern-Content-Streams verankert, nicht an den Grafikzustand, der beim Auswählen des Patterns aktuell ist, also muss die Matrix alle drei Schichten explizit komponieren — die Fixed-Page-Projektion, die Pfadtransformation und die Brush-lokale Transform — statt sich auf eine ambient CTM zu verlassen. HotPDF validiert auch, bevor es allokiert: RegisterImageTilingPattern begrenzt ein Pattern auf 1.024 Platzierungen und weist degenerierte Clips, nicht invertierbare Matrizen und ungültige Bildindizes zurück. Das allgemeine PDF-seitige Modell dahinter behandelt der Artikel zu Tiling-Patterns und dem Pattern-Farbraum samt der zugrundeliegenden Operatoren
// Fixed-Page-Projektion in die Pattern-Matrix eingefaltet, dann die Brush-lokale
PatternMatrix := HPDFMatFromOps( 0.75 * PathMatrix.A, -0.75 * PathMatrix.B,
0.75 * PathMatrix.C, -0.75 * PathMatrix.D,
0.75 * PathMatrix.E,
PageHeight - 0.75 * PathMatrix.F);
if Brush.HasTransform then
PatternMatrix := HPDFMatMul(PatternMatrix, BrushMatrix);
PatternName := Document.RegisterImageTilingPattern(Resource.ImageIndex,
Brush.Viewport.Left, Brush.Viewport.Top,
Brush.Viewport.Left + CellWidth, Brush.Viewport.Top + CellHeight,
CellWidth, CellHeight, Placements, pttNoDistortion, PatternMatrix);
Was geschieht, wenn ein radialer Farbverlauf kein Kreis ist?
XPS definiert eine RadialGradientBrush mit GradientOrigin, Center, RadiusX und RadiusY, der Brush ist also eine Ellipse. PDF Shading Typ 3, in ISO 32000-1 Abschnitt 8.7.4.5.4, blendet zwischen zwei Kreisen und hat keine Möglichkeit, eine Ellipse direkt auszudrücken. Die beiden Radien in eine Zahl zu mitteln ist der verlockende Kurzweg, und er ist sichtbar falsch bei jedem Brush, der nicht nahezu rund ist. HotPDF verlagert das Problem stattdessen ins Koordinatensystem: Es skaliert Y um RadiusY / RadiusX, registriert ein ehrliches kreisförmiges Shading in diesem skalierten Raum, wählt das Pattern und gibt unmittelbar die reziproke Skalierung aus, sodass die als nächstes geschriebene Pfadgeometrie weiterhin im ursprünglichen XPS-Benutzerraum liegt
ScaleY := Brush.RadiusY / Brush.RadiusX;
PatternName := Document.RegisterMultiStopRadialGradient(
Brush.StartX, Brush.StartY / ScaleY, 0,
Brush.EndX, Brush.EndY / ScaleY, Brush.RadiusX,
StopPositions, StopColours, 3);
if Abs(ScaleY - 1) > 0.000001 then
Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(ScaleY) + ' 0 0 cm'#10);
Page.SetFillPattern(PatternName); // das Pattern fängt die CTM genau hier ein
if Abs(ScaleY - 1) > 0.000001 then
Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(1 / ScaleY) + ' 0 0 cm'#10);
Die Reihenfolge in diesem Schnipsel ist der ganze Trick, und sie ist nicht stilistisch. Ein PDF-Shading-Pattern fängt die aktuelle Transformationsmatrix in dem Moment ein, in dem es als aktuelle Farbe gewählt wird, also muss die temporäre Skalierung vor SetFillPattern oder SetStrokePattern ausgegeben werden, und das Reziproke folgt der Auswahl, geht aber den Pfadoperatoren voraus. Die Reihenfolge in irgendeine Richtung falsch, und Sie haben einen Farbverlauf, der auf dem ersten Pfad korrekt rendert und auf jedem folgenden abdriftet. Eine verwandte Einschränkung gilt für den relativen Koordinatenmodus: RadiusX und RadiusY müssen getrennt gegen Pfadbreite und Pfadhöhe aufgelöst werden, denn beide mit einer einzigen Kantenlänge zu skalieren verändert stillschweigend das Seitenverhältnis der Ellipse auf jedem nicht-quadratischen Pfad
Wo die Konvertierung ehrlich über ihre Grenzen ist
Manche XPS-Konstrukte werden angenähert konvertiert, manche gar nicht, und die Entwurfsentscheidung durchgehend lautet: sie zählen statt sie zu faken. TIFF- und JPEG-XR-Parts werden über WIC rasterisiert und tragen kein Versprechen über bewahrtes Alpha, während PNG mit gültigem Alphakanal in ein Basisbild plus eine /SMask gespalten wird. Die intrinsische Bildgröße wird als pixel * 96 / DPI abgeleitet, wobei zuerst PNG-pHYs oder JPEG-JFIF-Dichte gelesen und sonst auf 96 DPI zurückgefallen wird, sodass ein fehlerhafter Dichte-Header bei einer vorhersagbaren statt einer willkürlichen Größe landet. Unaufgelöste Matrix-Ressourcen, nicht standardkonforme relative Transformationen, ColorConvertedBitmap, nicht unterstützte Gradient-Spread-Modi und fehlerhafte Geometrie erhöhen alle UnsupportedFeatureCount, und fehlerhafte Eingabe scheitert geschlossen, statt zu einer stillschweigend anderen Zeichnung abzubauen
Das ist die nützliche Haltung für einen Archivierungs-Konverter: Eine Konvertierung, die stillschweigend annähert, ist schlechter als eine, die Ihnen sagt, welche vier Elemente sie nicht darstellen konnte, denn nur die zweite gibt Ihnen etwas zum Prüfen, bevor das Dokument in ein Records-System versiegelt wird. Wenn Sie XPS- und OpenXPS-Konvertierung neben dem Rest der Dokumenten-Pipeline evaluieren — Seitenkomposition, Fonts, Signierung, PDF/A-Ausgabe — listet die Seite der HotPDF Delphi PDF component den vollständigen Funktionsumfang und die unterstützten Delphi- und C++Builder-Versionen