Article technique

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

Le rapport de bug arrive avec une capture : « Votre outil a rempli le formulaire, mais chaque champ est vide dans Acrobat. Quand je clique dans un champ, la valeur apparaît soudain. » Les données sont dans le fichier, la capture le prouve même, et pourtant le formulaire semble vide pour tous ceux qui le reçoivent. C'est le défaut le plus courant du travail programmatique sur formulaires PDF, et ce n'est pas un bug de bibliothèque : c'est ce qui arrive quand les valeurs de champs sont écrites sans régénérer les apparences de champs. Comprendre pourquoi demande une section de la spécification PDF ; le corriger demande un seul appel de méthode. Les exemples utilisent PDFium Component, composant VCL/LCL basé sur PDFium pour Delphi, C++Builder et Lazarus, mais les mécaniques du format valent pour tout outillage AcroForm.

Un champ, deux représentations : /V et /AP

Un champ texte AcroForm stocke sa valeur dans l'entrée /V du dictionnaire de champ (ISO 32000-1 §12.7.3.3). Ce que les lecteurs peignent réellement est toutefois le flux d'apparence du widget, un petit flux de contenu pré-rendu stocké sous /AP (§12.5.5). Écrivez /V sans reconstruire /AP et les deux divergent : les données existent, l'image des données non. Acrobat repeint l'apparence d'un champ lorsque celui-ci reçoit le focus, d'où le fait que cliquer dans un champ « révèle » la valeur dans le bug ci-dessus.

L'échappatoire historique, le flag NeedAppearances qui demande aux lecteurs de régénérer eux-mêmes les apparences, n'a jamais été respectée de manière cohérente et est dépréciée dans PDF 2.0 (ISO 32000-2). Les pipelines d'impression et générateurs de miniatures ne l'ont jamais respectée du tout : ils peignent ce que /AP contient, c'est-à-dire rien. Le contrat fiable est donc : celui qui écrit la valeur reconstruit aussi l'apparence.

La génération d'apparence est aussi l'endroit où polices et alignements se font sentir. Un flux régénéré met en page la valeur dans le rectangle du widget avec la police, la taille et le quadding du champ ; une valeur qui tient dans votre formulaire de test peut donc se couper ou rétrécir dans une copie client plus étroite du même champ. Les champs auto-size (taille de police zéro) réduisent le texte pour le faire tenir ; les champs à taille fixe coupent. Les deux résultats sont légaux, et le seul moyen de savoir ce qu'un formulaire produit est d'inspecter la sortie régénérée plutôt que la valeur écrite.

Ouvrir un formulaire : FormFill, FormType et la question XFA

L'accès aux champs exige que le sous-système form-fill, contrôlé par FormFill, soit activé avant l'ouverture du document. Une fois actif, FormType indique le type de formulaire rencontré, et la réponse change les fonctions que vous pouvez promettre :

Pdf.FileName := FormPath;
Pdf.FormFill := True;   // enable before Active; required for any field access
Pdf.Active := True;

case Pdf.FormType of
  ftNone:
    DisableFormPanel('This document has no interactive form');
  ftAcroForm:
    BuildFieldList;     // full field navigation and editing available
  ftXfaFull:
    ShowXfaNotice;      // XFA renders from its own XML template;
                        // treat field editing as limited
end;

Deux notes pratiques. AcroForm est le modèle de formulaire standard ISO 32000 et celui ciblé par toutes les API de cet article ; les documents XFA embarquent leur propre architecture XML de formulaire, et promettre aux clients une édition XFA complète sur la base d'une démo AcroForm rapide est un engagement que vous regretterez. Ensuite, FormFill initialise aussi le JavaScript du document : souhaitable dans un visualiseur de saisie où les scripts de calcul gardent les totaux à jour, explicitement indésirable dans un aperçu de fichiers non fiables. L'article sur l'aperçu PDF sécurisé couvre le côté FormFill := False de cet arbitrage.

Une traversée clavier prévisible

Les utilisateurs de saisie vivent sur la touche Tab ; la traversée des champs doit donc se comporter comme tous les autres formulaires. La famille d'API de focus, FocusFormField, FocusNextFormField, FocusPreviousFormField, FocusedFormFieldIndex et ClearFormFieldFocus, déplace le focus de formulaire sans simuler la souris :

procedure TFormViewer.HandleTabKey(Shift: TShiftState);
begin
  if ssShift in Shift then
    PdfView.FocusPreviousFormField
  else
    PdfView.FocusNextFormField;
  UpdateFieldStatus;  // e.g. "Field 4 of 17: InvoiceDate"
end;

