Article technique

Rapports PDF Delphi avec HotPDF : TextOut, polices et images

Générer un rapport se résume à placer trois choses sur une page et à les faire s'accorder sur leur emplacement : du texte à des coordonnées connues, des polices dont le rendu est le même sur le serveur que sur votre bureau, et des images dimensionnées pour s'adapter. Tout ce qu'une bibliothèque de rapports fait d'autre est organisé autour de ces trois éléments. HotPDF, la bibliothèque de génération de PDF de losLab pour Delphi et C++Builder, vous fournit chacun d'eux sous forme d'appel direct sur l'objet page, et la seule véritable friction est le système de coordonnées sous-jacent, qui fonctionne dans le sens opposé au canevas VCL auquel vous êtes habitué. Réglez d'abord cette orientation et le reste du travail de mise en page cessera de vous combattre

Placement du texte et l'origine en bas à gauche

Le premier rapport de presque tout le monde sort à l'envers. Le titre atterrit près du bord inférieur et chaque ligne en dessous grimpe vers le haut. Il n'y a pas de dysfonctionnement. L'espace utilisateur PDF, défini dans la norme ISO 32000-1 §8.3, place l'origine dans le coin inférieur gauche avec un Y qui augmente vers le haut, ce qui est l'image miroir du canevas GDI où le Y augmente vers le bas à partir du coin supérieur gauche. Cinq minutes passées à faire la paix avec cela sauvent une mise en page que vous réécririez autrement une fois que les chiffres cessent d'avoir un sens

L'appel central de l'objet page est TextOut(X, Y, Angle, Text). X et Y localisent le texte en points à partir du coin inférieur gauche, et l'Angle le fait pivoter en degrés, c'est ainsi qu'un tampon BROUILLON ou COPIE en diagonale est dessiné sans aucun support spécial. L'astuce qui permet à l'intuition formée sur la VCL de continuer à fonctionner est d'exprimer Y comme la hauteur de la page moins la distance que vous souhaitez par rapport au sommet :

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;

Les deux comportements avec état (stateful behaviors) de ce listing sont responsables de la plupart des bogues qui n'apparaissent qu'à la page deux. AddPage repointe CurrentPage sur la page qu'il vient de créer, de sorte qu'une référence de page que vous avez mise en cache plus tôt ne dessine plus là où vous l'attendez. La sélection de la police se fait également par page plutôt que par document. Si vous sautez le SetFont après un AddPage, le premier TextOut sur la nouvelle page revient à la valeur par défaut avec laquelle la page a commencé, et non à la police d'en-tête en gras que vous avez définie il y a trois pages. L'habitude sûre est de traiter "démarrer une nouvelle page" et "rétablir l'état du texte" comme une étape inséparable dans la boucle du rapport

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

La plupart des problèmes de polices sont en réalité des problèmes de déploiement déguisés. La police d'entreprise est installée sur votre machine de développement, le rapport s'affiche donc correctement sur votre écran et il est expédié (ships). L'hôte de production exécute la tâche sous un compte de service sur lequel cette police n'a jamais été installée, le moteur de rendu remplace discrètement par quelque chose qu'il peut trouver, et la première fois qu'on en entend parler, c'est lorsqu'un client demande pourquoi l'en-tête (letterhead) a changé. La solution consiste à cesser de faire confiance au répertoire de polices du système d'exploitation et à charger la police à partir d'un fichier que votre programme d'installation place sur le disque. L'appel d'enregistrement Unicode de HotPDF prend un chemin et fait précisément 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 ✓'));

TextOut accepte directement une WideString, ce qui compte plus qu'il n'y paraît à première vue. Un nom de client avec un accent, une rue allemande, une ville polonaise : ce ne sont pas des cas isolés (edge cases), ce sont les contenus normaux d'une table de clients, et ils passent par le même appel que les étiquettes ASCII que vous codez en dur, à condition que la police enregistrée contienne effectivement les glyphes. Une contrainte de version accompagne les polices intégrées : le document doit être au format PDF 1.5 ou ultérieur, donc si une exigence non liée vous limite à une ancienne version, c'est ce qui se cassera silencieusement. Les écritures de droite à gauche telles que l'arabe et l'hébreu nécessitent un véritable façonnage (shaping) plutôt qu'une simple recherche de glyphes, et cela a son propre pipeline ; consultez notre article sur le façonnage de texte d'écritures complexes avec HotPDF

Lorsqu'aucune police installée ne peut exprimer ce dont vous avez besoin, pensez aux caractères MICR sur un chèque ou à un ensemble de symboles propriétaires, les polices de type 3 comblent cette lacune. Vous définissez chaque glyphe comme un petit flux de contenu via RegisterType3Font et AddType3Glyph. C'est un coin spécialisé de l'API et vous l'utiliserez rarement, mais c'est beaucoup plus propre que de disperser des centaines de minuscules bitmaps de symboles sur une page

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

La manipulation des images se divise en deux étapes, et le but est de les garder séparées. AddImage prend un TBitmap ou un TJPEGImage, l'intègre une fois et renvoie un index. Les illustrations PNG doivent être décodées en bitmap avant d'y arriver. ShowImage dessine ensuite cet index où et aussi souvent que vous le souhaitez. L'ordre des arguments sur ShowImage est le seul endroit où il vaut la peine de ralentir pour lire :

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. Ce ne sont pas les coordonnées du coin opposé, et le dernier argument est un angle de rotation en degrés. Lisez la signature comme une boîte X1/Y1/X2/Y2 et un logo de 120 sur 40 placé à (50, 700) s'étire de là à (120, 40) à la place, s'étalant sur la majeure partie de la page. La sortie rend l'erreur évidente alors que le code source semble tout à fait raisonnable, c'est ce qui fait perdre un après-midi. La valeur par défaut de KeepImageAspectRatio est True, de sorte qu'une boîte avec les mauvaises proportions encadre (letterboxes) l'image au lieu de la déformer ; ne le basculez à False que lorsque vous avez réellement l'intention de l'étirer

