Article technique

Créer des champs AcroForm et des actions avec HotPDF dans Delphi

Une équipe de gestion de sinistres déploie un formulaire de consentement généré en Delphi qui semble irréprochable dans Acrobat : chaque case à cocher s'affiche, chaque libellé est aligné et la mise en page correspond à l'original papier. Trois semaines plus tard, le service de réception commence à rejeter un tiers des envois. Le backend attend que la case de consentement publie la valeur Y ; le formulaire exporte Yes, parce que son auteur a supposé que le libellé visible et la valeur exportée étaient la même chose. Rien dans la page rendue ne laisse voir la différence. C'est la propriété décisive du développement AcroForm : le widget visible et le contrat de données placé dessous sont deux structures distinctes, et une seule est vérifiée quand quelqu'un ouvre le fichier pour le regarder.

HotPDF est un composant VCL natif qui écrit les dictionnaires de champs AcroForm directement depuis du code Delphi et C++Builder ; les deux structures sont donc sous contrôle explicite du programme, et les deux peuvent se tromper indépendamment. Les sections suivantes couvrent la création de champs, les actions de boutons et le JavaScript au niveau champ, avec l'accent sur les endroits où l'aperçu de page et les données de formulaire divergent silencieusement.

Les noms de champs sont des clés de routage, pas des libellés

Chaque champ AcroForm porte un nom pleinement qualifié, et ISO 32000-1 §12.7.3 définit ce nom, jamais le libellé visible, comme la clé sous laquelle la valeur du champ circule dans les soumissions FDF, XFDF et HTTP. Deux conséquences surprennent régulièrement les développeurs issus de la conception de formulaires VCL, où le nom d'un contrôle n'est guère plus qu'un identifiant dans le code.

Premièrement, deux champs portant le même nom pleinement qualifié ne sont pas deux champs. Le modèle PDF les traite comme deux annotations widget d'un champ unique, partageant une seule valeur : saisissez dans l'un et l'autre se met à jour immédiatement. Répéter le nom d'un client sur chaque page d'un contrat est un usage légitime de ce comportement. L'obtenir par accident, parce qu'une boucle de génération a réutilisé 'Field1' sur trois pages, est un bug qu'aucune inspection visuelle ne détecte : chaque page affiche toujours son propre widget, et le miroir n'apparaît que lorsqu'un utilisateur commence à saisir.

Deuxièmement, les noms pointés comme applicant.email construisent une hiérarchie : le nœud parent applicant regroupe ses enfants pour les opérations de réinitialisation et de soumission qui ne ciblent qu'une partie du formulaire. Nommer les champs hiérarchiquement dès le départ ne coûte rien et rapporte dès que le système récepteur demande « uniquement le bloc applicant ».

Les boutons radio ajoutent une troisième règle : les boutons qui doivent basculer comme un seul groupe doivent partager un nom de groupe. Dans HotPDF, les appels AddRadioButton utilisant le même nom de groupe rattachent leurs widgets à un champ parent unique, et la valeur exportée de chaque bouton, 'basic' ou 'full', identifie l'option sélectionnée. Donnez plutôt un nom unique à chaque bouton et vous obtenez des interrupteurs marche/arrêt indépendants au lieu d'un groupe mutuellement exclusif.

Créer le jeu de champs page par page

HotPDF place les champs au moyen des méthodes de THPDFPage, de sorte que chaque champ appartient à l'objet page qui l'a créé. Un piège de séquencement domine ici : AddPage repointe immédiatement CurrentPage vers la nouvelle page ; tout appel de champ émis ensuite atterrit donc sur cette nouvelle page même s'il appartient logiquement à la précédente. Construisez chaque page entièrement, contenu dessiné et champs ensemble, avant d'appeler AddPage.

