Article technique

Modèles de blocs PDF en Delphi : texte, image et PDF

PDFlibPas (losLab PDF Library) remplit des modèles de blocs PDF depuis Delphi : un bloc est un rectangle nommé sur une page de modèle, typé texte, image ou PDF, et FillTextBlock, FillImageBlock et FillPDFBlock y dessinent des données variables. Les blocs se présentent comme des annotations de page PDFlibBlock:Text, PDFlibBlock:Image et PDFlibBlock:PDF, ou dans un dictionnaire page-piece sous /PieceInfo

Le cas dusage est du genre ennuyeux et à gros volumes. Un graphiste met en page un certificat, un billet dévénement ou une lettre de relance une fois, et le programme tamponne un nom, un numéro de siège, une image de signature ou une page de code-barres pré-rendue dans des emplacements fixes pour dix mille enregistrements. Les champs de formulaire savent en faire une partie, mais un champ AcroForm est un objet interactif avec son propre flux dapparence et ses propres règles, et vous finissez de toute façon généralement par laplatir. Un bloc est plus simple : un rectangle plus une poignée dindications de style, consommé une fois puis jeté

Quest-ce quun bloc PDF dans PDFlibPas, exactement ?

Un bloc PDFlibPas est une annotation de page dont le /Subtype vaut PDFlibBlock:Text, PDFlibBlock:Image ou PDFlibBlock:PDF, dont le /Rect est la zone réservée, et dont la chaîne /Contents contient name;key=value;key=value. Le premier segment est le nom du bloc, tout ce qui suit est une liste de propriétés. TPDFlib.AddBlock écrit lannotation avec /F 4 (Print) et sans flux dapparence, si bien quun bloc non rempli noccupe aucun pixel à lécran ni sur papier

Anatomie dun modèle de blocs PDFlibPas pour les données variables Delphi : une annotation de page avec /Subtype PDFlibBlock:Text, Image ou PDF, un /Rect réservé, une chaîne /Contents de nom et propriétés key=value, /F 4 Print et aucun flux dapparence, avec le dictionnaire page-piece /PieceInfo /PDFlib /Blocks comme seconde source
Les blocs sont énumérés à partir de 1 par page, annotations dabord et blocs page-piece ensuite ; les entrées de tableau deviennent Block1 et Block2, les clés /Custom sont aplaties en propriétés, et tout ce qui nest pas Image, PDF ou Page est traité comme texte

La lecture a une seconde source. Après lénumération des annotations de blocs, PDFlibPas se rabat sur le chemin de dictionnaire de page /PieceInfo << /PDFlib << /Blocks ... >> >> et sa variante /PieceInfo << /PDFlib << /Private << /Blocks ... >> >> >>. Les dictionnaires page-piece sont du PDF standard, définis dans ISO 32000-1 §14.5 comme lendroit où une application stocke des données privées attachées à une page, et cest là que vivent dordinaire les définitions de blocs des outils externes dauteur de modèles. Le conteneur /Blocks peut être un dictionnaire indexé par nom de bloc ou un simple tableau ; pour la forme tableau, PDFlibPas nomme les entrées Block1, Block2 et ainsi de suite sauf si un bloc porte son propre /Name

Dans un bloc page-piece, la valeur /Subtype (ou /Type) Image correspond à un bloc image, PDF ou Page à un bloc PDF, et tout le reste est traité comme texte. Toute autre clé excepté /Type, /Subtype, /Name, /Rect et /Custom devient une propriété, et les clés dun dictionnaire /Custom imbriqué sont aplaties également, si bien quune entrée /Custom << /department (Finance) >> se lit via GetBlockProperty(Page, Index, 'department'). Les indices de blocs partent de 1 par page, blocs dannotations dabord, blocs page-piece ensuite

Écrire un modèle de blocs avec AddBlock

TPDFlib.AddBlock(BlockType, BlockName, Left, Top, Width, Height, Properties) ajoute un bloc à la page actuellement sélectionnée et renvoie 1 en cas de succès. Il renvoie 0 pour un nom vide, une largeur ou hauteur nulle ou négative, ou un BlockType autre que 'text', 'image' ou 'pdf' (insensible à la casse). Properties est ajouté au nom avec un point-virgule, si bien quil emploie la même syntaxe key=value;key=value que la chaîne stockée

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    // graphisme statique : bordure, logo, libellés fixes ...
    Lib.DrawText(72, 200, 'This certifies that');

    Lib.AddBlock('text', 'recipient', 72, 230, 450, 44,
      'fontname=Times-Bold;fontsize=30;fillcolor={cmyk 0 0.55 1 0.1};alignment=center');
    Lib.AddBlock('text', 'course', 72, 290, 450, 24,
      'fontname=Helvetica;fontsize=14;alignment=center;fitmethod=word');
    Lib.AddBlock('image', 'signature', 360, 600, 160, 60, '');
    Lib.AddBlock('pdf', 'seal', 72, 580, 100, 100, '');

    if Lib.GetBlockCount(1) <> 4 then
      raise Exception.Create('Template blocks missing');
    Lib.SaveToFile('certificate-template.pdf');
  finally
    Lib.Free;
  end;
