Article technique

HotPDF Hello World en Delphi : votre premier PDF

Une première ligne écrite du point de vue de la spécification atterrit en y = 770. Le raisonnement est solide sur le papier : ISO 32000-1 place l'origine de l'espace utilisateur dans le coin inférieur gauche, une page A4 fait 842 points de haut, donc 770 tombe à un pouce sous le bord supérieur. Exécutez ce programme contre un composant qui mesure depuis le haut et le message atterrit à un pouce au-dessus du bord inférieur, le reste de la page vide au-dessus. Ajoutez une seconde page, et la police soigneusement choisie redevient de l'Arial 12 points. Les deux surprises viennent des mêmes deux décisions que tout programme PDF prend sur sa première page : quelle police est courante, et depuis quel coin on compte les nombres

Diagramme du flux du programme Hello World HotPDF en quatre appels en Delphi, avec l'origine de page en haut à gauche et deux pièges de configuration en évidence
Create, begin, draw, end constitue toute la colonne vertébrale, et l'origine en haut à gauche explique pourquoi les lignes suivantes utilisent des Y plus grands

Le plus petit programme HotPDF en Delphi

Le Hello World HotPDF est un programme console de quatre appels dans un ordre fixe : définir les propriétés du document, BeginDoc, dessiner avec SetFont et TextOut, puis EndDoc. Tout le reste est du Delphi ordinaire. Le voici, écrivant une page qui salue le lecteur en onze langues :

program HelloWorld;

{$APPTYPE CONSOLE}

uses
  Winapi.Windows,   // DEFAULT_CHARSET ; sur un Delphi antérieur à XE2, utilisez Windows simple
  HPDFDoc;          // unité principale de HotPDF

procedure CreateHelloWorld(const FileName: string);
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Réglages globaux du document : tous à passer avant BeginDoc
    Pdf.FileName := FileName;
    Pdf.AutoLaunch := False;            // défaut True : ouvre le visualiseur après EndDoc
    Pdf.Compression := cmFlateDecode;   // défaut cmNone
    Pdf.FontEmbedding := True;          // déjà le défaut, précisé pour le lecteur
    Pdf.EnableFontSubsetting := True;   // n'embarque que les glyphes réellement dessinés
    Pdf.BeginDoc;                       // crée la page 1 (A4)

    // Le 4e argument vaut ANSI_CHARSET par défaut ; passez DEFAULT_CHARSET explicitement
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 14, DEFAULT_CHARSET);

    // X, Y en points depuis le coin SUPÉRIEUR GAUCHE ; Y est le bord supérieur de la ligne
    Pdf.CurrentPage.TextOut(72,  72, 0, 'Hello, Delphi PDF world!');      // anglais
    Pdf.CurrentPage.TextOut(72, 102, 0, 'Hola, mundo Delphi PDF!');       // espagnol
    Pdf.CurrentPage.TextOut(72, 132, 0, 'Hallo, Delphi PDF Welt!');       // allemand
    Pdf.CurrentPage.TextOut(72, 162, 0, 'Bonjour, monde PDF Delphi!');    // français
    Pdf.CurrentPage.TextOut(72, 192, 0, 'Ciao, mondo Delphi PDF!');       // italien
    Pdf.CurrentPage.TextOut(72, 222, 0, 'Olá, mundo Delphi PDF!');        // portugais
    Pdf.CurrentPage.TextOut(72, 252, 0, 'Здравствуйте, Delphi PDF мир!'); // russe
    Pdf.CurrentPage.TextOut(72, 282, 0, 'こんにちは、Delphi PDFの世界!');    // japonais
    Pdf.CurrentPage.TextOut(72, 312, 0, 'Merhaba, Delphi PDF dünyası!');  // turc
    Pdf.CurrentPage.TextOut(72, 342, 0, '你好,Delphi PDF世界');           // chinois
    Pdf.CurrentPage.TextOut(72, 372, 0, '여보세요, Delphi PDF 세계!');      // coréen

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

begin
  CreateHelloWorld('HelloWorld.pdf');
  Writeln('Wrote HelloWorld.pdf');
end.

PDF multilingue Hello World produit par le composant HotPDF Delphi dans Delphi

THotPDF.Create(nil) couplé à try/finally ... Free est le pattern de possession Delphi habituel, et produire un PDF ne le change pas. Les lignes défilent vers le bas à partir de Y = 72, espacées de 30 points, ce qui laisse un interligne confortable pour du texte 14 points. Exécutez-le et HelloWorld.pdf apparaît dans le répertoire courant : une page, contenu compressé, police embarquée en sous-ensemble

Les propriétés du document se passent avant BeginDoc

BeginDoc démarre le document et crée sa première page, donc tout ce qui façonne le fichier entier doit être affecté avant. HotPDF l'impose pour certaines propriétés au lieu de le laisser à la convention. Affectez Compression après BeginDoc et le setter lève une exception avec le message Cannot set Compression value - document in progress. ; AutoLaunch est gardé de la même façon. Bruyant, c'est bien ici. Mettez tous les réglages du document dans un bloc au-dessus de BeginDoc et vous ne verrez jamais ce message

