Article technique

Création de PDF à partir de zéro avec le composant PDFium dans Delphi

PDFium a la réputation d'être un moteur de visualisation, le moteur de rendu derrière l'onglet PDF de Chrome, la première chose à clarifier est donc que le composant PDFium peut également créer un document qui n'a jamais existé auparavant. Le côté création enveloppe l'API d'objet de page (page-object API) de PDFium : vous créez un document vide, ajoutez des pages avec des dimensions explicites et déposez du texte, des chemins vectoriels et des images sur chaque page aux coordonnées de votre choix. Il n'y a pas de langage de description de page à apprendre ni de pilote d'impression dans la boucle. Vous appelez des méthodes, la bibliothèque assemble les objets PDF et SaveAs sérialise le résultat

Ce que vous n'obtenez pas, c'est un moteur de mise en page (layout engine). C'est assez important pour le dire d'emblée, car cela façonne chaque exemple ci-dessous. Le composant PDFium place le contenu là où vous le lui indiquez, en coordonnées absolues, et nulle part ailleurs. Il n'enveloppera pas (wrap) un paragraphe, ne fera pas couler le texte sur un saut de page, ni ne calculera un tableau à partir de lignes et de colonnes. C'est votre travail. Si vous êtes arrivé en vous attendant à quelque chose qui redistribue (reflows) la prose à la manière d'un traitement de texte, calibrez-vous maintenant : il s'agit d'une API de placement précise et de bas niveau, plus proche du dessin sur une zone de dessin (canvas) que de la composition (typesetting) d'un document. Pour les factures générées, les certificats, les étiquettes et les pages de rapport où vous savez déjà où chaque élément appartient, cette précision est exactement ce que vous voulez

Le minimum qui produit un fichier

Trois appels se dressent entre un TPdf vide et un PDF enregistré : créez le document, ajoutez une page, écrivez-le. Tout le reste est du contenu que vous superposez entre les deux

uses
  Vcl.Graphics,   // pour clBlack et TColor
  PDFium;         // TPdf vit ici

procedure CreateBlankPdf(const FileName: string);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                 // document vide en mémoire
    Pdf.AddPage(0, 595, 842);           // A4 portrait, en points
    Pdf.AddText('Première page', 'Arial', 18, 50, 780);
    Pdf.SaveAs(FileName);               // sérialiser sur disque
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Un détail fait trébucher les personnes qui ont vu des extraits (snippets) plus anciens : vous n'affectez pas Pdf.Active := True après CreateDocument. La propriété Active signale si un descripteur de document (document handle) existe, et CreateDocument en a déjà créé un, de sorte que la propriété est à True au moment où cet appel revient. Le redéfinir est au mieux une absence d'opération (no-op) et au pire trompeur pour le prochain lecteur. Active gagne sa croûte à la sortie : l'affectation de False libère le document sous-jacent avant Free, ce qui est l'ordre de démontage propre (clean teardown order). Traitez CreateDocument et une ouverture de chargement de fichier comme mutuellement exclusifs. La bibliothèque refuse de créer un nouveau document sur un TPdf qui en a déjà un ouvert, la réutilisation signifie donc fermer d'abord le document courant

Les coordonnées commencent en bas à gauche

La deuxième paire d'arguments d'AddText, et de tout appel de placement, est un point dans l'espace utilisateur PDF. L'origine se situe dans le coin inférieur gauche de la page, X s'étend vers la droite et Y s'étend vers le haut. Une unité est un point, 1/72 de pouce, donc une page A4 fait 595 par 842 unités et US Letter (lettre US) fait 612 par 792. Ce Y ascendant est la source la plus courante de confusion "mon texte est hors de la page", car les coordonnées de l'écran et de l'image bitmap placent l'origine en haut, avec Y qui croît vers le bas. Sur une page de 842 points de haut, un titre près du haut se situe autour de Y 780, et non de Y 60. Lorsqu'une exécution atterrit dans un endroit inattendu, la hauteur de la page moins votre Y est presque toujours le nombre que vous vouliez vraiment dire

