Article technique

Exporter des pages PDF en SVG dans Delphi avec HotPDF

HotPDF exporte une page de tout document PDF chargé sous forme de balisage SVG autonome en un seul appel, BuildLoadedPageSVG, qui renvoie le document SVG complet sous forme de chaîne. Le balisage exporté porte la géométrie de la page, le texte sous forme de véritables éléments SVG text, les images matricielles intégrées, ainsi que l'état de trait que les opérateurs PDF avaient établi à chaque opération de dessin

C'est justement sur ce dernier point que la plupart des convertisseurs maison s'effondrent discrètement. Transformer une page PDF en SVG ressemble à un problème de coordonnées, mais s'avère être un problème d'état. Le PDF est une machine à pile dont l'état graphique change au fil de l'interprétation du flux de contenu ; le SVG est un arbre déclaratif dont chaque élément porte ses propres attributs de présentation. Tout ce que l'interpréteur omet de capturer au moment où un élément est émis disparaît simplement de la sortie, et l'échec est silencieux : vous obtenez un SVG valide qui affiche une page subtilement erronée

Pourquoi une page PDF ne se convertit-elle pas simplement en SVG ?

Trois incompatibilités rendent la conversion non triviale, et toutes trois produisent un résultat qui semble plausible jusqu'à ce qu'on le compare côte à côte avec l'original. La première concerne l'axe des y. L'espace utilisateur PDF croît vers le haut depuis le coin inférieur gauche de la page ; le SVG croît vers le bas depuis le coin supérieur gauche. Un simple retournement au niveau de la page corrige les coordonnées de dessin, puis casse chaque glyphe, car retourner tout le canevas retourne aussi les formes des lettres

La deuxième incompatibilité est l'héritage. En PDF, q et Q empilent et dépilent un état graphique qui inclut l'épaisseur de trait, le style de fin de trait, le style de jonction, la limite d'onglet, le motif de tirets, la phase de tirets et l'alpha. En SVG, un élément qui ne nomme pas un attribut l'hérite d'un groupe ancêtre, ce qui est une règle de portée totalement différente. Un exportateur qui ne suit que la matrice de transformation courante et oublie l'état de trait laisse l'état restauré après un Q déteindre sur les éléments qui suivent

La troisième est que le PDF exprime plusieurs choses par convention plutôt que par valeur. Les styles de fin de trait et de jonction sont des entiers, une épaisseur de trait nulle signifie un trait fin dans l'espace périphérique plutôt qu'un trait invisible, et les variantes étoilées des opérateurs de peinture changent la règle de remplissage au lieu de la couleur. Chacun de ces cas nécessite une traduction, pas une simple copie

Un seul appel pour le cas courant

Pour la tâche ordinaire d'exportation de pages destinées à une visionneuse web, un outil de comparaison ou une remise de maquette, la surface d'API se résume à une seule fonction. BuildLoadedPageSVG prend un index de page indexé à zéro dans le document actuellement chargé et renvoie le document SVG sous forme d'AnsiString :

var
  Pdf: THotPDF;
  I: Integer;
  Svg: AnsiString;
  Output: TFileStream;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('statements.pdf', '') <= 0 then
      Exit;                     // LoadFromFile renvoie le nombre de pages
    for I := 0 to Pdf.LoadedPageCount - 1 do
    begin
      Svg := Pdf.BuildLoadedPageSVG(I);
      if Length(Svg) = 0 then
        Continue;
      Output := TFileStream.Create(Format('page-%d.svg', [I + 1]), fmCreate);
      try
        Output.WriteBuffer(Svg[1], Length(Svg));
      finally
        Output.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Le même export est exposé par l'outil en ligne de commande HotPDF via sa commande export-svg, utile dans les pipelines de build et les scripts de régression où l'on souhaite une représentation d'une page comparable en texte (diff) sans écrire de code Pascal. Le SVG étant du texte, il constitue un complément naturel au chemin matriciel décrit dans le rendu d'une page PDF en bitmap : le bitmap montre à quoi ressemble la page, le SVG montre de quoi elle est faite

Comment le texte PDF est-il projeté sur les éléments texte SVG ?

HotPDF compose la chaîne de matrices de texte comme préfixe par CTM par matrice de texte par retournement de glyphe, où le retournement de glyphe est une multiplication à droite par matrix(1,0,0,-1,0,0). Ce facteur à droite existe uniquement pour annuler le retournement vertical au niveau de la page pour les formes de glyphes, car un texte SVG dessiné dans un repère local retourné apparaîtrait sinon à l'envers. Placer la correction dans la matrice plutôt que dans du code à cas particuliers signifie que le texte pivoté, retourné et incliné ressort correctement sans branchements supplémentaires

