Un champ de formulaire PDF en lui-même n'est qu'une boîte qui contient une valeur. Ce qui permet à un formulaire de se comporter comme une petite application, c'est l'action qui lui est attachée : un clic qui masque une section, récupère des valeurs enregistrées dans un fichier, saute à la dernière page, ou exécute un script qui totalise une colonne. Rien de tout cela ne réside dans le champ. Cela réside dans un dictionnaire d'actions, et la norme ISO 32000-1 organise toute la famille dans le §12.6. Cet article parcourt les actions qu'un programme Delphi utilise le plus souvent et montre comment PDFlibPas relie chacune d'elles à un champ ou à un lien
Le modèle mental à retenir est qu'un champ et une action sont des objets distincts joints par une référence. Une annotation de widget ou une annotation de lien porte une action dans son entrée /A. L'action nomme le champ sur lequel elle opère par son titre, et non par son index, de sorte que le titre que vous donnez à un champ est l'identifiant que chaque action ultérieure utilise pour le trouver. Une fois cette séparation claire, l'API cesse de ressembler à un ramassis d'appels et commence à ressembler à 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. Étant donné que la destination est relative à la page que le visualiseur affiche actuellement, un bouton Suivant construit de cette manière fonctionne sur chaque page sans que vous n'ayez à calculer de cible
Dans PDFlibPas, une action nommée est attachée à un rectangle de zone réactive sur la page courante. 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 en premier lieu. Contrastez cela avec un lien explicite de type aller-à (go-to), 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 la façon la plus propre de construire un comportement d'affichage et de masquage sans script, et c'est ce que vous souhaitez pour un lien Afficher les détails ou pour deux panneaux mutuellement exclusifs où révéler 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 (true), afficher si faux (false)
La subtilité réside entièrement dans la façon dont cette cible est encodée, et c'est le genre de détail qui produit un formulaire qui fonctionne sur votre machine et échoue chez un client. Lorsque l'action nomme un seul champ, /T est écrit comme une seule chaîne de texte. Lorsqu'elle en nomme plusieurs, /T est écrit comme un tableau de chaînes de texte. Les visualiseurs plus anciens ne traitent pas un tableau à un seul élément de la même manière qu'une simple chaîne, l'encodage doit donc se ramifier en fonction du nombre : un nom unique doit être émis sous forme de chaîne, et non sous forme de tableau de longueur un, si la plus large gamme de lecteurs doit l'honorer. 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 l'écrivain é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);
Étant donné que l'action ne référence aucune ressource externe, elle reste compatible avec PDF/A. Les noms que vous transmettez sont des titres de champ pleinement qualifiés, c'est pourquoi un champ enfant à l'intérieur d'un groupe doit être adressé via son chemin complet avec des points plutôt que par son simple nom de feuille
ImportData : pré-remplissage à partir d'un FDF
Là où l'action Hide réorganise ce qui est déjà sur la page, l'action import-data importe des valeurs de l'extérieur. La norme ISO 32000-1 §12.6.4.8, Tableau 198, la définit comme une action qui peuple l'AcroForm à partir d'un fichier Forms Data Format (FDF) sur le disque. C'est l'action derrière un contrôle de type "Recharger les données d'exemple" ou "Réinitialiser aux valeurs par défaut", où un fichier FDF est fourni à côté du PDF et contient les valeurs canoniques des champs. L'appel reflète les autres, prenant le rectangle de la zone réactive, 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 lorsque le PDF est construit, mais il doit être présent lorsque l'utilisateur clique, et tous les antislashs dans le chemin sont réécrits dans la forme de barre oblique canonique PDF pour vous
Il vaut la peine d'énoncer clairement une contrainte car elle constitue une surprise fréquente. Une action import-data pointe vers un fichier externe, elle n'est donc pas autorisée dans 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 échoue à la validation. Si votre pipeline vise une sortie archivistique, le pré-remplissage doit avoir lieu au moment de la génération en écrivant les valeurs des champs directement, et non en les reportant à un clic
JavaScript : packages globaux et scripts par action
Pour une logique qui va au-delà de l'affichage, du masquage et de l'importation, la famille d'actions s'étend au JavaScript au niveau du document. Il existe deux endroits distincts où un script peut résider, et la différence est importante. Un package JavaScript au niveau du document est stocké une fois pour l'ensemble du fichier et s'exécute lors de 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 seule ligne qui appelle une fonction déjà définie par le package
PDFlibPas expose les deux. AddGlobalJavaScript stocke un package nommé au niveau du document ; la réutilisation d'un nom remplace ce qui était stocké sous celui-ci. AddLinkToJavaScript attache un script à une zone réactive pour 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 corps sur chaque contrôle qui en a besoin, et cela signifie qu'un visualiseur avec les scripts désactivés ne fait tout simplement rien au clic plutôt que de bloquer sur un blob en ligne malformé. Cela permet également de conserver des entrées par action de petite taille, ce qui maintient le fichier lisible lorsque vous l'inspectez ultérieurement
Champs, champs enfants et gel du résultat
Les actions ont besoin de champs sur lesquels agir, il est donc utile de voir comment un champ voit le jour. NewFormField crée un champ sur la page courante et renvoie son index ; le type entier sélectionne le genre, où 1 est Texte, 2 est Bouton-poussoir, 3 est Case à cocher, 4 est Bouton radio, 5 est Choix, 6 est Signature, et 7 est un Parent qui possède des enfants mais ne dessine rien lui-même. Le titre que vous transmettez ne peut pas contenir de point, car le point est le séparateur dans les noms pleinement qualifiés que les actions utilisent pour adresser les enfants
Les groupes de boutons radio et les formulaires hiérarchiques sont construits en donnant 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 geler un champ afin que son apparence actuelle devienne un contenu permanent de la page, FlattenFormField dessine le champ sur la page et le retire du formulaire. Après un aplatissement (flatten), les index des champs ultérieurs sont décalés 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 d'aplatissement est commenté à dessein. Omettez-le et le document est livré sous forme de formulaire actif dont les actions se déclenchent dans le lecteur. Activez-le et le champ est rendu sous forme de marques statiques, ce qui est souhaitable lorsque le formulaire a été complété et que le résultat doit voyager en tant qu'enregistrement fixe. Le même champ, le même code, deux documents très différents selon que vous le gelez ou non
Choisir le bon verbe
Les quatre actions se divisent nettement selon ce qu'elles toucheent. Une action nommée déplace la fenêtre d'affichage et ne nécessite aucun champ. Une action Hide modifie la visibilité et nécessite des titres de champ, l'encodage chaîne-contre-tableau étant géré pour vous. Une action import-data accède à un fichier sur le disque et est donc interdite dans PDF/A. Une action JavaScript exécute une logique arbitraire et est mieux divisée entre un package global de fonctions et de petits appels par action. Optez pour celle qui fait le travail le plus simplement : une action Hide est plus portable qu'un script qui définit un indicateur de masquage, 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
D'ici, deux sujets voisins complètent le tableau. Si le formulaire fait partie d'un document accessible, l'arborescence de structure que les lecteurs d'écran parcourent est couverte dans notre article sur les PDF balisés et la structure d'accessibilité. Lorsque le formulaire complété doit être verrouillé et signé, le flux de travail est décrit dans le guide de conformité et de l'établi de signature. Tous les trois s'appuient sur le même moteur, qui est fourni en tant que bibliothèque PDF pour Delphi aux côtés des API de création, de formulaire et de signature couvertes ailleurs sur ce blog