Technical Article

Formulaires PDF interactifs dans Delphi : Actions et JavaScript

Un champ de formulaire PDF n'est en soi qu'une boîte contenant une valeur. Ce qui fait qu'un formulaire se comporte comme une petite application, c'est l'action qui lui est associée : un clic qui masque une section, récupère des valeurs enregistrées depuis un fichier, saute à la dernière page ou exécute un script qui additionne une colonne. Rien de tout cela ne réside dans le champ. Cela vit dans un dictionnaire d'actions, et la norme ISO 32000-1 organise toute cette famille dans le §12.6. Cet article présente les actions les plus couramment utilisées par un programme Delphi et montre comment PDFlibPas connecte chacune d'elles à un champ ou à un lien.

Le modèle mental à conserver est qu'un champ et une action sont des objets distincts reliés par une référence. Une annotation de type widget ou de type lien porte une action dans son entrée /A. L'action désigne le champ sur lequel elle opère par son titre, et non par son index, de sorte que le titre attribué à un champ est la poignée que toute action ultérieure utilise pour le trouver. Une fois cette distinction claire, l'API cesse de ressembler à un ensemble de fonctions disparates et ressemble plutôt à un modèle unique appliqué à quatre types de verbes.

Actions nommées : navigation sans numéro de page

Les actions les plus simples ne comportent aucun paramètre. La norme ISO 32000-1 §12.6.4.11, Tableau 194, définit les actions nommées : le visualiseur interprète un nom symbolique au moment de l'exécution au lieu de suivre une destination stockée. Quatre noms sont universellement pris en charge, et ce sont exactement ceux qu'un lecteur attend d'une barre d'outils : NextPage, PrevPage, FirstPage et LastPage. Comme la destination est relative à la page actuellement affichée par le lecteur, un bouton Suivant construit de cette manière fonctionne sur chaque page sans que vous ayez à calculer une cible.

Dans PDFlibPas, une action nommée est attachée à un rectangle de zone sensible sur la page actuelle. Les quatrième et cinquième arguments entiers sélectionnent le verbe et l'apparence.

// NamedActionType: 0 = NextPage, 1 = PrevPage, 2 = FirstPage, 3 = LastPage
// Options bit 0 (value 1) draws a border around the hotspot
Pdf.AddLinkToNamedAction(500, 560, 60, 18, 0, 1);   // Next
Pdf.AddLinkToNamedAction(40, 560, 60, 18, 1, 1);    // Previous
Pdf.AddLinkToNamedAction(110, 560, 60, 18, 3, 1);   // jump to last page

Il n'y a pas de destination à synchroniser, ce qui est tout l'intérêt. Une action nommée survit à l'insertion et à la suppression de pages car elle ne nomme jamais de page au départ. Comparez cela avec un lien d'accès direct explicite, qui stocke un index de page cible que vous devez renuméroter dès que le document s'agrandit.

L'action Hide et son piège de tableau

L'action Hide, ISO 32000-1 §12.6.4.10, Tableau 196, bascule la visibilité d'un ou plusieurs champs. C'est le moyen le plus propre de créer un comportement d'affichage et de masquage sans script, et c'est ce que vous voulez pour un lien Afficher les détails ou pour deux panneaux mutuellement exclusifs où la révélation de l'un masque l'autre. L'action porte une cible dans son entrée /T et un booléen /H qui décide de la direction : masquer si vrai, afficher si faux.

La subtilité réside entièrement dans la manière dont cette cible est encodée, et c'est le genre de détail qui produit un formulaire qui fonctionne sur votre machine mais échoue sur celle d'un client. Lorsque l'action désigne un seul champ, /T est écrit sous forme d'une seule chaîne de caractères. Lorsqu'elle en désigne plusieurs, /T est écrit sous forme de tableau de chaînes. Les lecteurs plus anciens ne traitent pas un tableau à un élément de la même manière qu'une chaîne simple, de sorte que l'encodage doit s'adapter au nombre : un nom unique doit être émis sous forme de chaîne, et non de tableau de longueur un, pour que le plus grand nombre de lecteurs le respecte. PDFlibPas prend cette décision pour vous. Vous transmettez les noms de champs séparés par des virgules, des points-virgules ou des sauts de ligne, et le moteur émet une chaîne unique pour un nom et un tableau pour deux ou plus.

