Vous avez un modèle de facture fourni par un tiers, ou un contrat archivé qu'une personne a généré il y a des années dans un logiciel introuvable, et la demande consiste à le rendre interactif : placer une zone de signature dans un coin, ajouter quelques champs texte, transformer une simple liste de contrôle en vraies cases à cocher. Le piège, c'est que vous n'êtes pas en train d'écrire ce PDF depuis zéro. Il existe déjà, avec ses pages, ses flux de contenu et ses polices que vous ne contrôlez pas, et vous devez greffer des widgets AcroForm sur ce graphe d'objets sans le reconstruire. C'est un problème différent de la création d'un formulaire sur un document neuf, et la partie qui piège les gens reste invisible jusqu'à ce qu'ils ouvrent le résultat dans une visionneuse et que les champs qu'ils viennent d'écrire n'apparaissent nulle part sur la page
HotPDF est un composant PDF VCL natif pour Delphi et C++Builder, et depuis la version v2.247.0 il expose une famille dédiée de méthodes exactement pour cela : créer les six types de champs standards directement sur un document chargé avec LoadFromFile. Cet article explique ce que font ces méthodes, le dictionnaire ISO 32000-1 qu'elles construisent, et l'unique indicateur sans lequel toute l'opération produit silencieusement un fichier qui semble vide
Pourquoi la création de champs sur document chargé suit son propre chemin
Quand vous construisez un PDF à partir de rien, HotPDF possède tout le modèle d'objets. Chaque page est un wrapper THPDFPage écrivable, et l'ajout d'un champ texte via AddTextField relie le nouveau widget à l'objet d'annotation de la page, à l'objet page et à la collection de champs du formulaire, puis génère un flux d'apparence à partir des ressources de polices du document. Le flux d'apparence est la surface visible du widget, la boîte, la bordure et tout texte par défaut, dessinés comme des opérateurs PDF que la visionneuse rend tels quels
Un document chargé ne vous donne rien de tout cela. Les pages sont arrivées comme des dictionnaires bruts ; il n'existe pas de wrapper THPDFPage écrivable pour y accrocher un widget, et plus important encore il n'y a pas de chaîne de ressources de police prête à peindre des flux d'apparence. Le chemin chargé suit donc une autre voie. Il écrit les dictionnaires de champs directement sur le graphe d'objets analysé et adresse les pages par index de base zéro plutôt que par objet page. Les types de champs et les bits d'indicateur correspondent exactement au chemin de création depuis zéro, donc un champ Text reste un champ Text dans les deux cas ; ce qui change, c'est la plomberie sous-jacente et, surtout, la façon dont la surface du widget est dessinée
L'indicateur /NeedAppearances n'est pas optionnel ici
C'est le point unique qui décide si votre travail apparaît ou non. Comme le chemin chargé ne génère pas de flux d'apparence, un widget fraîchement ajouté arrive dans la visionneuse sans entrée /AP: un champ sans surface décrite. Beaucoup de visionneuses, lorsqu'on leur demande de rendre un widget qui n'a pas d'apparence et aucune consigne pour en construire une, n'affichent rien du tout. Le champ est dans le fichier, structurellement valide, adressable par un outil de remplissage de formulaires, et totalement invisible pour un humain
L'issue de secours est définie dans ISO 32000-1 §12.7.3 : le dictionnaire AcroForm porte un booléen /NeedAppearances et quand il vaut true une visionneuse conforme doit construire elle-même les flux d'apparence manquants à partir de la chaîne /DA propre à chaque champ et de sa valeur. HotPDF s'en charge pour vous. La première fois que vous ajoutez un champ à un document chargé, EnsureLoadedAcroForm s'exécute : si le catalog n'a pas de /AcroForm il en crée un, s'il n'y a pas de tableau /Fields il en crée un, et il force /NeedAppearances true. Vous ne l'appelez pas directement, mais savoir qu'il existe explique le comportement. Cela explique aussi une réserve de déploiement qu'il faut dire clairement : quelques visionneuses minimales ou non conformes ignorent /NeedAppearances et n'affichent toujours rien. Pour les lecteurs courants, l'indicateur remplit son rôle, mais si votre public utilise un moteur intégré inhabituel, testez-le avant de promettre quoi que ce soit
Ajouter les six types de champs
Chaque méthode suit la même forme. Vous passez l'index de page de base zéro, les quatre coins du rectangle du widget dans les coordonnées d'espace utilisateur PDF, le nom du champ, et les arguments supplémentaires nécessaires au type. Le rectangle est X1, Y1, X2, Y2 avec l'origine PDF en bas à gauche de la page, donc les valeurs Y plus grandes se trouvent plus haut ; c'est la convention de coordonnées du format de fichier, pas la convention d'écran en haut à gauche, et se tromper de sens est la deuxième erreur la plus courante après avoir oublié l'indicateur. Chaque appel renvoie l'index de base zéro du nouveau champ, ou -1 si l'index de page était hors limites ou si l'objet page n'a pas pu être résolu
var
Pdf: THotPDF;
Idx: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('contract.pdf') <= 0 then Exit;
// Text field: name, initial value, max length (0 = unlimited)
Idx := Pdf.AddLoadedTextField(0, 72, 680, 320, 700, 'FullName', '', 0);
// CheckBox: export value, initial checked state
Pdf.AddLoadedCheckBox(0, 72, 640, 90, 658, 'AgreeTerms', 'Yes', False);
// Signature field: just a name and a rectangle
Pdf.AddLoadedSignatureField(0, 360, 72, 540, 132, 'ApproverSig');
if Idx >= 0 then
Pdf.SaveLoadedDocument('contract-interactive.pdf');
finally
Pdf.Free;
end;
end;
Le troisième et le quatrième argument chaîne du champ texte sont le nom du champ et sa valeur initiale /V ; l'entier est /MaxLen, écrit seulement lorsqu'il est supérieur à zéro. HotPDF donne à chaque champ modifiable une chaîne d'apparence par défaut /Helv 12 Tf 0 0 0 rg, que ce qu'un lecteur qui respecte /NeedAppearances utilise pour décider de la police et de la couleur dans lesquelles il peint la valeur. La case à cocher prend une valeur d'export, la chaîne que le formulaire envoie lorsque la case est cochée, ainsi qu'un booléen pour l'état initial ; en interne, elle écrit les entrées de nom /V, /AS, et /DV correspondantes afin que l'état on/off soit cohérent dès l'ouverture du fichier. Une valeur d'export vide retombe sur Yes, le nom conventionnel de la case cochée
Champs de sélection et drapeaux /Ff
ComboBox et ListBox sont tous deux des champs de choix, de type /Ch dans ISO 32000-1 §12.7.4. La différence entre une liste déroulante et une liste à défilement tient à un bit dans l'entier des drapeaux du champ /Ff: le bit 18, le drapeau Combo, valeur $40000. HotPDF active ce bit pour AddLoadedComboBox et le laisse à zéro pour AddLoadedListBox; sinon les deux sont identiques, et tous deux prennent leurs choix comme un tableau ouvert de chaînes écrit dans l'entrée /Opt
// Dropdown (Combo flag set internally) with an initial selection
Pdf.AddLoadedComboBox(0, 72, 600, 300, 620, 'Country', 'Canada',
['United States', 'Canada', 'Mexico']);
// Scrolling list, no initial value
Pdf.AddLoadedListBox(0, 72, 520, 300, 590, 'Priority', '',
['Low', 'Normal', 'High']);
// Push button with a caption drawn through /MK
Pdf.AddLoadedPushButton(0, 360, 600, 480, 626, 'SubmitBtn', 'Submit');
Deux remarques sur la liste d'options. HotPDF écrit chaque entrée /Opt comme une simple chaîne, où la valeur d'export et l'étiquette affichée sont le même texte. ISO 32000-1 §12.7.4.4 autorise aussi la forme à deux éléments [export display] quand vous avez besoin que la valeur envoyée diffère de ce que l'utilisateur lit ; les méthodes de création sur document chargé utilisent la forme simple à une seule chaîne, donc si vous avez besoin de valeurs d'export et d'affichage distinctes, vous les définiriez vous-même dans le dictionnaire résultant. Et la valeur que vous passez comme sélection courante du champ devrait être l'une des options que vous avez fournies, puisque la visionneuse la compare à la liste
Le bouton poussoir est l'autre cas piloté par un drapeau : type de champ /Btn avec le bit 17, le drapeau PushButton, valeur $10000. C'est ce bit qui distingue un bouton cliquable d'une case à cocher, qui est aussi un champ /Btn mais sans ce bit. La légende que vous passez est écrite dans le dictionnaire de caractéristiques d'apparence /MK comme légende normale /CA. Il faut être honnête sur le périmètre : le bouton est créé avec son libellé et son rectangle, mais la méthode de création chargée n'attache pas d'action, donc à elle seule elle produit un bouton qui a l'air correct et ne fait rien lorsqu'on clique dessus. Brancher des actions d'envoi, de réinitialisation ou JavaScript est un sujet séparé ; côté création depuis zéro, le flux champ-plus-action est traité dans créer des champs et des actions AcroForm dans Delphi, qui est le bon point de comparaison pour ce que le chemin chargé laisse volontairement de côté
Le dictionnaire partagé par tous les champs
Sous les six méthodes se trouve un générateur commun qui construit l'annotation widget et l'enregistre à deux endroits. Il écrit /Type /Annot et /Subtype /Widget, le tableau /Rect issu de vos quatre coordonnées, les drapeaux d'annotation /F 4 qui activent le bit Print pour que le champ apparaisse sur papier autant qu'à l'écran, le nom du champ /T, le type de champ /FT, les drapeaux /Ff et un lien de retour /P vers l'objet page. Puis il ajoute le nouveau champ au tableau /Fields d'AcroForm et au tableau /Annots de cette page, en résolvant les références indirectes au passage afin d'étendre les vrais tableaux plutôt que d'orpheliner le widget
Cette double inscription compte, parce qu'un widget qui vit dans un seul des deux tableaux est cassé d'une façon subtile. Un champ présent dans /Fields mais absent du /Annots de la page est connu du formulaire mais jamais peint ; le cas inverse est peint mais inconnu de la logique du formulaire. HotPDF garde les deux synchronisés à chaque ajout, ce qui est le genre de comptabilité qu'il faudrait autrement faire exactement à la main face à la spécification
Quelques limites honnêtes
Posez les attentes avant de bâtir un flux de travail là-dessus. Le comportement de transformation puis régénération dépend du fait que la visionneuse respecte /NeedAppearances, ce qui couvre Acrobat, les moteurs PDF récents des navigateurs et les lecteurs de bureau courants, mais ne constitue pas une garantie absolue pour tous les moteurs du monde réel. Si vous devez produire un fichier dont les champs s'affichent identiquement partout, y compris dans les visionneuses qui ignorent l'indicateur, vous entrez dans le territoire des flux d'apparence et le chemin de création depuis zéro qui peint /AP pour vous est le meilleur choix. Le champ de signature, lui aussi, est créé comme un widget de signature vide prêt à être signé ; placer le champ n'est pas la même chose qu'appliquer une signature cryptographique
Pour modifier ce qui existe déjà plutôt que d'ajouter quelque chose, l'opération liée est l'aplatissement des formulaires, où vous recoupez les champs interactifs dans le contenu statique de la page afin que les valeurs deviennent permanentes et non modifiables ; cet aller-retour, y compris la manière dont les formulaires portant XFA sont gérés, est expliqué dans aplatir XFA et les champs AcroForm dans Delphi. L'ajout de champs et l'aplatissement des champs sont deux extrémités du même cycle de vie : cet article explique comment apporter l'interactivité à un document qui en était dépourvu, et l'aplatissement explique comment la retirer une fois que le formulaire a servi
L'API de formulaire sur document chargé présentée ici fait partie du HotPDF Component standard pour Delphi et C++Builder, avec la référence complète des drapeaux de champ, de la gestion de l'apparence et du reste du modèle AcroForm