Article technique

XPS et OpenXPS vers PDF en Delphi : coordonnées et pinceaux

HotPDF convertit les paquets XPS et OpenXPS en PDF à l’intérieur de Delphi et C++Builder sans pilote d’impression, en projetant chaque coordonnée de page fixe 96 DPI à travers une unique matrice de page à échelle 0.75 retournant Y, en publiant chaque VisualBrush comme un Form XObject partagé, et en transformant les modes de tuile ImageBrush en motifs de mosaïque PDF natifs au lieu de dessins d’images répétés

Le scénario qui entraîne la plupart des ateliers Windows là-dedans est terne et incontournable. Quelque chose imprime déjà vers la Microsoft XPS Document Writer — un rapport ERP ancien, un formulaire signé, un lot de relevés — et la politique d’archivage dit PDF. XPS est un très bon format de capture et un format terrible à remettre à un système d’archivage dans dix ans. Le fichier de spool doit donc devenir un PDF page pour page, et dès que vous commencez à écrire ce convertisseur vous découvrez que la partie intéressante n’est pas le XML. C’est que XPS et PDF ne sont pas d’accord sur la position de l’origine, la valeur d’une unité, et ce qu’un pinceau a le droit d’être

Du paquet au PDF en une seule passe

Le point d’entrée est le registre de gestionnaires de documents, pas une classe XPS spéciale. THPDFDocumentHandlerRegistry.RegisterStandardHandlers installe les gestionnaires XPS, EPUB et CBZ ; la reconnaissance est fondée sur le contenu, si bien qu’un paquet portant [Content_Types].xml plus au moins une partie .fpage obtient 95 même quand l’extension du fichier ment, tandis qu’une simple extension .xps ou .oxps n’obtient que 10. Cet ordre compte quand vous acceptez des dépôts, car un attaquant renommant un EPUB en .xps ne doit pas piloter le pipeline

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 est le score honnête de cette conversion
  finally
    Output.Free;
    Registry.Free;
  end;
end;

Tout ce qui touche à la conversion est budgété avant d’être tenté. THPDFDocumentHandlerOptions.Default plafonne les entrées d’archive à 10 000, les octets d’archive décompressés à 1 GiB, le taux de compression à 200, les ressources à 4 096, et les pages à 10 000, et il porte un CancellationToken facultatif pour qu’un travail côté serveur puisse être arrêté en plein paquet. Lisez ensuite Info.UnsupportedFeatureCount et traitez une valeur non nulle comme un constat réel : HotPDF compte délibérément ce qu’il n’a pas su projeter au lieu de dessiner une approximation et de garder le silence là-dessus

Pourquoi une page XPS a-t-elle besoin d’une matrice plutôt que de coordonnées réécrites ?

Parce que réécrire les coordonnées perd la pile de transformations. Une FixedPage XPS est spécifiée en unités 96 DPI avec l’origine en haut à gauche et Y croissant vers le bas ; l’espace utilisateur PDF est en 72 DPI avec l’origine en bas à gauche et Y croissant vers le haut. La correction naïve consiste à multiplier chaque nombre par 0.75 et à soustraire chaque Y de la hauteur de page au fil de l’émission. Cela marche pour un chemin plat et s’effondre dès qu’un RenderTransform, un Canvas imbriqué, ou une matrice propre au pinceau entre en jeu, car ces transformations sont définies dans l’espace XPS et votre réécriture par coordonnée l’a déjà quitté. HotPDF garde donc la projection sous forme de matrice et la compose. HPDFXPSPageMatrix renvoie les constantes fixes une fois par page, HPDFMultiplyXPSMatrix la concatène avec la transformation de chemin accumulée, et le résultat est émis comme un unique opérateur cm avant la géométrie. Les données de chemin sont alors écrites en nombres XPS inchangés, ce qui explique aussi que la syntaxe de géométrie abrégée puisse partager le même analyseur borné que celui utilisé pour les données de chemin SVG — seul le jeton de règle de remplissage F0 ou F1 en tête est traité par l’adaptateur XPS. Si vous avez suivi le même raisonnement pour l’import vectoriel EMF et WMF, la forme de l’argument vous est familière : les formats d’import sont convertis par matrice, jamais par arithmétique sur les coordonnées feuilles