procedure BuildClaimForm(Pdf: THotPDF);
begin
  // Page 1: applicant block
  Pdf.CurrentPage.AddTextField('applicant.name', '', Rect(50, 700, 300, 722));
  Pdf.CurrentPage.AddTextField('applicant.email', '', Rect(50, 660, 300, 682));
  Pdf.CurrentPage.AddCheckBox('consent', 'Y', Rect(50, 620, 70, 640), False);
  Pdf.CurrentPage.AddRadioButton('coverage', 'basic', Rect(50, 580, 70, 600), True);
  Pdf.CurrentPage.AddRadioButton('coverage', 'full', Rect(90, 580, 110, 600), False);
  Pdf.CurrentPage.AddComboBox('plan', 'Standard',
    ['Basic', 'Standard', 'Premium'], Rect(50, 540, 200, 565));

  Pdf.AddPage;  // CurrentPage now points at page 2
  Pdf.CurrentPage.AddListBox('riders', 'None',
    ['None', 'Flood', 'Earthquake'], Rect(50, 500, 200, 600));
end;

Les coordonnées suivent la convention PDF : l'origine est dans l'angle inférieur gauche de la page, la même convention que celle utilisée par TextOut pour le texte dessiné. Rect(50, 100, 200, 120) se trouve donc près du bas d'une page Letter, pas près du haut. Les équipes qui portent des tables de mise en page depuis VCL, où Y grandit vers le bas depuis le haut, produisent presque toujours une première version où tous les champs sont inversés verticalement. Convertissez les coordonnées dans un helper partagé plutôt qu'à chaque appel, afin que la correction s'applique partout à la fois.

Relier les boutons aux actions URI, JavaScript et submit

Un bouton poussoir ne fait rien tant qu'une action ne lui est pas attachée. HotPDF expose les types d'action d'ISO 32000-1 §12.6.4 par l'énumération THPDFButtonActionbaURI, baJavaScript, baSubmitURL, baResetForm, baHide, baShow, baNamed — ainsi que deux méthodes qui créent le bouton et lient son action en une seule étape.

// Open a help page in the system browser
Pdf.CurrentPage.AddPushButtonWithAction('btnHelp', 'Help',
  'https://www.example.com/claims-help', Rect(320, 700, 420, 730), baURI);

// Run viewer-side JavaScript
Pdf.CurrentPage.AddPushButtonWithAction('btnRecalc', 'Recalculate',
  'app.alert("Totals updated.");', Rect(320, 660, 420, 690), baJavaScript);

// Submit as XFDF and keep empty fields in the payload
Pdf.CurrentPage.AddPushButtonWithSubmitAction('btnSubmit', 'Submit claim',
  'https://api.example.com/claims', Rect(320, 620, 420, 650),
  [sffXFDF, sffIncludeNoValueFields]);

Les flags de soumission méritent plus d'attention de conception qu'ils n'en reçoivent habituellement. AddPushButtonWithSubmitAction prend un ensemble THPDFSubmitFormFlags, et un ensemble vide produit un POST encodé URL simple, format que beaucoup de points de terminaison d'exemple acceptent et que beaucoup de points de terminaison de production refusent. L'ajout de sffXFDF bascule le payload vers XFDF ; sffGetMethod change le verbe HTTP ; sffIncludeNoValueFields fait apparaître les champs vides dans le payload au lieu de les supprimer silencieusement, ce qui compte dès que le consommateur distingue « absent » de « vide ». L'ensemble de flags fait réellement partie de votre contrat d'interface avec le point de terminaison récepteur ; choisissez-le avec l'équipe qui parse la soumission, pas après le premier lot rejeté.

JavaScript au niveau champ : frappe, format, validation

Au-delà des clics sur boutons, HotPDF attache du JavaScript aux événements par champ que les lecteurs capables de scripts déclenchent pendant la saisie. Les trois déclencheurs correspondent à différents moments du cycle d'entrée : les actions de frappe s'exécutent à l'arrivée des caractères, les actions de format réécrivent la valeur affichée après validation d'un changement, et les actions de validation acceptent ou refusent la valeur validée.

// Reject committed values that are not plausible email addresses
Pdf.AttachFieldKeyStrokeAction('applicant.email',
  'if (event.willCommit && !/^[\w.-]+@[\w.-]+\.\w+$/.test(event.value)) event.rc = false;');

