Article technique

Navigation dans les champs de formulaire PDF dans Delphi (Composant PDFium)

Appuyez sur Tab dans un formulaire PDF généré par votre code, et le curseur atterrit à deux champs de l'endroit où il devrait se trouver, ignore complètement la deuxième colonne, ou saute de nouveau en haut après le troisième champ au lieu du quatrième. La personne qui remplit une facture dans votre visionneuse s'attend à ce que le clavier parcoure le formulaire de la même manière qu'il parcoure tous les formulaires Web qu'elle a jamais utilisés. Lorsque ce n'est pas le cas, elle attrape la souris, part à la recherche de la case suivante, et décide discrètement que votre outil n'est pas terminé. Un parcours de champ prévisible fait la différence entre une visionneuse de saisie de données que les gens tolèrent et une en laquelle ils ont confiance, et il s'agit presque entièrement d'utiliser la bonne API de focus (focus API) au lieu de simuler la saisie au clavier par des clics simulés

Les exemples ci-dessous utilisent le Composant PDFium, un composant VCL/LCL basé sur PDFium pour Delphi, C++Builder et Lazarus. La navigation est l'une des trois choses qu'une visionneuse de formulaires doit réussir ; les deux autres, ouvrir le formulaire correctement et enregistrer les valeurs remplies afin qu'elles s'affichent réellement, sont celles où se cachent la plupart des surprises, les trois sont donc couvertes ci-dessous

Ouverture d'un formulaire : FormFill, FormType et la question XFA

L'accès aux champs nécessite que le sous-système de remplissage de formulaires, contrôlé par la propriété FormFill, soit activé avant l'ouverture du document. Une fois actif, FormType vous indique à quel type de formulaire vous êtes confronté, et la réponse modifie l'ensemble de fonctionnalités que vous pouvez promettre :

Pdf.FileName := FormPath;
Pdf.FormFill := True;   // activer avant Active ; requis pour tout accès aux champs
Pdf.Active := True;

case Pdf.FormType of
  ftNone:
    DisableFormPanel('Ce document ne contient pas de formulaire interactif');
  ftAcroForm:
    BuildFieldList;     // navigation complète dans les champs et édition disponibles
  ftXfaFull:
    ShowXfaNotice;      // XFA s'affiche à partir de son propre modèle XML ;
                        // traiter l'édition de champ comme limitée
end;

Deux remarques pratiques découlent de ce choix (switch). AcroForm est le modèle de formulaire de la norme ISO 32000 standard, et c'est ce que cible chaque API ici. Les documents XFA intègrent leur propre architecture de formulaire XML, donc promettre à un client une édition XFA complète après une rapide démonstration d'AcroForm est un engagement que vous regretterez. La deuxième remarque concerne les effets secondaires : définir FormFill sur True initialise également le JavaScript du document. Dans une visionneuse de saisie de données, c'est tout à fait approprié, car ce sont les scripts de calcul qui maintiennent à jour un total cumulé au fur et à mesure que quelqu'un tape. Dans une fenêtre de prévisualisation de fichiers d'origine inconnue, c'est tout à fait inapproprié. L'article sur l'aperçu PDF sécurisé couvre le côté FormFill := False de ce compromis

Le parcours avec la touche de tabulation (Tab) qui atterrit là où les utilisateurs l'attendent

Revenons au problème de clavier depuis le début. La tentation est de simuler Tab en synthétisant un clic de souris sur le rectangle du widget suivant, ce qui échoue dès qu'un champ défile hors de l'écran ou que deux widgets se chevauchent. L'API de focus déplace directement le propre focus du formulaire à la place, sans aucune estimation géométrique (geometry guesswork). Cinq appels le couvrent : FocusFormField par index, FocusNextFormField et FocusPreviousFormField pour le parcours, FocusedFormFieldIndex pour lire où vous en êtes, et ClearFormFieldFocus pour abandonner complètement le focus

procedure TFormViewer.HandleTabKey(Shift: TShiftState);
begin
  if ssShift in Shift then
    PdfView.FocusPreviousFormField
  else
    PdfView.FocusNextFormField;
  UpdateFieldStatus;  // par ex. "Champ 4 de 17 : InvoiceDate"
end;