Deux des défauts méritent un second regard. Compression démarre à cmNone, donc un programme qui n'y touche jamais écrit des flux de contenu non compressés ; pour une page de texte c'est seulement du gâchis, pour un rapport de centaines de pages c'est la première chose que tout le monde remarque à la taille du fichier. AutoLaunch démarre à True, ce qui signifie qu'un outil console ouvre le PDF fini dans le visualiseur associé dès que EndDoc rend la main. Agréable au bureau, faux dans un service, un job planifié ou un agent de build. Pour ces environnements, mettez-le à False dans le code ; pour les exécutions CI où le code ne se change pas, la variable d'environnement HOTPDF_SUPPRESS_AUTO_LAUNCH à 1 coupe aussi le lancement

La gestion des polices, c'est là que nous prenons parti. FontEmbedding vaut True par défaut, donc la fonte voyage avec le fichier et se rend sur une machine qui n'a jamais eu Arial Unicode MS d'installé. EnableFontSubsetting, en revanche, est désactivé par défaut pour garder les polices complètes disponibles aux workflows d'édition. Pour un message de salutation, une facture ou un rapport générés, activez le sous-ensemblage. Arial Unicode MS couvre un énorme éventail d'écritures, et embarquer la fonte entière pour afficher onze lignes courtes gonflerait le fichier de plusieurs mégaoctets sans bénéfice visible. Ne le laissez désactivé que si quelqu'un en aval éditera le texte et a besoin de glyphes que vous n'avez pas dessinés

Où se trouve (0, 0) sur une page HotPDF ?

HotPDF mesure les coordonnées de page depuis le coin supérieur gauche avec Y croissant vers le bas, le même repère qu'un TCanvas VCL. À la Resolution par défaut de 72, une unité vaut un point, 1/72 de pouce. Le fichier PDF lui-même stocke toujours des coordonnées ascendantes, comme l'exige ISO 32000-1 §8.3.2.3 ; HotPDF convertit chaque Y que vous passez en le soustrayant de la hauteur de page quand il écrit le flux de contenu

L'arithmétique de l'exemple d'ouverture est courte. La page par défaut que créent BeginDoc et AddPage est A4, 595 sur 842 points. Une ligne censée siéger à un pouce du haut vaut Y = 72 en termes HotPDF et y = 770 en PDF brut, car 842 − 72 = 770. Passez 770 à TextOut et le texte descend de 770 points vers le bas depuis le haut, soit 72 points au-dessus du bord inférieur

Il y a un second piège, plus discret, dans le même appel. Par défaut le Y que vous passez est le bord supérieur du texte, et HotPDF descend de la hauteur du texte avant de poser la ligne de base. Le code porté depuis GDI attend habituellement exactement cela. Le code porté depuis une autre bibliothèque PDF, ou depuis votre propre écrivain de flux de contenu, attend habituellement que Y soit la ligne de base, et chaque ligne ressort une hauteur de texte plus bas que prévu. THPDFPage.TextFromBaseLine(True) bascule la page courante en positionnement par ligne de base :

// Pont pour des coordonnées venant d'une source ascendante (une mise en page PDF,
// un diagramme de spec, une autre bibliothèque). Helper écrit pour cet article,
// pas une partie de HotPDF. Valide à la Resolution par défaut de 72
function FromBottomUp(Page: THPDFPage; PdfY: Single): Single;
begin
  Result := Page.Height - PdfY;
end;

procedure DrawFromPdfCoordinates(Pdf: THotPDF);
begin
  Pdf.CurrentPage.SetFont('Arial', [], 12);
  // Y nomme désormais la ligne de base, comme les opérateurs de texte du PDF placent les glyphes
  Pdf.CurrentPage.TextFromBaseLine(True);
  // Ligne de base 770 points au-dessus du bord inférieur, soit Y = 72 depuis le haut
  Pdf.CurrentPage.TextOut(72, FromBottomUp(Pdf.CurrentPage, 770), 0,
    'Baseline one inch below the top edge');
end;

Resolution est l'autre réglage qui change ce que signifient les nombres. Mettez THotPDF.Resolution à 96 et les coordonnées X/Y, les marges et les résultats de TextWidth s'expriment en 1/96 de pouce, pratique quand la mise en page a été conçue en pixels d'écran. Width et Height de page continuent de rapporter des points, pourtant, donc un code de mise en page qui mélange bornes de page et coordonnées de dessin devrait lire CurrentPage.UserWidth et CurrentPage.UserHeight, qui reviennent dans les mêmes unités que X et Y. À 72 les deux paires concordent. Si vous préférez penser en millimètres, la propriété MeasurementUnit (muPoints par défaut, avec muMM, muInches, muCentimeters, muTwips et muPixels disponibles) est le commutateur plus propre. Choisissez une convention par programme. Ne les empilez jamais

Pourquoi la page deux imprime en Arial 12 points ?

