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