Le comportement qui pose problème aux gens est le bouclage (wrap). Le parcours fonctionne à travers l'ordre de tabulation de la page en cours et boucle en son sein : dépassez le dernier champ et vous êtes de retour au premier. Les deux fonctions d'avancement renvoient le nouvel index de champ, ou -1 lorsque la page ne contient aucun champ du tout. Ce bouclage se fait par page, pas par document, ce qui signifie que passer à la page suivante est votre tâche, pas celle de la bibliothèque. Comparez l'index renvoyé à celui dont vous êtes parti, remarquez quand il a bouclé et avancez PageNumber vous-même si le formulaire est censé se lire comme une séquence continue. Omettez cette vérification et un formulaire de deux pages piège silencieusement le curseur sur la première page, ce qui est sa propre variante de la plainte concernant la touche Tab cassée

Le parcours devient utile une fois que le reste de l'interface utilisateur y réagit. L'événement OnFormFieldEnter se déclenche à l'arrivée du focus, et sur la visionneuse, OnFormFieldFocusChange signale le nouvel index de champ, de sorte qu'un panneau latéral peut rester synchronisé avec ce que le clavier vient de sélectionner. Lorsque vous avez besoin du mappage inverse, d'une position d'écran vers un champ, la propriété indexée FormFieldAt effectue le test d'impact (hit-testing) pour les aperçus d'info-bulles (tooltip previews) et les panneaux de clic-pour-éditer (click-to-edit). Il y a un avantage d'accessibilité discret dans tout cela : le focus suivant le propre ordre des champs du document, le chemin que vous câblez pour la touche Tab est le même que celui qu'un lecteur d'écran annonce, sans travail supplémentaire

Afficher des noms de champs au lieu de simples numéros d'index nécessite une propriété supplémentaire. FormFieldInfo[] renvoie un enregistrement TPdfFormFieldInfo par index, contenant le nom du champ, son type, la taille de la police, son état coché, sa valeur d'exportation et son appartenance à un groupe, ce qu'une liste de navigation devrait afficher ("Champ 4 de 17 : InvoiceDate" plutôt que "4"). Les groupes de boutons radio (radio groups) constituent le cas valant un fichier de test dédié. Plusieurs widgets peuvent partager un seul nom de champ, de sorte qu'une liste assemblée naïvement à partir de widgets affiche le même groupe plusieurs fois et sème la confusion chez tous ceux qui la lisent

Pourquoi les valeurs remplies ressortent vides et l'appel qui y remédie

L'autre plainte qui remplit les files d'attente d'assistance est plus alarmante qu'une touche Tab capricieuse : un formulaire est rempli par programmation, le client l'ouvre dans Acrobat et tous les champs semblent vides. Cliquez dans un champ et sa valeur apparaît. Les données sont dans le fichier depuis le début. Ce qui manque, c'est l'image des données, et il est utile d'en comprendre la raison une fois pour toutes car elle explique toute une famille de bogues

Un champ de texte AcroForm stocke sa valeur dans l'entrée /V du dictionnaire de champs (ISO 32000-1 §12.7.3.3). Ce qu'une visionneuse peint réellement est quelque chose de distinct : le flux d'apparence (appearance stream) du widget sous /AP (§12.5.5), un petit extrait de contenu pré-rendu. Écrivez /V et laissez /AP de côté, et les deux divergent. La valeur est là ; la version rendue de celle-ci est périmée (stale) ou absente. Acrobat reconstruit l'apparence d'un champ lorsqu'il gagne le focus, ce qui est l'explication complète des valeurs qui n'apparaissent qu'au clic. L'ancien indicateur NeedAppearances, qui demandait aux visionneuses de régénérer les apparences pour vous, n'a jamais fonctionné uniformément et est obsolète (deprecated) dans PDF 2.0, et les serveurs d'impression et les générateurs de miniatures l'ignorent complètement. Ils peignent /AP et rien d'autre, donc si /AP est vide, ils impriment une case vide

L'affectation d'une valeur via FormField[i] écrit uniquement /V. C'est pourquoi le remplissage d'un formulaire est une séquence en trois étapes, et l'étape que les équipes omettent est celle du milieu :

procedure TFormViewer.FillAndSave(const Values: array of WString;
  const OutputPath: string);
var
  i: Integer;
begin
  for i := 0 to Pdf.FormFieldCount - 1 do
    Pdf.FormField[i] := Values[i];   // écrit /V uniquement

  // Reconstruire les flux d'apparence /AP ; sans cela, le formulaire
  // parait vide dans Acrobat jusqu'à ce que chaque champ soit cliqué
  Pdf.GenerateFormAppearances;

  Pdf.SaveAs(OutputPath);
end;