HotPDF compose la matrice de page XPS fixe avec la transformation de chemin accumulée, si bien qu’un système de coordonnées XPS 96 DPI à origine en haut à gauche atteint l’espace utilisateur PDF 72 DPI avec son origine en bas à gauche, émis comme un unique opérateur cm par visuel
La projection reste une matrice et est composée avec chaque transformation imbriquée, si bien que les données de chemin peuvent être écrites en nombres XPS inchangés
function HPDFXPSPageMatrix(PageHeight: Double): THPDFXPSMatrix;
begin
  Result.A :=  0.75;       // unité XPS 96 DPI vers point PDF 72 DPI
  Result.B :=  0;
  Result.C :=  0;
  Result.D := -0.75;       // le Y XPS croît vers le bas, le Y PDF vers le haut
  Result.E :=  0;
  Result.F := PageHeight;  // hauteur de page PDF, en points
end;

// Une CTM composée par visuel, émise avant tout opérateur de chemin
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));

Comment réutiliser un VisualBrush sans le dessiner deux fois ?

Un VisualBrush peint un arbre visuel arbitraire — enfants Canvas, Path et Glyphs — dans une région, éventuellement répété sur toute la région. HotPDF compile ce visuel une fois en Form XObject PDF puis le place, ce qui est la même stratégie de ressources que celle décrite dans l’import SVG via des Form XObjects. Deux détails décident si cela marche. D’abord, le visuel doit être parcouru comme enfants XML directs : un balayage plat à la recherche d’éléments dignes de mosaïque fait remonter les visuels imbriqués jusqu’au niveau supérieur de la page et détruit à la fois le scoping des ressources et l’ordre de peinture. Ensuite, le contenu est capturé avec la matrice de page XPS vers PDF déjà appliquée, si bien que publier le Form exige de multiplier par l’inverse de cette matrice, sinon chaque placement réapplique l’échelle 0.75 et le retournement de Y. Le Form doit aussi posséder ses ressources : HotPDF ne copie que les polices, XObjects, motifs, ExtGStates et espaces colorimétriques que le flux de contenu capturé référence réellement ; cloner tout le dictionnaire de ressources de la page entraînerait le Form en cours d’enregistrement dans son propre graphe de ressources et bâtirait un cycle. Les polices restent dans un dictionnaire direct sur les pages ordinaires et ne sont promues en dictionnaire indirect partagé que lorsque le contenu capturé contient réellement un Tf, si bien qu’un document sans visuels réutilisables ne paie pas la machinerie. Notez une frontière de spécification bonne à connaître avant de remplir un rapport de bogue : la section 13.4 de l’ECMA-388 exige que ViewboxUnits et ViewportUnits d’un VisualBrush soient tous deux Absolute, donc les unités relatives ne sont pas une fonctionnalité manquante — ce sont des entrées non conformes, et HotPDF refuse d’inventer une sémantique de coordonnées pour elles

HotPDF compile un arbre visuel VisualBrush XPS une fois en Form XObject PDF, le publie à travers l’inverse de la matrice de page fixe pour que les placements ne réappliquent pas l’échelle, et ne copie que les ressources que le contenu capturé référence réellement
Le contenu est capturé avec la matrice de page déjà appliquée, si bien que le Form est publié à travers son inverse et ne porte que les ressources que son propre flux de contenu référence

Mosaïque ImageBrush : quatre modes, quatre tailles de cellule

Les modes de tuile XPS se projettent sur les motifs de mosaïque PDF de la section 8.7.3 de l’ISO 32000-1 plutôt que d’être déployés en placements d’images répétés sur toute la zone couverte, ce qui garde la taille de sortie et le temps de conversion indépendants de la surface de page couverte par le pinceau. La correspondance est mécanique une fois qu’on la voit : la réflexion s’exprime en mettant des placements en miroir à l’intérieur d’une même cellule de motif et en agrandissant la cellule en conséquence

  • Tile — un placement, la cellule reste un viewport 1×1
  • FlipX — deux placements, cellule élargie à 2×1
  • FlipY — deux placements, cellule étendue en hauteur à 1×2
  • FlipXY — quatre placements, cellule étendue à 2×2

Chaque placement porte son propre rectangle de découpe, car une correspondance Viewbox qui déborderait de sa sous-cellule baverait dans la réflexion voisine. Le /Matrix du motif est la partie qui piège. Un motif de mosaïque est ancré à l’espace utilisateur par défaut de son flux de contenu parent, pas à l’état graphique courant au moment où le motif est sélectionné, si bien que la matrice doit composer explicitement les trois couches — la projection de page fixe, la transformation Path, et le Transform propre au pinceau — au lieu de compter sur une CTM ambiante. HotPDF valide aussi avant d’allouer : RegisterImageTilingPattern limite un motif à 1 024 placements et refuse les découpes dégénérées, les matrices non inversibles et les indices d’images invalides. Si vous voulez le modèle général côté PDF derrière cela, les motifs de mosaïque et l’espace colorimétrique Pattern couvre les opérateurs sous-jacents