// HideFlag non-zero hides the listed fields (/H true); zero shows them.
// One name -> /T is a text string. Two or more -> /T is an array of strings.
Pdf.AddLinkToHideField(40, 700, 90, 18, 'ShippingAddress', 1, 1);
Pdf.AddLinkToHideField(140, 700, 90, 18,
  'ShippingName,ShippingAddress,ShippingZip', 1, 1);

Comme l'action ne fait référence à aucune ressource externe, elle reste compatible avec le format PDF/A. Les noms que vous passez sont des titres de champs entièrement qualifiés, c'est pourquoi un champ enfant à l'intérieur d'un groupe doit être ciblé via son chemin complet séparé par des points plutôt que par son simple nom de feuille.

ImportData : pré-remplissage depuis FDF

Là où l'action Hide réorganise ce qui est déjà sur la page, l'action d'importation de données apporte des valeurs de l'extérieur. La norme ISO 32000-1 §12.6.4.8, Tableau 198, la définit comme une action qui remplit l'AcroForm à partir d'un fichier Forms Data Format (FDF) sur le disque. C'est l'action qui se cache derrière un contrôle de type Recharger les données d'exemple ou Réinitialiser par défaut, où un fichier FDF est fourni à côté du PDF et contient les valeurs de champs canoniques. L'appel reflète les autres, prenant le rectangle de la zone sensible, le chemin vers le FDF et un masque de bits d'apparence : Pdf.AddLinkToImportData(40, 660, 120, 18, 'defaults.fdf', 1). Le fichier n'a pas besoin d'exister lors de la construction du PDF, mais il doit être présent lorsque l'utilisateur clique, et tous les antislashs dans le chemin sont réécrits pour vous sous la forme canonique de barre oblique du PDF.

Une contrainte mérite d'être énoncée clairement car elle est souvent une surprise. Une action d'importation de données pointe vers un fichier externe, elle n'est donc pas autorisée en PDF/A. Lorsque le document est en mode PDF/A, l'appel renvoie zéro et n'ajoute rien plutôt que de produire un fichier qui échouerait à la validation. Si votre flux de production cible une sortie d'archivage, le pré-remplissage doit se faire au moment de la génération en écrivant directement les valeurs des champs, et non en les différant lors d'un clic.

JavaScript : packages globaux et scripts par action

Pour une logique qui dépasse le cadre du masquage, de l'affichage et de l'importation, la famille d'actions s'étend au JavaScript au niveau du document. Un script peut résider dans deux endroits distincts, et la différence est importante. Un package JavaScript au niveau du document est stocké une seule fois pour tout le fichier et s'exécute à l'ouverture du document, ce qui en fait l'emplacement idéal pour les définitions de fonctions et l'état partagé. Un script par action est attaché à un lien ou à un champ spécifique et ne s'exécute que lorsque cet objet est activé, ce qui en fait l'emplacement idéal pour la ligne unique qui appelle une fonction déjà définie par le package.

PDFlibPas expose les deux. AddGlobalJavaScript stocke un package nommé au niveau du document ; réutiliser un nom remplace ce qui y était stocké. AddLinkToJavaScript attache un script à une zone sensible afin qu'un clic l'exécute.

// Document-level package: define a reusable function once.
Pdf.AddGlobalJavaScript('Totals',
  'function recalcTotal() {' +
  '  var net = this.getField("Net").value;' +
  '  var tax = this.getField("Tax").value;' +
  '  this.getField("Gross").value = Number(net) + Number(tax);' +
  '}');

// Per-action script on a link: just call the shared function.
Pdf.AddLinkToJavaScript(40, 620, 100, 18, 'recalcTotal();', 1);

Garder la fonction dans le package global et l'appel dans le lien n'est pas une préférence de style. Cela évite de dupliquer le même code sur chaque contrôle qui en a besoin, et cela signifie qu'un lecteur dont les scripts sont désactivés ne fera tout simplement rien au clic plutôt que de planter sur un bloc en ligne mal formé. Cela permet également de conserver de petites entrées par action, ce qui maintient le fichier lisible lorsque vous l'inspecterez plus tard.

Champs, champs enfants et figement du résultat

