Article technique

Formulaires PDF Interactifs dans Delphi : Actions et JavaScript

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 PDF Library for Delphi 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

Diagramme PDF Library for Delphi d'une annotation lien ou widget PDF référençant un dictionnaire d'action qui s'éventaille vers les verbes d'action named, hide, import-data et JavaScript, la carte import-data étant marquée exclue du PDF/A
Un seul dictionnaire d'action derrière un lien ou un widget alimente quatre verbes distincts — déplacement de la vue, visibilité, import depuis le disque et script

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 PDF Library for Delphi, 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
// Le bit 0 d'Options (valeur 1) dessine une bordure autour du 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);   // saute à la dernière 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. PDF Library for Delphi 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

Diagramme PDF Library for Delphi des règles d'encodage de l'action Hide PDF où exactement un nom de champ devient une chaîne de texte /T tandis que deux noms ou plus deviennent un tableau de chaînes, avec notes sur le sens de l'indicateur hide, la sécurité PDF/A et les titres de champs pleinement qualifiés
Le rédacteur se branche sur le nombre de champs si bien qu'un nom isolé part en chaîne brute que les anciens lecteurs honorent, tandis que deux noms ou plus deviennent un tableau basculé ensemble
// HideFlag non nul masque les champs listés (/H true) ; zéro les affiche
// Un seul nom -> /T est une text string. Deux ou plus -> /T est un tableau de 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

PDF Library for Delphi 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

Diagramme PDF Library for Delphi du modèle JavaScript PDF à deux niveaux où un paquet global au niveau document définit recalcTotal à l'ouverture du document et chaque lien porte un script d'une ligne par action qui l'appelle au clic
Définissez recalcTotal une seule fois dans le package de niveau document et laissez chaque zone réactive cliquable l'atteindre par un appel d'une ligne par action
// Package au niveau document : on définit une fois une fonction réutilisable
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);' +
  '}');

// Script par action sur un lien : on appelle juste la fonction partagée
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;

    // Un champ texte que l'action Hide ciblera par son titre
    FldShip := Pdf.NewFormField('ShippingAddress', 1);
    Pdf.SetFormFieldBounds(FldShip, 40, 120, 240, 20);
    Pdf.SetFormFieldValue(FldShip, '');

    // Branche un lien Hide et un lien de navigation sur cette 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

    // Un script au niveau document, disponible pour tous les events du fichier
    Pdf.AddGlobalJavaScript('OnOpen',
      'app.alert("Form ready", 3);');

    // Fige le champ si la sortie ne doit plus être éditable
    // 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