end;

Lanalyseur de propriétés est délibérément souple. Les clés se comparent sans tenir compte de la casse, les valeurs sont élaguées mais sinon renvoyées verbatim (une valeur écrite {top left} revient avec ses accolades), et une clé absente renvoie une chaîne vide. La seule limite dure est le séparateur : une valeur ne peut pas contenir de point-virgule, parce que le point-virgule termine le segment

Comment FillTextBlock applique-t-il police, couleur et alignement ?

TPDFlib.FillTextBlock(Page, BlockIndex, Text, Options) lit les propriétés du bloc, les applique à létat de dessin, fait tenir le texte dans le rectangle du bloc avec FitTextBox, puis restaure la police, la taille de texte, la couleur de texte et lalignement de lappelant dans un bloc finally. Cette restauration compte dans une boucle de fusion : un bloc de titre rouge de 30 pt ne peut pas laisser fuir son style dans le prochain appel DrawText que vous faites sur la même page

  • fontname (ou font) : réutilise dabord une police déjà chargée dans le document sous ce nom, puis mappe les noms Standard 14 tels que Helvetica-Bold, Times-Roman ou Courier, et tente enfin AddTrueTypeFont avec le nom
  • fontsize (ou size) : la taille de texte passée à lajusteur, qui la réduit si le texte ne tient pas
  • fillcolor (ou textcolor, color) : #RRGGBB, {rgb r g b}, {cmyk c m y k}, {gray g}, listes brutes de 3 ou 4 nombres, ou les noms black, white, red, green, blue et gray ; les composantes RGB supérieures à 1 se lisent comme 0–255
  • alignment, align et position se lisent ensemble : left, right, center (ou centre) et justify fixent lalignement horizontal, top et bottom fixent le placement vertical, et le défaut est centré verticalement
  • fitmethod : word ou nofitsplit empêche de couper les mots longs, scale ou expand laisse la taille grandir pour remplir la boîte

Le chemin CMYK est la raison pour laquelle les imprimeurs sy intéressent. {cmyk 0 0.55 1 0.1} appelle SetTextColorCMYK, si bien que le texte variable est émis en DeviceCMYK et atteint la couleur de marque sur presse sans aller-retour RGB. FillTextBlock renvoie 1 quand le texte a été dessiné ; il renvoie 0 pour un Text vide, un bloc absent ou un bloc qui nest pas un bloc texte, et dans tous ces cas le bloc reste en place

Ordre de traitement de FillTextBlock dans PDFlibPas pour Delphi : les propriétés du bloc fontname, fontsize, fillcolor et alignment sont appliquées à létat de dessin, la valeur CMYK passe par SetTextColorCMYK, le texte est ajusté avec FitTextBox, et la police, taille, couleur et alignement de lappelant reviennent dans un bloc finally
Le style du bloc est appliqué une fois puis annulé, si bien quun titre rouge de 30 pt ne peut pas fuir dans le prochain DrawText ; lappel renvoie 1 quand le texte a été dessiné et 0 avec le bloc laissé en place pour un texte vide ou un bloc non texte

Remplir les blocs image et PDF, et les bits Options

FillImageBlock(Page, BlockIndex, ImageID, Options) dessine une image ajoutée précédemment, et FillPDFBlock(Page, BlockIndex, SourceDocumentID, SourcePage, Options) importe une page dun autre document chargé comme Form XObject et la dessine, ce qui garde entièrement vectoriel un contenu tel quun sceau, une page de code QR ou un en-tête de lettre. Les deux préservent le rapport de proportions de la source et centrent le résultat dans le rectangle du bloc. Les trois fonctions de remplissage partagent les mêmes bits Options : le bit 1 (valeur 1) retire le bloc après un remplissage réussi, en supprimant lannotation ou en retirant lentrée du conteneur /Blocks, et le bit 2 (valeur 2) étire le contenu image et PDF au rectangle exact au lieu de lajuster