Parce que chaque nouvelle page démarre d'un état connu. THotPDF.AddPage rend la nouvelle page courante, la dimensionne en A4 et y appelle SetFont('Arial', [], 12), en gardant la taille à 12 points quelle que soit la Resolution. BeginDoc crée la page une par le même chemin, voilà pourquoi le Hello World définit sa police juste après BeginDoc et pas avant. La conséquence pour une sortie multi-pages se manque facilement : le SetFont appelé sur la page une ne se transmet pas. Les lignes latines de la page deux restent présentables en Arial, juste plus petites. Les lignes CJK trahissent le problème, car Arial n'a pas leurs glyphes et le lecteur affiche des cadres vides ou rien du tout

Une police de départ prévisible est le bon design pour un objet page : le code de dessin de la page cinq ne dépend pas en silence de ce que la page quatre a laissé derrière elle. Le coût, c'est que la configuration de page doit être répétée, et la façon la plus propre de la répéter est une procédure unique que chaque page traverse, la première comprise :

uses
  Winapi.Windows, Vcl.Graphics, HPDFDoc;

// Votre propre helper : un seul endroit qui sait ce que réclame une page neuve
procedure PreparePage(Pdf: THotPDF; IsFirst: Boolean);
begin
  if not IsFirst then
    Pdf.AddPage;                       // remet la police à Arial 12
  Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 14, DEFAULT_CHARSET);
end;

procedure WriteGreetings(Pdf: THotPDF; Lines: array of string);
const
  TopMargin = 72;
  Leading   = 30;
  LinesPerPage = 20;
var
  I, Row: Integer;
begin
  PreparePage(Pdf, True);              // la page 1 existe déjà après BeginDoc
  Row := 0;
  for I := 0 to High(Lines) do
  begin
    if Row = LinesPerPage then
    begin
      PreparePage(Pdf, False);
      Row := 0;
    end;
    Pdf.CurrentPage.TextOut(72, TopMargin + Row * Leading, 0, Lines[I]);
    Inc(Row);
  end;

  // Un titre en gras réclame son propre SetFont, puis la police du corps revient
  Pdf.AddPage;
  Pdf.CurrentPage.SetFont('Arial Unicode MS', [fsBold], 18, DEFAULT_CHARSET);
  Pdf.CurrentPage.TextOut(72, 72, 0, 'Appendix');
  Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 14, DEFAULT_CHARSET);
  Pdf.CurrentPage.TextOut(72, 110, 0, 'Body text after the heading');
end;

Si vos pages basculent aussi en positionnement par ligne de base avec TextFromBaseLine, mettez cet appel dans PreparePage aussi, à côté de la police. Garder chaque réglage par page dans une seule procédure fait qu'une nouvelle exigence, une couleur ou une taille différente, s'ajoute une fois et atteint toutes les pages

Une police pour plusieurs écritures, et où elle s'arrête

Un seul SetFont peut porter latin, cyrillique, japonais et chinois sur une même page, car Arial Unicode MS regroupe les glyphes d'un très large éventail d'écritures dans une seule fonte. Le quatrième argument de SetFont est le jeu de caractères Windows, et son défaut est ANSI_CHARSET, qui attache la sélection de police à la page de code occidentale. Passer DEFAULT_CHARSET (valeur 1) laisse Windows choisir la fonte par son nom, ce que veut une page multilingue

La police doit exister sur la machine qui construit le PDF. HotPDF embarque la fonte que Windows résout pour ce nom, et quand Arial Unicode MS manque, Windows substitue une autre fonte sans broncher. La sortie embarque alors la substitut, et les lignes dans des écritures qu'elle ne couvre pas ressortent en cadres vides. Un serveur de build est l'endroit habituel où cela se manifeste, car personne n'y a installé les polices Office. Vérifiez la liste des polices embarquées du premier fichier produit par une machine neuve avant de lui confier un traitement par lot

Quand aucune fonte ne couvre chaque chaîne, TextOutWithFallback prend les mêmes arguments que TextOut, résout une police par caractère, dessine chaque segment avec la fonte qui sait le rendre et restaure votre police d'origine ensuite. Les écritures de droite à gauche comme l'arabe et l'hébreu demandent plus qu'une couverture de glyphes : elles exigent du façonnage et un ordre de segments inversé, c'est le territoire de la fonction RtLTextOut, habituellement associée à un fichier TrueType chargé via RegisterUnicodeTTF

À partir de là, les étapes naturelles suivantes sont la taille, le style et la rotation du texte, et le placement délibéré de plusieurs blocs sur une page, ce que parcourt l'exemple TextOut. Filets, cadres et images utilisent le même objet CurrentPage et les mêmes coordonnées haut-gauche, donc tout ce qui précède sur les origines et la configuration par page s'applique à eux sans changement

La page du HotPDF Delphi Component couvre le reste de l'API de dessin pour Delphi et C++Builder, des formes et images aux formulaires, une fois ce Hello World exécuté proprement avec vos propres polices