// Display US phone numbers as (NNN) NNN-NNNN
Pdf.AttachFieldFormatAction('applicant.phone',
  'event.value = event.value.replace(/(\d{3})(\d{3})(\d{4})/, "($1) $2-$3");');

// Refuse applicants under 18 at commit time
Pdf.AttachFieldValidateAction('applicant.age',
  'if (parseInt(event.value) < 18) event.rc = false;');

Définir event.rc = false dans un script de frappe ou de validation force le lecteur à rejeter l'entrée. Gardez toutefois la limite en vue : ce code ne s'exécute que dans les lecteurs dotés d'un moteur JavaScript. Acrobat et quelques produits desktop l'exécutent ; la plupart des lecteurs mobiles, moteurs intégrés au navigateur et pipelines d'impression l'ignorent totalement. Les scripts de champs améliorent la qualité des données pour les utilisateurs qui en bénéficient ; ils ne constituent pas une frontière de sécurité, et chaque valeur soumise doit encore être validée côté serveur à son arrivée.

Défauts qui passent la revue visuelle

Certains défauts de formulaire sont structurellement invisibles lors d'une vérification « ouvrir et regarder ». Ces quatre cas expliquent la plupart des escalades AcroForm qui arrivent au support, et chacun peut être capturé mécaniquement avant publication :

  • Dérive de valeur exportée. Une case créée comme AddCheckBox('consent', 'Yes', ...) publie Yes ; un consommateur qui attend Y rejette chaque soumission alors que la page semble parfaite. Exportez le formulaire rempli en XFDF depuis Acrobat et comparez les valeurs au schéma du consommateur.
  • Miroir de valeur accidentel. Des noms pleinement qualifiés dupliqués fusionnent les champs en un seul. Le symptôme apparaît à la saisie, jamais à la génération ; testez donc en tapant dans le formulaire, pas en le rendant.
  • Valeurs de combo hors liste. Si la valeur courante passée à AddComboBox ne figure pas parmi les options, les lecteurs ne s'accordent pas : l'afficher, la vider ou la signaler. Gardez la valeur par défaut dans la liste.
  • Champs encore modifiables après clôture du workflow. HotPDF ne fournit pas d'appel de flattening d'apparence pour les champs AcroForm ; la manière prise en charge de figer un formulaire terminé consiste à créer les champs avec le flag ffReadOnly, qui conserve la valeur rendue par le flux d'apparence du champ tout en bloquant les modifications. Un champ en lecture seule reste un objet de formulaire vivant, que les outils d'assemblage et de signature en aval gèrent de façon prévisible.

Un comportement côté lecteur mérite une note de régression même si aucun changement de code ne le corrige : les déploiements Acrobat en entreprise peuvent désactiver JavaScript ou restreindre les cibles de soumission par stratégie ; une action qui a fonctionné pendant tout le développement peut donc être inerte sur le poste d'un client. Donnez aux soumissions un repli visible, au minimum une instruction imprimée, pour le cas où le bouton ne ferait rien.

Où le travail sur les formulaires rejoint le reste du document

Un champ de signature est lui-même un type de champ AcroForm ; un formulaire qui sera certifié ou contresigné plus tard doit donc réserver ce champ pendant la génération plutôt que l'ajouter ensuite, les détails au niveau octets étant traités dans l'article compagnon sur les signatures numériques et la signature PAdES avec HotPDF. Si vos entrées arrivent sous forme de paquets XFA plutôt que d'AcroForm natif, aplatir XFA en champs AcroForm est un workflow séparé avec son propre modèle de pertes, car les deux technologies de formulaire sont mutuellement exclusives dans un même fichier.

Les méthodes de champ, d'action et de déclencheur présentées ici font partie de l'API standard HotPDF Component pour Delphi et C++Builder ; la page produit renvoie à la référence complète, y compris les surcharges de flags de champ et l'énumération complète des flags de soumission.