GenerateFormAppearances est le correctif complet. Il reconstruit le flux d'apparence de chaque widget à partir des valeurs actuelles, des polices et de l'alignement (quadding), de sorte qu'une visionneuse qui n'exécute jamais d'événement de focus, un serveur d'impression ou un générateur de miniatures, peint l'état rempli de toute façon. Appelez-le une fois après le lot d'affectations, et non une fois par champ. La génération d'apparence effectue un véritable travail de mise en page, et les appels par champ multiplient cela sur un grand formulaire pour rien

La régénération des apparences est également le moment où les polices et l'alignement s'affirment, ce qui est la source d'une surprise de second ordre. Le nouveau flux dispose chaque valeur à l'intérieur du rectangle du widget en utilisant la police, la taille et l'alignement du champ. Une valeur qui se place confortablement dans votre formulaire de test peut être tronquée (clip) ou se rétrécir (shrink) dans la copie d'un client où le même champ est plus étroit. Les champs à redimensionnement automatique (taille de police zéro) rétrécissent le texte pour l'adapter ; les champs à taille fixe se contentent de le tronquer. Les deux sont légaux, et la seule façon honnête de savoir ce que fait un formulaire donné est de regarder la sortie régénérée plutôt que la chaîne que vous avez écrite. Lorsque quelqu'un signale que le texte est coupé au bord d'une case, c'est presque toujours la raison

Traitez la vérification comme une partie de la finition du travail, pas comme une réflexion après coup (afterthought). Ouvrez le fichier enregistré dans Acrobat et confirmez que les valeurs sont visibles avant de toucher à un champ quelconque. Imprimez-le ensuite en PDF ou dans une image à partir d'une visionneuse différente, une qui ignore complètement la logique des formulaires, et confirmez que les valeurs survivent également à ce chemin. Entre eux, ces deux contrôles détectent toutes les variantes de la dérive /V-vers-/AP

Configurations de champs qui passent la démonstration et échouent sur le terrain

Les formulaires de démonstration propres cachent un ensemble de cas marginaux (edge cases) que les fichiers clients ne cachent pas. Quatre d'entre eux expliquent la plupart des rapports "ça a marché sur ma machine"

  • Valeurs d'exportation des cases à cocher. L'état "activé" (on) n'est pas toujours Yes. Un formulaire est libre de définir sa propre valeur d'exportation, et écrire la mauvaise chaîne laisse la case visuellement non cochée tandis que votre code est convaincu de l'avoir activée. Lisez la valeur d'exportation de FormFieldInfo[] plutôt que d'en supposer une
  • Groupes de boutons radio à nom partagé. Un seul champ, plusieurs widgets. La valeur que vous attribuez décide de quel widget est lu comme sélectionné, de sorte qu'un code d'interface utilisateur qui suppose qu'un nom correspond à un rectangle finit par dessiner l'anneau de focus sur le mauvais bouton
  • Champs calculés. Les totaux maintenus par le JavaScript du document se mettent à jour en réponse aux événements de champ. Un remplissage par programmation qui contourne ces événements doit soit déclencher un nouveau calcul, soit écraser directement les champs calculés. Un formulaire où les lignes individuelles (line items) et le total sont en désaccord est pire que n'importe laquelle des deux solutions
  • Champs obligatoires masqués. Les formulaires conditionnels masquent des champs qui sont toujours marqués comme obligatoires. Décidez d'emblée si votre validation respecte la visibilité ou l'indicateur d'obligation brut, puis notez cette décision quelque part où l'assistance pourra la trouver

Une distinction mérite d'être établie avant qu'elle ne vous pose problème : générer des apparences n'est pas aplatir (flattening). GenerateFormAppearances rend les valeurs visibles partout tout en laissant les champs modifiables. L'aplatissement intègre (bakes) l'apparence dans un contenu de page statique et supprime définitivement l'interactivité, ce qui est approprié pour une copie d'archivage et inapproprié pour un formulaire que la personne suivante doit encore remplir. Si FormType renvoie ftXfaFull plutôt que ftAcroForm, aucune des surfaces d'édition présentées ici ne s'applique proprement de toute façon, puisque le document s'affiche à partir de son propre modèle XML ; détectez ce cas et informez-en l'utilisateur, plutôt que de le laisser trouver la limite par lui-même

Le sous-système de remplissage de formulaires, le parcours du focus et la génération d'apparence présentés ici font partie du Composant PDFium pour Delphi, C++Builder et Lazarus/FPC. Si votre visionneuse gère également les annotations (markup) de relecteur en plus des données de formulaire, l'article sur la révision des annotations couvre ce modèle adjacent