function FillCertificate(const TemplateFile, SealFile, SignatureFile,
  OutFile: WideString; Values: TStrings): Boolean;
var
  Lib: TPDFlib;
  SealID, SignatureID, I: Integer;
  BlockName, BlockType: WideString;
begin
  Result := False;
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(SealFile, '') <> 1 then Exit;
    SealID := Lib.SelectedDocument;          // le vrai ID de document
    if Lib.LoadFromFile(TemplateFile, '') <> 1 then Exit;
    // le modèle est maintenant le document sélectionné
    SignatureID := Lib.AddImageFromFile(SignatureFile, 0);

    // parcourir à rebours : le bit 1 retire chaque bloc et renumérote les suivants
    for I := Lib.GetBlockCount(1) downto 1 do
    begin
      BlockName := Lib.GetBlockName(1, I);
      BlockType := Lib.GetBlockType(1, I);
      if BlockType = 'text' then
        Lib.FillTextBlock(1, I, Values.Values[BlockName], 1)
      else if (BlockType = 'image') and (SignatureID <> 0) then
        Lib.FillImageBlock(1, I, SignatureID, 1)
      else if BlockType = 'pdf' then
        Lib.FillPDFBlock(1, I, SealID, 1, 1);
    end;
    Result := Lib.SaveToFile(OutFile) = 1;
  finally
    Lib.Free;
  end;
end;

Deux détails de cette boucle sont faciles à rater. La boucle part de lindice le plus haut vers le bas parce que retirer le bloc 2 transforme lancien bloc 3 en bloc 2 ; compter vers le haut avec le bit 1 posé saute un bloc sur deux. Et le document source de FillPDFBlock est chargé en premier et son ID capturé avant que le modèle soit chargé, parce que les IDs dimages appartiennent au document sélectionné, donc AddImageFromFile doit tourner pendant que le modèle est sélectionné. Pour des lots où le sceau ou len-tête change par enregistrement, lassemblage de pages Form XObject et les modèles de filigranes composables de PDFlibPas emploient le même mécanisme de capture et de dessin sur des pages entières

Pourquoi les coordonnées de blocs sortent-elles inversées ?

Les rectangles de blocs sortent inversés quand le code mélange les deux systèmes de coordonnées que PDFlibPas manipule. Un /Rect dannotation est stocké en espace utilisateur PDF, mesuré depuis le bas de la page (ISO 32000-1 §12.5.2), tandis que lAPI de dessin TPDFlib mesure depuis le haut. AddBlock prend une entrée basée sur le haut et stocke le bord bas comme PageHeight - Top - Height ; GetBlockRect et les trois fonctions de remplissage relisent le rectangle et le convertissent avec PageHeight - URy

En pratique cela signifie : utilisez GetBlockRect(Page, BlockIndex, Left, Top, Width, Height) quand vous avez besoin de la géométrie de lemplacement, par exemple pour dessiner un cadre de débogage ou placer une légende à côté dun bloc, et ne lisez pas /Rect via un appel dannotation générique pour lentraîner directement dans DrawText. Un bloc page-piece avec /Rect [40 700 240 730] sur une page letter de 792 pt rapporte Top = 62, pas 700. Le bug que cela prévient est le classique où chaque valeur remplie atterrit en miroir près de la marge basse sans quaucune erreur ne surgisse

Inversion de coordonnées du rectangle de bloc dans PDFlibPas pour Delphi : les valeurs /Rect vivent en espace utilisateur PDF basé sur le bas tandis quAddBlock prend une entrée basée sur le haut et stocke PageHeight moins Top moins Height, si bien que /Rect 40 700 240 730 sur une page letter de 792 pt se relit Top 62 via GetBlockRect et sa conversion PageHeight moins URy
Mélanger les deux systèmes de coordonnées met en miroir chaque valeur remplie vers la marge basse sans une seule erreur ; lisez la géométrie de lemplacement via GetBlockRect au lieu dentraîner /Rect directement dans DrawText

Des pièges dont il vaut la peine dapprendre

Le bug le plus instructif du code des blocs était un décalage dun caractère. Les annotations de blocs sont reconnues par un test de préfixe sur le sous-type, et 'PDFlibBlock:' compte 12 caractères. Une première version comparait Copy(Subtype, 1, 13) à ce littéral de 12 caractères, qui ne peut jamais être égal, donc GetBlockCount renvoyait 0 pour chaque modèle et chaque remplissage rapportait "no such block". Rien ne plantait et rien nétait journalisé. La leçon se généralise à tout moteur de modèles : affirmez le nombre de blocs dun modèle connu dans un test, parce quune énumération silencieuse à zéro correspondance ressemble exactement à une page vide