// Projection de page fixe fondue dans la matrice du motif, puis celle du pinceau
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);

Que se passe-t-il quand un dégradé radial n’est pas un cercle ?

XPS définit un RadialGradientBrush avec GradientOrigin, Center, RadiusX et RadiusY, si bien que le pinceau est une ellipse. L’ombrage PDF de type 3, en section 8.7.4.5.4 de l’ISO 32000-1, fond deux cercles entre eux et n’a aucun moyen d’exprimer une ellipse directement. Faire la moyenne des deux rayons en un seul nombre est le raccourci tentant et il est visiblement faux sur tout pinceau qui n’est pas proche du rond. HotPDF déplace plutôt le problème dans le système de coordonnées : il met Y à l’échelle par RadiusY / RadiusX, enregistre un ombrage circulaire honnête dans cet espace mis à l’échelle, sélectionne le motif, et émet immédiatement l’échelle réciproque pour que la géométrie de chemin écrite ensuite reste dans l’espace utilisateur XPS d’origine

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);   // le motif capture la CTM juste ici
if Abs(ScaleY - 1) > 0.000001 then
  Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(1 / ScaleY) + ' 0 0 cm'#10);

L’ordre dans cet extrait est toute l’astuce, et il n’est pas stylistique. Un motif d’ombrage PDF capture la matrice de transformation courante au moment où il est choisi comme couleur courante, si bien que l’échelle temporaire doit être émise avant SetFillPattern ou SetStrokePattern, et la réciproque doit suivre la sélection mais précéder les opérateurs de chemin. Se tromper d’ordre dans un sens ou l’autre donne un dégradé qui se rend correctement sur le premier chemin et dérive sur tous les suivants. Une contrainte apparentée vaut pour le mode de coordonnées relatives : RadiusX et RadiusY doivent être résolus séparément contre la largeur et la hauteur du chemin, car mettre les deux à l’échelle par une seule longueur de bord change silencieusement le rapport de forme de l’ellipse sur tout chemin non carré

HotPDF projette un RadialGradientBrush XPS elliptique sur l’ombrage PDF de type 3 en mettant Y à l’échelle, en enregistrant un ombrage circulaire dans l’espace mis à l’échelle, et en émettant l’échelle réciproque seulement après que le motif a capturé la matrice de transformation courante
L’ellipse est absorbée par le système de coordonnées plutôt que par l’ombrage, et l’échelle temporaire doit encadrer la sélection du motif dans exactement cet ordre

Là où la conversion est honnête sur ses limites

Certains éléments XPS sont convertis approximativement et d’autres ne le sont pas du tout, et le choix de conception de bout en bout est de les compter plutôt que de les simuler. Les parties TIFF et JPEG XR sont rastérisées via WIC et ne portent aucune promesse sur l’alpha préservé, tandis qu’un PNG avec un canal alpha valide est scindé en image de base plus un /SMask. La taille intrinsèque d’une image est dérivée comme pixel * 96 / DPI, en lisant d’abord la densité pHYs du PNG ou JFIF du JPEG puis en retombant sur 96 DPI, si bien qu’un en-tête de densité défectueux aboutit à une taille prévisible plutôt qu’arbitraire. Les ressources de matrices non résolues, les transformations relatives non standard, ColorConvertedBitmap, les modes d’extension de dégradé non pris en charge et la géométrie malformée incrémentent tous UnsupportedFeatureCount, et une entrée malformée échoue fermé au lieu de dégénérer en un dessin silencieusement différent

Voilà la posture utile pour un convertisseur d’archivage : une conversion qui approxime en silence est pire qu’une qui vous dit quels quatre éléments elle n’a pas su représenter, car seule la seconde vous donne quelque chose à vérifier avant que le document ne soit scellé dans un système d’archivage. Si vous évaluez la conversion XPS et OpenXPS aux côtés du reste du pipeline documentaire — composition de pages, polices, signature, sortie PDF/A — la page du composant PDF HotPDF pour Delphi liste l’ensemble complet des fonctionnalités et les versions prises en charge de Delphi et C++Builder