AddPage prend une position d'insertion comme premier argument, exprimée en base 1, avec 0 comme raccourci pratique de "début de document". Passez 0 ou 1 pour la première page et la page est insérée au début ; transmettez la valeur correspondant au décompte (count) auquel vous ajoutez afin d'ajouter à la fin. La page nouvellement ajoutée devient également la page courante, celle que ciblent les appels de dessin ultérieurs, il n'y a donc pas d'étape distincte de "sélection de cette page" après son ajout. Si vous ajoutez plusieurs pages et avez ensuite besoin de redessiner sur une page précédente, définissez PageNumber pour déplacer le curseur ; pendant que vous remplissez les pages dans l'ordre où vous les créez, vous pouvez le laisser tranquille

Écrire du texte et la règle de police qui mord silencieusement

La signature AddText contient tout ce dont une seule exécution (single run) a besoin : la chaîne (string), un nom de police, une taille en points, l'ancre (anchor) X et Y, puis la couleur optionnelle, un octet alpha pour la transparence et un angle de rotation en degrés

procedure WriteHeader(Pdf: TPdf; const Title, Author: string);
begin
  // Titre en noir, opacité par défaut, pas de rotation
  Pdf.AddText(Title, 'Arial', 20, 50, 780);
  // Une signature d'auteur (byline) plus claire 24 points en dessous
  Pdf.AddText('Par ' + Author, 'Arial', 11, 50, 756, clGray);
  // Un léger tampon de brouillon diagonal en travers de la page
  Pdf.AddText('BROUILLON', 'Arial', 64, 180, 380, clGray, $30, 45.0);
end;

L'octet alpha va de $00 (invisible) à $FF (opaque), ce qui fait du tampon de brouillon un filigrane (watermark) plutôt qu'un bloc solide : $30 correspond à environ dix-neuf pour cent d'opacité, ce qui est suffisant pour lire au travers. L'angle fait pivoter l'exécution dans le sens inverse des aiguilles d'une montre autour de son ancre, de sorte que 45 degrés donne le tampon classique d'un coin à l'autre. Rien de tout cela ne nécessite une fonctionnalité de filigrane séparée. Un filigrane n'est qu'un appel AddText de grande taille, semi-transparent et pivoté, et le dessiner avant ou après le corps (body) décide s'il se trouve derrière ou au-dessus du contenu

Les polices méritent une attention particulière, car le mode d'échec est silencieux. Lorsque vous passez un nom de police, le composant PDFium demande au système d'exploitation les données TrueType de cette police et les intègre (embeds) dans le document, c'est pourquoi un fichier construit sur votre machine se rend de manière identique sur une machine où la police n'a jamais été installée. Le piège est ce qui se passe lorsque le nom n'est pas résolu : une faute de frappe ou une police (face) qui n'est tout simplement pas présente sur la machine de génération. Il n'y a pas d'exception. La bibliothèque se replie (falls back) sur la création d'un objet texte qui porte le nom comme une simple étiquette, avec rien d'intégré, et laisse le lecteur substituer tout ce qu'il considère comme proche. Le texte apparaît dans vos tests, semble plausible, et modifie les métriques ou les glyphes dès le moment où le fichier s'ouvre quelque part où des polices différentes sont installées. Utilisez des noms dont vous savez qu'ils sont présents sur la machine génératrice, traitez la liste des polices comme une dépendance de déploiement, et ouvrez un échantillon dans une visionneuse sur un système propre avant de faire confiance à la sortie

Formes vectorielles : créer un chemin (path), puis le valider (commit)