Connaissez le comportement aux limites : les appels de traversée suivent l'ordre de tabulation de la page courante et bouclent ; avancer après le dernier champ revient au premier, et les deux fonctions retournent le nouvel index de champ, ou -1 si la page n'a pas de champ. Passer à la page suivante est votre décision : détectez la boucle en comparant les indices et avancez PageNumber vous-même si l'objectif est une traversée document. Associez la traversée à OnFormFieldEnter et, côté visualiseur, OnFormFieldFocusChange, pour garder un panneau latéral synchronisé, et utilisez la propriété indexée FormFieldAt pour le hit-testing. Les utilisateurs de lecteurs d'écran bénéficient gratuitement de cette traversée : le focus suit l'ordre de champ propre au document, donc le chemin construit pour Tab est aussi celui des technologies d'assistance.

Pour les UI pilotées par métadonnées, FormFieldInfo[] retourne un enregistrement TPdfFormFieldInfo par index ; c'est ainsi que vous étiquetez les champs dans une liste de navigation au lieu d'afficher de simples numéros. Les groupes radio méritent ici un fichier de régression dédié : plusieurs widgets partagent un seul nom de champ, et une liste naïvement construite depuis les widgets affiche des doublons apparents qui troublent les utilisateurs.

La séquence remplir-enregistrer qui survit à Acrobat

Tout ce qui précède converge vers une séquence en trois étapes dont l'étape centrale est celle que les équipes sautent :

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];   // writes /V only

  // Rebuild the /AP appearance streams; without this the form
  // looks blank in Acrobat until each field is clicked
  Pdf.GenerateFormAppearances;

  Pdf.SaveAs(OutputPath);
end;

GenerateFormAppearances est toute la correction du rapport de bug initial. Il reconstruit les flux d'apparence des widgets depuis les valeurs, polices et quadding courants, de sorte que chaque lecteur, y compris ceux qui n'exécutent jamais d'événements de focus comme serveurs d'impression et thumbnailers, peint l'état rempli. Appelez-le une fois après le lot d'affectations plutôt qu'à chaque champ ; la génération d'apparence touche polices et mise en page, et les appels par champ multiplient le coût sur les grands formulaires sans bénéfice.

La vérification appartient à la définition du terminé : ouvrez le fichier enregistré dans Acrobat et confirmez que les valeurs sont visibles sans cliquer dans les champs, puis imprimez vers PDF ou image depuis un second lecteur et confirmez que les valeurs survivent à un pipeline qui ignore toute logique de formulaire. Ces deux contrôles capturent toutes les variantes de divergence /V contre /AP.

Formulaires de production qui cassent les implémentations propres

Courte liste de configurations qui passent les tests de démo et échouent avec les fichiers client :

  • Valeurs exportées des cases. L'état « on » n'est pas toujours Yes ; les formulaires définissent des valeurs exportées arbitraires, et écrire la mauvaise laisse la case visuellement décochée alors que votre code croit avoir réussi.
  • Groupes radio à nom partagé. Un champ, plusieurs widgets. L'affectation de valeur choisit quel widget s'affiche coché, et du code UI par widget supposant un nom pour un rectangle dessine le mauvais focus.
  • Champs calculés. Les totaux calculés par JavaScript de document se mettent à jour sur événements de champs. Un remplissage programmatique qui contourne les événements doit soit déclencher le recalcul, soit écraser explicitement les champs calculés ; livrer un formulaire où lignes et total divergent est pire que les deux options.
  • Champs requis masqués. Les formulaires conditionnels cachent des champs qui restent marqués requis. Décidez si votre validation honore la visibilité ou le flag brut, et documentez la décision où le support peut la trouver.

FAQ

Pourquoi mes valeurs remplies n'apparaissent-elles que lorsqu'un champ est cliqué ?

Les valeurs ont été écrites dans /V mais les flux d'apparence /AP n'ont jamais été régénérés ; les lecteurs peignent donc l'apparence obsolète et vide jusqu'à ce qu'un événement de focus force la reconstruction. Appelez GenerateFormAppearances après les affectations et avant SaveAs.

La navigation de champs fonctionne-t-elle sur les formulaires XFA ?

Vérifiez d'abord FormType. ftAcroForm donne toute la surface de navigation et d'édition décrite ici ; ftXfaFull signifie que le document se rend depuis son propre modèle XML et que l'interaction champ à champ est limitée. Détectez-le et affichez un message plutôt que de laisser les utilisateurs le découvrir.

Le flattening est-il la même chose que la génération d'apparences ?

Non. GenerateFormAppearances garde les champs interactifs tout en rendant leurs valeurs visibles partout. Le flattening convertit l'apparence en contenu de page statique et supprime définitivement l'interactivité : juste pour une sortie d'archive, mauvais pour un formulaire que la personne suivante doit éditer.

Le sous-système form-fill, la traversée de focus et la génération d'apparence montrés ici font partie de PDFium Component pour Delphi, C++Builder et Lazarus/FPC. Si votre visualiseur gère aussi les annotations de revue avec les données de formulaire, l'article sur la revue d'annotations couvre ce modèle voisin.