Le positionnement horizontal utilise la syntaxe multi-valeurs x de l'élément SVG text, une coordonnée par caractère, accumulée à partir de chaque avance de glyphe plus l'espacement de caractères Tc et l'espacement de mots Tw en vigueur à ce moment. La mise à l'échelle horizontale Tz est intégrée dans les colonnes a et c de la matrice de texte plutôt qu'émise séparément, de sorte qu'une visionneuse qui ignore les attributs de texte exotiques place quand même chaque glyphe là où le PDF l'avait placé. Le texte produit par une mise en forme complexe, traité dans la mise en forme de texte à écriture complexe, suit le même chemin, car le moteur de mise en forme a déjà résolu les clusters en glyphes positionnés au moment où le flux de contenu est interprété

Rotation et images : deux retournements faciles à inverser

Une page avec une entrée /Rotate non nulle nécessite une pré-transformation composée d'un retournement par rapport à la hauteur du canevas pivoté et d'une rotation exprimée dans un espace d'affichage orienté vers le haut. Les trois matrices de rotation sont (0,-1,1,0,0,W) pour 90 degrés, (-1,0,0,-1,W,H) pour 180 et (0,1,-1,0,H,0) pour 270, où W et H sont les dimensions de la page avant rotation. Les calculer à la main invite des erreurs de signe à exactement trois endroits, c'est pourquoi l'exportateur les compose via la même routine de multiplication matricielle qui gère toutes les autres transformations

Les images intégrées nécessitent leur propre retournement, car l'espace image du PDF place la première ligne d'échantillons sur le bord supérieur du carré unité, alors que l'élément SVG image utilise un repère local orienté vers le bas. La transformation émise est donc le CTM multiplié à droite par matrix(1,0,0,-1,0,1). Se tromper ici produit des photographies retournées verticalement sur une page par ailleurs parfaite, un défaut qu'un relecteur repère instantanément et qu'un test automatisé manque souvent

Que préserve réellement le périphérique d'état graphique ?

HotPDF distribue les opérateurs d'état de trait w, J, j, M et d via une interface de périphérique optionnelle distincte, de sorte que la fidélité du trait a été ajoutée sans modifier la vtable du périphérique de contenu existant et sans casser la compatibilité binaire pour le code compilé avec des versions antérieures. Concrètement, le SVG exporté reçoit des mots-clés traduits plutôt que des entiers PDF bruts :

// Les énumérations entières du PDF deviennent des attributs mots-clés SVG
//   fin de trait  0, 1, 2  ->  butt, round, square
//   jonction  0, 1, 2  ->  miter, round, bevel
//
// Une épaisseur de trait nulle signifie un trait fin dans l'espace
// périphérique en PDF, donc l'exportateur émet vector-effect="non-scaling-stroke"
// pour garder le trait visible et large d'environ un pixel périphérique après le CTM
//
// f* B* b* sélectionnent la règle pair-impair et émettent fill-rule="evenodd",
// tandis que f B b conservent la règle d'enroulement non nul par défaut de SVG

La restauration de l'état à un Q couvre ensemble l'opacité, l'épaisseur de trait, le style de fin de trait, le style de jonction, la limite d'onglet, le motif de tirets et la phase de tirets. Les Form XObjects imbriqués capturent et restaurent le même ensemble complet à leurs limites, de sorte qu'une bordure en tirets définie à l'intérieur d'un tampon ne peut pas laisser fuir son motif dans le contenu de page qui suit. Si vous suivez déjà le comportement de découpage et du CTM pour d'autres raisons, il s'agit du même modèle d'état que celui qui apparaît dans l'import vectoriel EMF et WMF, fonctionnant dans le sens inverse

Les limites à connaître avant la mise en production

L'exportateur est transparent sur son périmètre, et connaître ses limites en amont coûte moins cher que de les découvrir en production. La couleur atteint le périphérique SVG via les opérateurs rg, RG, g et G. Les remplissages établis via un espace colorimétrique plus scn, qui est la façon dont les couleurs Separation, DeviceN et ICCBased sont peintes, n'arrivent pas au périphérique sous forme de triplet RVB résolu, de sorte que les pages utilisant des couleurs d'accompagnement de cette manière exportent leur géométrie mais pas ces couleurs. Pour les sources destinées à l'impression, rastérisez plutôt, ou aplatissez d'abord les couleurs d'accompagnement ; le modèle de peinture lui-même est traité dans le rendu des couleurs d'accompagnement Separation et DeviceN

Deux remarques plus modestes font gagner du temps de débogage. Les littéraux de couleur hexadécimaux sont émis en majuscules, donc un test qui vérifie #ff0000 échoue face à un #FF0000 parfaitement correct. Et le périphérique SVG est à comptage de références via son interface, ce qui signifie que le libérer consiste à laisser l'interface sortir de portée plutôt qu'à appeler Free sur l'objet, une distinction qui compte si vous étendez le périphérique pour émettre votre propre balisage en plus du contenu de la page

L'export SVG s'associe naturellement à la comparaison structurelle lorsque vous devez savoir si un document généré a réellement changé entre deux builds. La boîte à outils plus large autour des documents chargés, du rendu à l'édition en passant par l'export, est documentée sur la page du composant PDF Delphi HotPDF