Article technique

Rapports PDF Delphi avec HotPDF : TextOut, polices et images

La première facture que presque toute équipe rend avec une bibliothèque PDF sort mal de la même façon : le texte d'en-tête se place le long du bord inférieur de la page, et chaque ligne suivante monte. Rien n'est cassé. L'espace utilisateur PDF, selon ISO 32000-1 §8.3, place l'origine dans l'angle inférieur gauche avec Y croissant vers le haut, exactement l'inverse du canevas GDI sur lequel un développeur VCL dessine depuis des années. HotPDF, bibliothèque de génération PDF de losLab pour Delphi et C++Builder, expose directement ce modèle de coordonnées ; les cinq minutes passées à l'intérioriser maintenant évitent une réécriture de mise en page plus tard. Cet article parcourt les primitives de sortie dont un générateur de rapports a réellement besoin : texte positionné, polices qui survivent au déploiement, placement d'images et dessin vectoriel.

Placement du texte et origine en bas à gauche

L'appel central de l'objet page est TextOut(X, Y, Angle, Text). X et Y positionnent le texte en points depuis le coin inférieur gauche, et Angle le fait pivoter en degrés, ce qui permet les tampons diagonaux DRAFT et COPY sans mécanisme supplémentaire. L'idiome qui garde l'intuition VCL utilisable consiste à calculer Y comme la hauteur de page moins la distance depuis le haut :

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice-0001.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(50, 792 - 50, 0, 'INVOICE');       // 50pt from top of Letter
    Pdf.CurrentPage.SetFont('Arial', [], 10);
    Pdf.CurrentPage.TextOut(50, 792 - 70, 0, 'Date: 2026-06-11');
    Pdf.CurrentPage.TextOut(300, 400, 45, 'COPY');              // rotated stamp
    Pdf.AddPage;                                                // CurrentPage now points here
    Pdf.CurrentPage.SetFont('Arial', [], 10);                   // font state does not carry over
    Pdf.CurrentPage.TextOut(50, 742, 0, 'Page 2 detail rows');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Deux comportements à état dans ce listing causent la plupart des bugs de page deux. AddPage repointe CurrentPage vers la nouvelle page ; toute référence conservée vers l'objet page précédent est donc obsolète pour le dessin. Et le choix de police est par page : appelez SetFont à nouveau après chaque AddPage, sinon le premier TextOut de la nouvelle page n'utilisera pas la police que vous croyez. Une boucle de rapport doit traiter « nouvelle page » et « rétablir l'état texte » comme une seule unité.

Des polices qui existent sur le serveur, pas seulement sur votre poste

Les échecs de polices sont des échecs de déploiement. La machine de développement possède la police corporate ; le compte de service Windows sur l'hôte de production ne l'a pas, et la sortie substitue silencieusement. Le modèle défensif consiste à charger les polices depuis des fichiers contrôlés par votre installateur plutôt qu'à faire confiance au dossier de polices de l'OS, et l'appel d'enregistrement Unicode de HotPDF fait exactement cela :

Pdf.RegisterUnicodeTTF('C:\ProgramData\MyApp\Fonts\NotoSans.ttf');
Pdf.CurrentPage.SetFont('NotoSans', [], 12);
Pdf.CurrentPage.TextOut(50, 700, 0, WideString('Łódź — Ünïcode test ✓'));

Notez que TextOut prend directement un WideString ; les données client contenant autre chose que la page de code locale, ce qui en pratique signifie toutes les données client, passent donc par le même appel que les éléments ASCII du rapport, à condition que la police choisie couvre les glyphes. Les polices Unicode intégrées exigent aussi que le document soit en PDF 1.5 ou ultérieur ; gardez ce plancher de version en tête si une autre exigence vous bloque sur une version plus ancienne. Pour les écritures qui demandent du shaping et pas seulement une recherche de glyphe, notamment arabe et hébreu, le pipeline droite-gauche dédié est couvert dans notre article sur le shaping des écritures complexes avec HotPDF.

Pour le cas rare où aucun fichier de police ne peut représenter ce dont vous avez besoin, marques de type MICR ou symboles propriétaires, HotPDF prend en charge les polices Type 3 via RegisterType3Font et AddType3Glyph, chaque glyphe étant un petit flux de contenu que vous définissez. C'est un outil de niche, mais il vaut mieux que livrer des symboles sous forme de centaines de minuscules images.

Images : les arguments du milieu sont largeur et hauteur, pas un coin

HotPDF sépare l'enregistrement d'image de son placement. AddImage ingère un TBitmap ou un TJPEGImage une fois, décodez d'abord les PNG en bitmap, et retourne un index ; ShowImage place cet index autant de fois que nécessaire. La signature est la partie à relire deux fois :

var
  Png: TPngImage;
  Logo: TBitmap;
  LogoIdx: Integer;
begin
  Png := TPngImage.Create;
  Logo := TBitmap.Create;
  try
    Png.LoadFromFile('brand-logo.png');
    Logo.Assign(Png);                       // decode PNG to a bitmap
    LogoIdx := Pdf.AddImage(Logo, icFlate); // lossless for flat-color art
  finally
    Logo.Free;
    Png.Free;
  end;
  // (Index, X, Y, Width, Height, Angle) — not (X1, Y1, X2, Y2)
  Pdf.CurrentPage.ShowImage(LogoIdx, 50, 700, 120, 40, 0);
end;

