TPdf.SetFocusedFormFieldText dans le composant PDFium écrit dans le tampon d'édition vivant du champ de formulaire actuellement ciblé, et pour un formulaire XFA ce tampon n'atteint jamais le paquet datasets qui est sérialisé sur disque — si bien qu'une valeur qu'un utilisateur tape, et dont votre code confirme qu'elle a été acceptée, a silencieusement disparu la prochaine fois que le fichier s'ouvre. Les champs AcroForm n'ont pas ce problème : le même appel valide dans l'entrée /V du champ au moment où le focus s'en va. Un utilisateur qui remplit un formulaire d'admission XFA, enregistre, et rouvre pour trouver le champ montant à nouveau vide ne rencontre pas un défaut de rendu — il touche la limite de ce que le moteur PDFium lui-même expose pour écrire des données de formulaire
C'est une question plus étroite que détecter un formulaire XFA en premier lieu, ou faire fonctionner son JavaScript : pas « PDFium prend-il en charge XFA » et pas « comment exécuter des scripts AcroForm » mais spécifiquement ce qui arrive à une valeur après que SetFocusedFormFieldText signale un succès. La version courte est qu'AcroForm et XFA ne sont pas deux dialectes du même modèle de formulaire en ce qui concerne le chemin d'écriture de PDFium — ce sont deux modèles de formulaire avec deux relations complètement différentes entre ce qu'un utilisateur tape et ce qu'un enregistrement capture réellement, et confondre les deux est ce qui transforme un appel d'API d'une ligne en un ticket de support trois semaines après le lancement du pilote d'un client. L'article sur JavaScript AcroForm montre l'appel d'une ligne et énonce le résultat AcroForm-contre-XFA dans un commentaire de code ; celui-ci reste sur cette même API et parcourt le chemin d'écriture interne, la preuve par le paquet datasets que l'écriture XFA n'atterrit jamais, pourquoi l'écart se trouve dans PDFium lui-même plutôt que dans la liaison Delphi, et une solution de contournement de correction de votre propre XML pour les documents ayant besoin que la modification survive à un enregistrement
Comment SetFocusedFormFieldText écrit-il une valeur de champ ?
TPdf.SetFocusedFormFieldText fonctionne en simulant une modification au niveau de la frappe, pas en injectant directement une valeur dans le modèle de document. En interne, il appelle FORM_SelectAllText pour sélectionner le contenu actuel du champ ciblé, puis FORM_ReplaceSelection pour écraser la sélection avec la nouvelle chaîne — les deux mêmes opérations qu'un sélectionner-tout-et-taper piloté par clavier déclencherait. Comme l'écriture passe par le chemin d'édition de texte interactif de PDFium plutôt que de le contourner, tout script de frappe, de format, ou de calcul lié au champ se déclenche exactement comme il le ferait pour un humain en train de taper, ce qui rend l'API utile pour le remplissage de formulaire programmatique dans une visionneuse qui garde JavaScript actif. La contrepartie côté lecture est FocusedFormFieldText, adossée à FORM_GetFocusedText, et elle reflète le même tampon vivant que SetFocusedFormFieldText vient d'écrire
if Pdf.FocusedFormFieldIndex >= 0 then
begin
if Pdf.SetFocusedFormFieldText('1284.50') then
Log('Buffer now reads: ' + Pdf.FocusedFormFieldText)
else
Log('No field is focused, or it does not accept text');
end
else
Log('Focus a field first - FocusFormField or a real click');
Pourquoi AcroForm conserve-t-il la valeur et XFA la perd-il ?
Les champs de texte et combo AcroForm persistent car le propre environnement de remplissage de formulaire de PDFium valide le tampon d'édition pour vous : à l'instant où le champ perd le focus, le tampon est écrit dans l'entrée /V du champ, la même clé que tout lecteur PDF conforme regarde pour connaître la valeur stockée d'un champ. TPdf.ClearFormFieldFocus — qui appelle FORM_ForceToKillFocus en arrière-plan — force cette validation à la demande, si bien que le code qui définit une valeur programmatiquement n'a pas à attendre un véritable clic de souris ailleurs dans l'interface. Enregistrez immédiatement après, et le nouveau texte fait partie du graphe d'objets du document avant même que TPdf.SaveAs ne s'exécute, car /V est une véritable entrée dans un véritable dictionnaire de champ, pas quelque chose ajouté après coup
Pdf.FocusFormField(FieldIndex);
Pdf.SetFocusedFormFieldText('1284.50');
Pdf.ClearFormFieldFocus; // forces the /V commit now
Pdf.SaveAs('invoice-acroform.pdf');
// Reopen and confirm - this is an AcroForm document, so it holds
Pdf.Active := False;
Pdf.FileName := 'invoice-acroform.pdf';
Pdf.Active := True;
Pdf.FocusFormField(FieldIndex);
Assert(Pdf.FocusedFormFieldValue = '1284.50'); // passes
Où vit réellement une modification de champ XFA ?
Les champs XFA n'ont pas ce câblage. Le texte qu'un utilisateur tape atterrit dans un tampon CPWL_Edit qui appartient à la couche de rendu et d'interaction XFA de PDFium, et cette couche n'a aucun chemin de code qui recopie le tampon dans le paquet datasets stocké dans le PDF. TPdf.GetXfaDatasets rend l'écart visible : appelez-le avant et après une modification sur un champ XFA et les octets qu'il renvoie sont identiques, car la méthode lit le paquet original avec lequel le document a été ouvert, jamais l'état vivant du widget que vous venez d'éditer. Rien de tout cela n'est un bogue de cache ou un problème de synchronisation de rafraîchissement — le paquet datasets sur disque et le tampon d'édition en mémoire sont simplement deux morceaux d'état différents que l'API publique de PDFium ne connecte jamais
var
Before, After: TBytes;
begin
Before := Pdf.GetXfaDatasets;
Pdf.FocusFormField(FieldIndex);
Pdf.SetFocusedFormFieldText('1284.50');
After := Pdf.GetXfaDatasets;
// Before and After are byte-for-byte identical on an XFA document -
// the edit never touched the packet GetXfaDatasets reads from
end;
Est-ce un bogue du composant PDFium ou une limitation de PDFium ?
La pièce manquante se trouve dans PDFium lui-même, pas dans la liaison Delphi par-dessus. L'API publique de PDFium n'a pas de FPDF_SetXFAPacket pour injecter un paquet mis à jour et pas de FPDF_SaveAsXFA pour demander au moteur XFA de sérialiser son DOM actuel en XML datasets avant un enregistrement. FPDF_SaveAsCopy — l'export qui soutient TPdf.SaveAs — écrit le graphe d'objets de document que PDFium possède déjà ; il n'a aucun crochet pour demander au moteur XFA de purger d'abord son état vivant, car ce crochet n'existe pas en amont. Le composant PDFium ne peut pas ajouter une réconciliation que PDFium lui-même n'a jamais implémentée, et livrer un sérialiseur DOM-vers-XML maison qui devine l'état XFA interne de PDFium serait pire que l'écart honnête : cela aurait l'air de fonctionner jusqu'à ce que la prochaine version de PDFium change quelque chose que personne en dehors du projet ne peut voir
Cette limite est apparue lors du même audit v2.13.2 qui a construit SetFocusedFormFieldText en premier lieu. FORM_ReplaceSelection avait été liée dans la table d'import DLL depuis des versions sans jamais être appelée depuis du code Pascal, et ajouter le chemin d'écriture qui l'a finalement utilisée est ce qui a rendu l'écart de persistance suffisamment concret pour être documenté plutôt que théorique. Le même cycle d'audit a révélé un écart sans rapport mais apparenté dans l'esprit : JavaScript AcroForm avait été silencieusement désactivé depuis la v2.13.0 car la plateforme JS n'était câblée qu'à l'intérieur de la branche d'initialisation XFA, si bien que les documents AcroForm ordinaires avec app.alert ou des champs calculés n'obtenaient jamais de moteur de script du tout. Celui-là était corrigeable — étendre la plateforme JS à chaque document indépendamment de XFA — et il a été livré dans la même version ; l'écart de persistance couvert ici ne l'était pas, pour les raisons ci-dessus. La correction JavaScript et les événements de veto hôte qui l'entourent sont couverts dans exécuter JavaScript AcroForm avec le composant PDFium
Que devriez-vous faire à ce sujet en Delphi ?
Pour les documents AcroForm, la correction n'est rien de plus qu'une bonne habitude : appelez ClearFormFieldFocus (ou déplacez autrement le focus) avant SaveAs chaque fois qu'une valeur a été définie programmatiquement, plutôt que de supposer qu'une interaction UI ultérieure déclenchera la validation pour vous. Pour un document qui pourrait être soit AcroForm soit XFA — ce qui est le cas courant dans une visionneuse à usage général — vérifiez FormType ou le booléen XFA avant de promettre à un appelant qu'un enregistrement tiendra, et lisez détecter les formulaires XFA et extraire les paquets XFA pour l'ensemble complet des sondes, y compris le cas XFAF où du contenu XFA est superposé à des widgets AcroForm par ailleurs ordinaires qui honorent bien /V
Pour un véritable formulaire XFA dynamique où les valeurs éditées doivent survivre à un enregistrement, le tampon d'édition interactif n'est pas du tout le bon outil. Le chemin durable consiste à traiter GetXfaDatasets comme votre référence de base, pas votre résultat : lisez-le une fois à l'ouverture du document, gardez votre propre trace de ce que l'utilisateur a changé champ par champ — exactement les valeurs que votre interface possède déjà, puisque PDFium ne vous les rendra pas après coup — appliquez ces correctifs vous-même dans le XML de référence, et pilotez votre propre sortie. Une écriture qui passe par du XML que votre propre code contrôle survit à un enregistrement qu'un tampon CPWL_Edit ne pourrait jamais survivre
function ExportEditedXfaValue(Pdf: TPdf; const FieldPath,
NewValue: string): TBytes;
var
DatasetsXml: string;
begin
// GetXfaDatasets ships with PDFium Component; PatchXmlNode below is
// your own helper over your own XML library, nothing PDFium provides
DatasetsXml := TEncoding.UTF8.GetString(Pdf.GetXfaDatasets);
DatasetsXml := PatchXmlNode(DatasetsXml, FieldPath, NewValue);
Result := TEncoding.UTF8.GetBytes(DatasetsXml);
end;
Détecter l'écart avant qu'un client ne le fasse
TPdf.SaveAs renvoie True que la valeur de champ XFA ait survécu ou non, car du point de vue de PDFium, l'enregistrement a véritablement réussi — il a écrit chaque octet qu'on lui a demandé d'écrire. Cela fait de ceci exactement le genre de défaut qui passe à travers un test de fumée et atteint un client : rien ne lève d'exception, rien ne se journalise, le fichier s'ouvre bien, seule la valeur spécifique est fausse. Un test d'aller-retour qui rouvre réellement le fichier enregistré et compare la valeur du champ — ou compare GetXfaDatasets avant et après, selon l'exemple précédent — a sa place dans la suite de régression de toute visionneuse qui laisse les utilisateurs éditer du contenu XFA, pas seulement les chemins AcroForm qui fonctionnent par défaut
Rien de tout cela n'est un défaut à signaler contre le composant PDFium, plutôt une limite autour de laquelle concevoir : SetFocusedFormFieldText fait précisément ce que son nom indique pour les deux modèles de formulaire, et la différence de résultat se retrace proprement à ce à quoi AcroForm et XFA relient chacun ce tampon côté PDFium. L'API, les primitives de focus et d'enregistrement, et les lecteurs de paquet référencés ici font partie du composant PDFium pour Delphi et C++Builder