Les lignes, les rectangles et les régions remplies passent par un chemin (path). Vous en ouvrez un avec CreatePath, qui définit le point de départ et tout le style en même temps, le mode de remplissage (fill mode), les couleurs de remplissage et de trait (stroke) avec leurs propres octets alpha, la largeur de trait, les extrémités (caps) et les jointures de ligne. Vous l'étendez ensuite avec LineTo, BezierTo et ClosePath, et enfin AddPath valide (commits) le chemin terminé sur la page. L'étape de validation est facile à oublier et ne produit rien si vous la sautez

procedure DrawDivider(Pdf: TPdf; X, Y, Width: Single);
begin
  // Une fine règle horizontale. La surcharge rectangle définit une boîte directement :
  // X, Y, Width, Height, puis le mode de remplissage et les couleurs.
  Pdf.CreatePath(X, Y, Width, 0.5, fmNone, clBlack, $FF,
    True, clBlack, $FF, 1.0);
  Pdf.AddPath;
end;

procedure DrawTriangle(Pdf: TPdf);
begin
  // Surcharge par points : commencez au premier sommet, ligne vers le reste, fermez.
  Pdf.CreatePath(200, 300, fmWinding, clBlue, $80, True, clNavy, $FF, 2.0);
  Pdf.LineTo(300, 300);
  Pdf.LineTo(250, 400);
  Pdf.ClosePath;
  Pdf.AddPath;          // rien n'est dessiné jusqu'à ce que ceci s'exécute
end;

Deux surcharges (overloads) couvrent les cas courants. La forme à quatre coordonnées prend X, Y, la largeur et la hauteur et vous donne un rectangle aligné sur les axes en un seul appel, ce qui est ce que vous utilisez pour dessiner une règle, une bordure de cellule ou un panneau d'arrière-plan rempli. La forme à deux coordonnées ne définit qu'un point de départ, et vous tracez le reste du contour vous-même avec LineTo et BezierTo. Le mode de remplissage (Fill mode) contrôle la façon dont les régions superposées (overlapping) sont peintes : fmWinding (nonzero winding - enroulement non nul) convient à la plupart des formes pleines, fmAlternate (even-odd - pair-impair) gère les découpes et les contours qui s'entrecroisent, et fmNone laisse un chemin uniquement tracé (stroked-only) sans remplissage, ce qu'utilise le séparateur ci-dessus

Les tableaux sont des chemins et du texte, assemblés à la main

Parce qu'il n'y a pas de primitive de tableau, un tableau est une boucle. Vous décidez des décalages X (offsets) de colonne et de la hauteur de ligne, écrivez chaque cellule avec AddText, et dessinez les règles avec des chemins de rectangle. L'arithmétique vous appartient, mais elle est simple, et une fois écrite, elle se généralise à n'importe quelle grille dont vous avez besoin

procedure DrawTable(Pdf: TPdf; Left, Top: Double);
const
  ColX: array[0..2] of Double = (0, 110, 210);  // décalages de colonne
  RowH = 20;
var
  Y: Double;
  Row: Integer;
begin
  // Ligne d'en-tête
  Pdf.AddText('Article', 'Arial', 10, Left + ColX[0], Top);
  Pdf.AddText('Qté', 'Arial', 10, Left + ColX[1], Top);
  Pdf.AddText('Prix', 'Arial', 10, Left + ColX[2], Top);

  // Règle sous l'en-tête
  Pdf.CreatePath(Left, Top - 5, 260, 0.5, fmNone, clBlack, $FF);
  Pdf.AddPath;

  // Lignes de données, décalant Y vers le bas à chaque itération
  Y := Top;
  for Row := 1 to 3 do
  begin
    Y := Y - RowH;
    Pdf.AddText('Article ' + IntToStr(Row), 'Arial', 9, Left + ColX[0], Y);
    Pdf.AddText(IntToStr(Row * 2), 'Arial', 9, Left + ColX[1], Y);
    Pdf.AddText('$' + IntToStr(Row * 10) + '.00', 'Arial', 9, Left + ColX[2], Y);
  end;
end;