La séparation entre l'enregistrement et le placement s'avère payante sur de longs tirages (runs). Étant donné que AddImage intègre les pixels une fois et que chaque ShowImage avec cet index pointe vers le même objet intégré, l'endroit où vous appelez AddImage décide de la taille du fichier. Appelez-le à l'intérieur de la boucle de page pour un relevé (statement) de 500 pages et le même logo est intégré 500 fois. Appelez-le une fois avant la boucle, conservez l'index, et le logo est stocké une seule fois. Un petit dictionnaire indexé par le chemin de la ressource suffit pour s'assurer que chaque image distincte est enregistrée exactement une fois

Le choix du codec est l'autre levier de taille. Le contenu photographique, les pièces jointes numérisées, etc., ont leur place dans JPEG : passez icJpeg à AddImage et abaissez JpegQuality à environ 85, car la propriété commence à 100 et la différence à 85 est invisible sur une page imprimée. Les illustrations aux couleurs unies telles que les logos, les graphiques et les dessins au trait (line drawings) ont leur place dans icFlate, où la compression sans perte est déjà compacte et où le format JPEG étalerait un effet de halo (ringing) visible autour des bords durs. Une exécution de relevé qui pousse une photo en pleine qualité sur chaque page peut gonfler pour atteindre des gigaoctets ; le même contenu au format JPEG 85 occupe environ un dixième de la taille, et aucun lecteur ne s'en aperçoit

Règles, boîtes et ombrages avec des primitives de chemin (path primitives)

La ligne horizontale sous un en-tête de tableau et la boîte grise derrière un chiffre de totaux n'ont pas besoin d'être des images. Dessinez-les sous forme de vecteurs et ils resteront nets à n'importe quel zoom, s'imprimeront avec netteté et n'ajouteront presque rien au fichier. HotPDF suit le même modèle que celui utilisé par les flux de contenu PDF bruts : construisez un chemin (path), puis appelez un opérateur qui le peint

// 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;

L'ordre n'est pas facultatif : définissez l'état de la peinture (paint state), construisez le chemin, puis appelez Stroke ou Fill. Un chemin que vous construisez mais que vous ne peignez jamais n'apporte rien à la page, ce qui est presque toujours la réponse lorsqu'une règle (line) "n'apparaît pas". SetRGBFillColor prend un seul TColor, de sorte que les constantes VCL familières telles que clNavy et clBlack s'intègrent directement, et Rectangle utilise les mêmes arguments de largeur et de hauteur que le placement d'images plutôt que deux coins. Une mise en garde concernant les lignes fines : tout ce qui est en dessous d'environ un demi-point peut paraître élégant sur un moniteur puis disparaître sur une imprimante de bureau de 600 dpi, donc 0,75pt est un plancher raisonnable pour toute règle qui doit survivre à l'impression

Pagination avec des données réelles, et non des échantillons de données

Un détail à régler avant la mise en page : les colonnes numériques doivent être alignées sur leur bord droit, et la façon d'y parvenir est de mesurer la largeur de rendu de chaque valeur et de la positionner en retrait de la limite de la colonne, et non de remplir la chaîne avec des espaces de début (leading spaces). Le remplissage par des espaces (space padding) ne s'aligne que dans une police à espacement fixe (monospaced), et personne ne compose un rapport financier avec une police à espacement fixe. Faites d'abord passer les valeurs dans les routines sensibles aux paramètres régionaux (locale-aware) de Delphi, telles que FormatFloat, afin que le séparateur des milliers dont vous mesurez la largeur soit le même que celui qui sera réellement affiché par les paramètres régionaux du client

Le danger avec la pagination est que vous l'écriviez sur l'ensemble de données de démonstration, où dix courtes lignes tiennent sur une seule page et où la boucle n'a jamais besoin de se rompre. La production vous remet un client dont le nom de l'entreprise fait 140 caractères et un relevé de 4 000 lignes d'articles, et maintenant la boucle doit se rompre correctement à chaque fois. Le modèle (pattern) qui tient la route est un curseur Y unique qui se déplace vers le bas à mesure que vous soustrayez la hauteur de chaque ligne, et un test (check) qui démarre une nouvelle page au moment où le curseur franchirait la marge inférieure. Vers le bas signifie ici que le Y diminue, ce qui est le seul endroit où l'origine en bas à gauche reste contre-intuitive. Gardez tout cela dans une seule routine qui réédite également SetFont et redessine l'en-tête courant (running header) sur la nouvelle page, et les bogues de décalage d'une page ne s'implanteront jamais. Lorsque les mêmes rapports doivent également respecter des règles d'archivage ou d'accessibilité, les choix que vous faites ici, les polices que vous intégrez, si la sortie est balisée, quels espaces colorimétriques vous utilisez, sont ceux que ces normes contrôlent (police) ; le guide HotPDF PDF/A, PDF/X et PDF/UA vaut la peine d'être lu avant que le modèle ne se durcisse

Chaque appel présenté ici, le positionnement du texte, l'enregistrement des polices, l'intégration des images et le dessin des chemins, est fourni dans le Composant HotPDF pour Delphi et C++Builder, dont la référence documente l'intégralité de l'API de sortie ainsi que les fonctionnalités de formulaires, de chiffrement et de signature qu'elle accompagne