Le second piège est celui des IDs de documents. TPDFlib.LoadFromFile renvoie un code de statut, 1 pour succès et 0 pour échec, pas lID du document quil vient de charger. Passer ce 1 comme SourceDocumentID à FillPDFBlock échoue parce que 1 nest pas un ID de document valide. Appelez SelectedDocument juste après un chargement réussi pour obtenir le vrai ID, et SelectDocument pour revenir au modèle si vous avez chargé la source ensuite

Un plus petit : FillTextBlock avec une chaîne vide ne fait rien et garde le bloc. Pour un publipostage avec des champs optionnels, comme une seconde ligne dadresse manquante, cest dordinaire ce que vous voulez, mais lannotation non remplie reste alors dans le fichier de sortie. Retirez-la explicitement avec DeleteAnnotation ou remplissez une espace unique avec le bit 1 si la sortie doit être propre. Tous les appels de blocs sauvegardent et restaurent la page sélectionnée, si bien quun remplissage en page 3 ne change pas où va votre prochain appel de dessin

Quelles sont les limites dinteropérabilité des annotations de blocs ?

Les annotations de blocs sont une convention privée, pas une fonction PDF standard. PDFlibBlock:Text et ses frères ne figurent pas parmi les types dannotations listés dans ISO 32000-1 §12.5.6.1, donc une visionneuse les traite comme annotations inconnues. Comme le drapeau Invisible est à zéro et quil ny a pas de flux dapparence, les visionneuses conformes ne dessinent rien pour elles, mais certaines visionneuses peuvent toujours les lister dans un panneau de commentaires, et dautres outils PDF ne comprendront ni ne préserveront leur sémantique

  • Remplissez avec le bit 1 posé pour tout ce qui quitte votre système, afin que le fichier final ne contienne que du contenu de page ordinaire
  • PDF/A nautorise pas de types dannotations hors spécification PDF (ISO 19005-1 §6.5.2), donc une sortie archivée ne doit pas porter dannotations de blocs non remplis
  • Les blocs page-piece sont plus sûrs à garder dans un modèle, puisque /PieceInfo est standard et que les consommateurs ignorent les données quils ne possèdent pas
  • Les blocs ne sont pas des champs de formulaire : pas dinteractivité, pas de valeur de champ dans le fichier et pas dexport vers FDF ou XFDF ; pour des données qui doivent rester éditables, voyez léchange de données de formulaires FDF, XFDF et XFA dans PDFlibPas

Si le modèle doit déclarer ses champs variables de façon standardisée et vérifiable par un validateur pour un workflow dimpression, les blocs sont le mauvais outil. Cest à cela que sert PDF/VCR-1, et le manifeste demplacements de modèles PDF/VCR-1 dans PDFlibPas couvre la façon dont ces emplacements vivent dans larbre de structure plutôt que dans des annotations

FAQ

PDFlibPas peut-il remplir des modèles de blocs créés par dautres outils ?

Oui, quand loutil stocke les blocs sous /PieceInfo /PDFlib /Blocks ou /PieceInfo /PDFlib /Private /Blocks, sous forme de dictionnaire ou de tableau, avec un /Rect de quatre nombres. GetBlockCount les compte après toute annotation PDFlibBlock:*, et les clés personnalisées sexposent via GetBlockProperty

FillTextBlock change-t-il la police que jai sélectionnée avant lappel ?

Non. FillTextBlock sauvegarde la police sélectionnée, la taille de texte, la couleur de texte et lalignement, applique les propriétés fontname, fontsize, fillcolor et alignment du bloc, puis restaure létat dorigine, même si lajustement échoue

Comment placer un logo vectoriel ou une page de code-barres dans un bloc ?

Chargez le PDF source, stockez SelectedDocument comme son ID, resélectionnez le document modèle si besoin, et appelez FillPDFBlock(Page, BlockIndex, SourceID, SourcePage, 1). La page est importée comme Form XObject, donc elle reste vectorielle et est ajustée au bloc avec son rapport de proportions préservé sauf si vous ajoutez le bit 2

Les modèles de blocs, limport de pages Form XObject et le reste de la boîte à outils de données variables sont livrés dans PDFlibPas, la losLab PDF Library for Delphi, qui couvre la création, lédition et la production dimpression PDF pour des projets Delphi et C++Builder