Notez que le Y descend de la hauteur de ligne à chaque passe, encore une fois parce que le haut est positif. C'est également là que l'absence de mesure du texte se fait sentir : rien n'empêche un nom d'élément long de déborder (overrunning) sur la colonne suivante, car la bibliothèque ne sait pas quelle largeur a été rendue pour votre chaîne. Pour une sortie à format fixe où vous contrôlez les données, vous dimensionnez généreusement les colonnes et vous passez à autre chose. Pour un contenu véritablement variable, soit vous contraignez les entrées (inputs), soit vous mesurez vous-même les largeurs de glyphes avant de les placer, c'est le moment où une bibliothèque de composition dédiée commence à s'amortir (pay for itself)

Images et pages multiples

Le contenu matriciel (Raster content) arrive via les assistants d'image. AddPicture prend un TPicture chargé et le place à un point, avec une largeur et une hauteur facultatives pour le mettre à l'échelle ; AddImage accepte un chemin de fichier ou un TBitmap directement, et AddJpegImage diffuse (streams) des octets JPEG sans aller-retour via un bitmap. Comme pour tout le reste, les coordonnées de placement sont le coin inférieur gauche de l'image dans l'espace utilisateur, et la largeur et la hauteur sont la taille sur la page en points, et non les dimensions en pixels de la source

procedure CreateMultiPageReport(const FileName: string; PageCount: Integer);
var
  Pdf: TPdf;
  P: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;
    for P := 1 to PageCount do
    begin
      Pdf.AddPage(P, 595, 842);     // ajouter (append) ; la nouvelle page devient courante
      Pdf.AddText('Page ' + IntToStr(P) + ' sur ' + IntToStr(PageCount),
        'Arial', 10, 50, 30);       // pied de page (footer) près du bord inférieur
      // ... dessiner le corps (body) de cette page ici ...
    end;
    Pdf.SaveAs(FileName);
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Un document multipage est le modèle à page unique (single-page pattern) dans une boucle. Chaque AddPage ajoute une page et la rend courante, de sorte que le corps et le pied de page que vous dessinez ensuite atterrissent sur la page que vous venez d'ajouter. Vous ne réaffectez pas PageNumber à l'intérieur de cette boucle, car l'ajout d'une page y a déjà déplacé le curseur ; vous n'avez besoin de PageNumber que lorsque vous revenez à une page dans le désordre de création. Appelez SaveAs une fois à la fin, une fois la dernière page remplie. Si vous avez besoin d'un profil d'archivage plutôt que d'un fichier standard, le même objet document expose SaveAsPdfA et les autres variantes de conformité, de sorte que le choix du standard de sortie est un appel d'enregistrement différent, pas un chemin de construction différent

Où cela s'intègre (fits)

Le cadrage honnête (honest framing) est que l'API de création du composant PDFium est une couche mince et fidèle (faithful, thin layer) sur le modèle objet de page (page-object model) de PDFium : création de document réelle, polices intégrées réelles, contenu vectoriel et matriciel (raster) réel, sérialisé dans un fichier conforme aux normes. Ce n'est pas, et ne prétend pas être, un moteur de documents à redistribution (reflowing document engine). La ligne de démarcation est la mise en page du texte. Si votre sortie est modélisée (templated), des factures, des certificats, des étiquettes, des tableaux de bord rendus sur une grille fixe, le modèle de coordonnées absolues est direct et rapide et le code reste lisible. Si votre sortie est une prose de forme longue qui doit s'enrouler (wrap) et se paginer d'elle-même, vous allez reconstruire un moteur de mise en page au-dessus de ces appels, et c'est le mauvais outil pour le travail. Savoir de quel côté de cette ligne vous vous trouvez constitue l'essentiel de la décision

Les méthodes de création décrites ici font partie du Composant PDFium pour Delphi, qui associe (pairs) cette voie de création aux fonctionnalités de rendu et d'extraction de texte pour lesquelles PDFium est le plus connu