Les actions ont besoin de champs sur lesquels agir, il est donc utile de voir comment un champ est créé. NewFormField crée un champ sur la page actuelle et renvoie son index ; le type entier sélectionne le genre, où 1 is Text, 2 is Pushbutton, 3 is Checkbox, 4 is Radiobutton, 5 is Choice, 6 is Signature et 7 is un Parent qui possède des enfants mais ne dessine rien lui-même. Le titre que vous passez ne peut pas contenir de point, car le point est le séparateur dans les noms entièrement qualifiés que les actions utilisent pour cibler les enfants.

Les groupes radio et les formulaires hiérarchiques sont construits en attribuant des enfants à un champ parent. NewChildFormField ajoute un enfant sous un parent nommé, et pour les cas de boutons radio et de choix, AddFormFieldSub ajoute les options individuelles et renvoie un index temporaire que vous utilisez pour positionner chacune d'elles. Lorsque la phase interactive est terminée et que vous souhaitez figer un champ pour que son apparence actuelle devienne un contenu de page permanent, FlattenFormField dessine le champ sur la page et le supprime du formulaire. Après un aplatissement, les index des champs suivants se décalent d'une unité vers le bas, ce qui est la seule chose à retenir si vous aplatissez plusieurs champs dans une boucle.

var
  Pdf: TPDFlib;
  FldShip: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    Pdf.SetOrigin(1);          // top-left origin
    Pdf.SetPageSize('A4');
    Pdf.NewPage;

    // A text field the Hide action will target by its title.
    FldShip := Pdf.NewFormField('ShippingAddress', 1);
    Pdf.SetFormFieldBounds(FldShip, 40, 120, 240, 20);
    Pdf.SetFormFieldValue(FldShip, '');

    // Wire a Hide link and a navigation link to this page.
    Pdf.DrawText(40, 110, 'Toggle shipping block:');
    Pdf.AddLinkToHideField(220, 100, 70, 16, 'ShippingAddress', 1, 1);
    Pdf.AddLinkToNamedAction(500, 800, 60, 18, 3, 1);  // Last page

    // A document-level script available to every event in the file.
    Pdf.AddGlobalJavaScript('OnOpen',
      'app.alert("Form ready", 3);');

    // Freeze the field if the output should no longer be editable.
    // Pdf.FlattenFormField(FldShip);

    if Pdf.SaveToFile('form_actions.pdf') <> 1 then
      raise Exception.Create('Save failed');
  finally
    Pdf.Free;
  end;
end;

L'appel de mise à plat (flatten) est commenté à dessein. Laissez-le de côté et le document sera livré sous forme de formulaire actif dont les actions s'activent dans le lecteur. Activez-le et le champ sera converti en éléments statiques, ce qui est ce que vous souhaitez lorsque le formulaire a été rempli et que le résultat doit être transmis sous forme d'enregistrement figé. Le même champ, le même code, deux documents très différents selon que vous le figez ou non.

Choisir le bon verbe

Les quatre actions se divisent clairement selon ce qu'elles affectent. Une action nommée déplace la zone d'affichage et n'a besoin d'aucun champ. Une action Hide modifie la visibilité et nécessite des titres de champs, l'encodage chaîne contre tableau étant géré pour vous. Une action d'importation de données accède à un fichier sur le disque et est donc interdite en PDF/A. Une action JavaScript exécute une logique arbitraire et s'avère plus efficace lorsqu'elle est répartie entre un package global de fonctions et de petits appels par action. Optez pour la solution la plus simple : une action Hide is plus portable qu'un script qui définit un indicateur masqué, et une action nommée est plus durable qu'une destination de page stockée car il n'y a pas de numéro à maintenir.

À partir de là, deux sujets connexes complètent le tableau. Si le formulaire fait partie d'un document accessible, l'arbre de structure parcouru par les lecteurs d'écran est traité dans notre article sur le PDF balisé et la structure d'accessibilité. Lorsque le formulaire rempli doit être verrouillé et signé, le flux de travail est décrit dans le guide pratique de l'espace de travail de conformité et de signature. Tous trois s'appuient sur le même moteur, fourni avec la bibliothèque PDF pour Delphi en même temps que les API de création, de formulaire et de signature présentées par ailleurs sur ce blog.