Les deux nombres après la position sont une largeur et une hauteur, pas un coin opposé, et le dernier argument est un angle de rotation. Du code écrit avec l'hypothèse X1/Y1/X2/Y2 produit des logos étirés sur presque toute la page, bug évident dans la sortie et déroutant dans la source. Corrélat : KeepImageAspectRatio vaut True par défaut ; une boîte mal proportionnée ajoute donc des bandes plutôt que de déformer. Ne le mettez à False que si l'étirement est réellement voulu.

La séparation enregistrement-placement compte aussi pour les performances et la taille : AddImage intègre les données bitmap une seule fois, et chaque ShowImage avec le même index réutilise cet objet intégré. Une série de 500 relevés qui appelle AddImage à chaque page pour le même logo intègre le logo 500 fois ; la même série qui enregistre une fois et réutilise l'index l'intègre une fois. Mettez les indices en cache dans un petit dictionnaire indexé par chemin d'actif et le problème n'apparaît jamais.

La taille de fichier vit ici aussi. Le contenu photographique doit passer par l'encodage JPEG, en passant icJpeg à AddImage et en réglant JpegQuality autour de 85, puisque la propriété vaut 100 par défaut ; c'est visuellement propre pour les pièces jointes scannées et les photos, à une fraction de la taille lossless. Gardez PNG pour les visuels à aplats comme logos et graphiques, où les artefacts JPEG se voient et où la compression Flate est déjà efficace. Une série de relevés qui intègre une photo par page avec de mauvais réglages expédie des gigaoctets ; la même série en JPEG 85 expédie un dixième de cela sans plainte visuelle.

Règles, boîtes et aplats avec primitives de chemin

Les filets de tableau et boîtes de totaux n'ont besoin d'aucune image ; les primitives vectorielles produisent une sortie plus nette à tout zoom et coûtent presque rien en taille de fichier. Le modèle est construction de chemin puis opérateur de peinture :

// Horizontal rule under the table header
Pdf.CurrentPage.SetLineWidth(0.75);
Pdf.CurrentPage.MoveTo(50, 660);
Pdf.CurrentPage.LineTo(545, 660);
Pdf.CurrentPage.Stroke;

// Shaded totals box: X, Y, width, height
Pdf.CurrentPage.SetRGBFillColor(RGB(235, 235, 235));
Pdf.CurrentPage.Rectangle(395, 120, 150, 40);
Pdf.CurrentPage.Fill;

La discipline d'ordre est la même que dans les flux de contenu PDF bruts : définir l'état de peinture, construire le chemin, puis appeler Stroke ou Fill. Un chemin qui n'est jamais peint disparaît simplement, explication habituelle quand une règle « ne s'affiche pas ». SetRGBFillColor prend un seul TColor ; les constantes VCL, clNavy, clBlack, fonctionnent donc directement, et Rectangle suit la même convention largeur-hauteur que le placement d'image. Les hairlines demandent une précaution : les épaisseurs sous environ un demi-point sont élégantes à l'écran et peuvent disparaître entièrement sur une imprimante de bureau 600 dpi ; 0,75 pt est un plancher raisonnable pour des filets de tableau qui doivent survivre au papier.

Pagination sur données réelles, pas sur données d'exemple

Les colonnes numériques révèlent une autre habitude à prendre tôt : alignez les montants sur leur bord droit en calculant la position X depuis la limite droite de la colonne et la largeur rendue de chaque valeur, plutôt qu'en bourrant les chaînes d'espaces. Les espaces ne s'alignent qu'en polices monospace, et les rapports financiers ne sont jamais composés en monospace. Formatez les valeurs avec les routines Delphi sensibles à la locale, comme FormatFloat, avant de mesurer, afin que le séparateur de milliers attendu par le client soit celui dont vous mesurez la largeur.

Le jeu de démonstration a dix lignes courtes ; la production a un client dont le nom de société fait 140 caractères et un relevé avec 4 000 lignes. Une boucle de rapport robuste suit un curseur Y vers le bas, soustrait la hauteur de chaque ligne, et saute à une nouvelle page quand le curseur franchirait la marge basse, en se rappelant que « vers le bas » signifie Y décroissant dans ce système de coordonnées. Placez la gestion du saut de page à un seul endroit, réémettez SetFont et redessinez l'en-tête courant dedans, et les bugs de page en trop disparaissent. Si vos rapports doivent aussi satisfaire des exigences d'archivage ou d'accessibilité, les choix de génération effectués ici, polices intégrées, sortie balisée, espaces couleur, sont précisément ce que les standards contraignent ; voyez le guide HotPDF PDF/A, PDF/X et PDF/UA avant que le modèle ne se fige.

FAQ

Pourquoi mon texte se rend-il en bas de page ?

L'origine PDF est en bas à gauche avec Y croissant vers le haut. Convertissez les positions relatives au haut avec PageHeight - Offset, ou concevez votre code de mise en page autour de l'origine inférieure gauche dès le départ.

Pourquoi la police est-elle fausse sur la page 2 mais correcte sur la page 1 ?

Le choix de police ne traverse pas les pages, et AddPage bascule CurrentPage vers la nouvelle page. Appelez SetFont après chaque AddPage avant le premier TextOut.

Comment garder une taille de fichier raisonnable avec beaucoup de photos intégrées ?

Passez icJpeg à AddImage et réglez JpegQuality près de 85 pour le contenu photographique ; réservez icFlate lossless aux logos en aplats et au line art. Enregistrez chaque image distincte une seule fois avec AddImage et réutilisez l'index.

Référence produit

Chaque appel de cet article est livré avec le HotPDF Component pour Delphi et C++Builder, qui documente toute l'API texte, polices, images et dessin avec les fonctions de formulaires, chiffrement et signature couvertes ailleurs sur ce blog.