Une valeur de champ de formulaire PDF « revient en arrière » quand le champ est stocké comme dictionnaire direct : PDFium promeut la copie dans le tableau /Annots de la page en objet indirect, tandis que /Fields garde une seconde copie directe. PDFium Component pour Delphi gère les écritures de champs typées en unifiant les deux références en un seul propriétaire indirect avant d'écrire
Le symptôme a d'ordinaire des airs de maison hantée. L'écriture renvoie True, et la lecture suivante très exactement rend l'ancienne valeur. Sauvegardez, rouvrez, et l'ancienne valeur est toujours là. Rien n'a levé, et le PDF a l'air parfaitement ordinaire dans chaque viewer que vous possédez
Le symptôme : une écriture réussie qui ne tient pas
Le schéma d'échec est étroit mais très reproductible : il ne surgit que sur des formulaires dont les dictionnaires de champs sont écrits inline (comme objets directs) plutôt que comme objets indirects numérotés. La plupart des outils d'auteurs émettent des champs indirects, donc le problème se cache pendant des mois puis apparaît sur des fichiers d'un générateur particulier, souvent un exporteur artisanal qui écrit tout le /AcroForm inline
Deux observations ensemble pointent la cause : l'écriture rapporte un succès, et une lecture native du champ à travers la couche de formulaire renvoie la valeur précédente. Quand vous voyez les deux, arrêtez de soupçonner votre encodage de chaînes et commencez à regarder la possession d'objets
Pourquoi une valeur de champ de formulaire PDF revient-elle après une écriture réussie ?
Une valeur de champ de formulaire PDF revient en arrière parce que deux dictionnaires séparés prétendent tous deux être le même champ, et l'écriture a mis à jour la copie que l'arbre de champs ne consulte pas. La valeur d'un champ vit dans son entrée /V, et l'ISO 32000-1 §12.7.3.1 définit où cette entrée fait autorité : sur le dictionnaire de champ atteint depuis /AcroForm /Fields, en descendant par les /Kids vers le champ terminal, avec les entrées héritables résolues depuis /Parent
La même clause autorise une commodité qui cause tout le trouble. Quand un champ a exactement une annotation widget, le dictionnaire de champ et le dictionnaire d'annotation widget peuvent être fusionnés en un seul dictionnaire. Dans un fichier bien formé, ce dictionnaire fusionné est un objet indirect, référencé depuis deux endroits : depuis /Fields (ou les /Kids d'un parent) et depuis le tableau /Annots de la page. Deux références, un objet, un /V
Deux copies d'un même dictionnaire
L'ISO 32000-1 §7.3.10 est explicite : seul un objet indirect possède une identité partageable. Un dictionnaire direct n'a pas de numéro d'objet, donc quand un générateur écrit le dictionnaire champ/widget fusionné inline à la fois dans /Fields et dans /Annots, le fichier ne contient pas un objet référencé deux fois. Il contient deux dictionnaires d'aspect identique qui ont par hasard les mêmes clés et valeurs
PDFium tolère ça, et ce faisant rend la scission permanente. Quand il a besoin d'un handle vers l'annotation, PDFium peut promouvoir le dictionnaire widget direct du tableau /Annots de la page en objet indirect, mais la copie directe équivalente reste où elle était, dans /AcroForm /Fields (ou dans les /Kids du parent). À partir de ce moment, le document détient deux propriétaires pour un champ logique. Un writer qui édite le /V de l'annotation de page obtient un succès authentique au niveau du dictionnaire. L'arbre de champs, que la couche de formulaire et tout lecteur ultérieur traitent comme la source de vérité, ne voit jamais le changement
Voilà pourquoi la valeur périmée survit à une sauvegarde et réouverture : la nouvelle valeur a été sérialisée fidèlement dans un objet que la hiérarchie de champs ne possède pas
Comment PDFium Component restaure un propriétaire de champ unique
PDFium Component normalise la possession des champs directs avant chaque écriture typée : il promeut les dictionnaires /AcroForm et de champs directs en objets indirects, lie chaque widget direct à son annotation de page correspondante, et réécrit /Fields, /Kids, /Parent et /Annots pour que chaque référence pointe le même objet indirect. Ce n'est qu'alors qu'il change le /V, pour que la valeur du champ, le widget sélectionné et le document sauvegardé partagent un seul propriétaire
La partie dure est l'identité. Deux dictionnaires directs de contenu identique ne prouvent pas qu'ils sont le même widget ; un formulaire peut légitimement contenir deux widgets qui se ressemblent. Le normalisateur refuse donc de deviner, et exige un nom d'annotation /NM explicite et unique (ISO 32000-1 §12.5.2) sur chaque widget direct. Les règles appliquées par le chemin d'écriture sont strictes :
- Un widget direct exige un
/NMnon vide d'au plus 1024 unités UTF-16, et exactement une annotation de page peut porter le même nom - La copie directe et l'annotation de page doivent se sérialiser à l'identique sauf
/Parentet le/APrégénéré par PDFium ; toute autre différence refuse l'écriture - Les deux liens parents sont vérifiés séparément, pour qu'un
/Parentcorrect sur l'annotation de page ne puisse pas masquer un conflictuel sur la copie directe d'origine - Un
/Parentmanquant est fourni seulement quand une possession unique de l'arbre de champs rend la réponse non ambiguë, et les copies parentes directes équivalentes sont re-liées à leur propriétaire promu - Les noms de champs complets en doublon, les arbres de champs cycliques ou à possession multiple et les champs de signature se refusent avant que quoi que ce soit ne soit publié
- Les annotations sans rapport, comme les liens sur la même page, et les entrées
/AcroFormsans rapport sont conservées intactes
Le normalisateur tourne aussi sous budgets durs : 4096 visites d'arbre de champs, pages ou membres de tableau, profondeur d'imbrication 64, 256 nouveaux objets indirects, 1 MiB par dictionnaire promu, 4 MiB d'octets promus au total, 128 MiB d'octets de dictionnaires inspectés, et 4096 unités UTF-16 par nom de champ complet avec 262144 sur tout le formulaire. Un fichier hostile ou cassé bute sur un budget et échoue avec un message d'erreur au lieu de transformer un remplissage en déni de service
Toute l'opération est transactionnelle. TPdf.SetFormFieldValues capture un instantané, construit une mise à jour incrémentale contenant les objets normalisés et les nouvelles valeurs, charge ce candidat, et ne remplace le document vivant qu'une fois le candidat vérifié. Si la normalisation refuse le fichier, ou qu'une requête ultérieure d'un lot échoue, le document d'origine, ses valeurs et son curseur logique de champs restent exactement tels quels
Écrire et vérifier une valeur de champ direct en Delphi
Le schéma sûr est écrire, puis relire à travers la couche de formulaire native, et traiter une discordance comme un échec même quand l'écriture a renvoyé True. TPdf.SetFormFieldValue adresse un champ par index sur la page courante, prend une TPdfFormValue typée et remplit un TPdfFormValueWriteReport ; TPdf.FormField[Index] relit la valeur à travers la couche de formulaire de PDFium, qui résout l'arbre de champs au lieu de faire confiance au widget
uses
System.SysUtils, PDFium;
procedure WriteTextField(Pdf: TPdf; PageNo, FieldIndex: Integer;
const NewText: WString);
var
Value: TPdfFormValue;
Report: TPdfFormValueWriteReport;
begin
Pdf.PageNumber := PageNo; // SetFormFieldValue vise la page courante
Value := Default(TPdfFormValue);
Value.Kind := fvkText;
Value.Text := NewText;
if not Pdf.SetFormFieldValue(FieldIndex, Value, Report) then
raise Exception.CreateFmt('Field %d on page %d rejected: %s',
[FieldIndex, PageNo, Report.ErrorMessage]);
// Vérifiez contre l'arbre de champs, pas contre notre propre variable
if Pdf.FormField[FieldIndex] <> NewText then
raise Exception.CreateFmt('Field "%s" did not keep the new value',
[Pdf.FormFieldInfo[FieldIndex].Name]);
end;
Sur un fichier à possession insoluble, par exemple un widget direct sans /NM, SetFormFieldValue renvoie False, Report.UpdatedWidgets vaut zéro et Report.ErrorMessage nomme le problème, comme « Direct widgets require an explicit unique NM identity ». C'est le comportement voulu : un refus clair au lieu d'une demi-écriture silencieuse. Une précondition de plus vaut d'être connue. L'écriture typée est annotation-only et n'acquiert pas de focus natif, donc si un éditeur de champ natif est encore focusé, toute la transaction se refuse avant que l'instantané soit pris ; appelez Pdf.ClearFormFieldFocus délibérément d'abord plutôt que de retenter en boucle
Pour plusieurs champs, surtout entre pages, utilisez la forme par lot. TPdf.SetFormFieldValues accepte de 1 à 256 requêtes, les applique toutes à un seul candidat, et publie tout ou rien. TPdfFormValuesWriteReport.FailedRequest vous dit quelle requête a cassé le lot
procedure FillInvoiceHeader(Pdf: TPdf);
var
Requests: TPdfFormValueRequests;
Batch: TPdfFormValuesWriteReport;
begin
SetLength(Requests, 2);
Requests[0] := Default(TPdfFormValueRequest);
Requests[0].Page := 1;
Requests[0].FieldIndex := 0;
Requests[0].Value.Kind := fvkText;
Requests[0].Value.Text := 'ACME Tooling Inc.';
Requests[1] := Default(TPdfFormValueRequest);
Requests[1].Page := 2;
Requests[1].FieldIndex := 0;
Requests[1].Value.Kind := fvkText;
Requests[1].Value.Text := '2026-10-27';
if not Pdf.SetFormFieldValues(Requests, Batch) then
raise Exception.CreateFmt('Request %d failed: %s',
[Batch.FailedRequest, Batch.ErrorMessage]);
// Batch.UpdatedFields = 2 ; Batch.UpdatedWidgets compte chaque widget synchronisé
end;
Deux requêtes adressant le même champ terminal sont rejetées comme propriétaire en doublon au lieu de laisser la dernière écriture gagner, et le document source est préservé. Les valeurs case à cocher et radio suivent les mêmes règles ; le découpage /V face à /AS pour les boutons est couvert dans valeur de champ case à cocher face à état de widget dans PDFium
Prouver que la valeur survit à la sauvegarde et à la réouverture
Une relecture dans la même session prouve que le document vivant est cohérent. Une sauvegarde et réouverture prouve que le fichier l'est, et le contrôle est assez bon marché pour rester dans vos propres tests d'intégration
procedure SaveAndReopenCheck(Pdf: TPdf; const Target: string;
PageNo, FieldIndex: Integer; const Expected: WString);
var
Reopened: TPdf;
begin
if not Pdf.SaveAs(Target, saNoIncremental, pvUnknown) then
raise Exception.Create('Save failed');
Reopened := TPdf.Create(nil);
try
Reopened.FileName := Target;
Reopened.Active := True;
Reopened.PageNumber := PageNo;
if Reopened.FormField[FieldIndex] <> Expected then
raise Exception.Create('Saved file lost the field value');
finally
Reopened.Free;
end;
end;
Deux autres pièges au niveau octet : Trim et le /ID du trailer
Normaliser les champs directs signifie relire des octets de dictionnaires bruts, ce qui fait remonter deux pièges plus petits, pertinents pour tout code Delphi qui touche la syntaxe PDF
Le premier est SysUtils.Trim sur des octets PDF bruts portés par un AnsiString. Une chaîne literal comme une option de choix peut légitimement contenir du texte UTF-16BE avec un marqueur d'ordre des octets FE FF et des octets NUL embarqués. En Delphi Unicode, SysUtils.Trim prend un string, donc l'appel convertit silencieusement l'AnsiString en UTF-16 avec la page de code ANSI active et reconvertit le résultat en sens inverse. Selon cette page de code, l'aller-retour peut réécrire les octets hauts, abîmer le BOM et même déranger le délimiteur qui suit. L'approche sûre garde le lexer sur les octets : sautez les blancs PDF par position (l'ISO 32000-1 §7.2.2 liste NUL, tab, LF, FF, CR et espace) et tranchez avec Copy, en ne décodant qu'une fois la valeur avérée être une chaîne de texte PDF
// Risqué sur des octets PDF bruts : AnsiString -> string -> AnsiString (W1057/W1058)
Token := Trim(Token);
// Sûr au niveau octet : sauter les blancs PDF par position, puis trancher
P := 1;
while (P <= Length(Token)) and (Token[P] in [#0, #9, #10, #12, #13, ' ']) do
Inc(P);
Token := Copy(Token, P, MaxInt);
Le second piège se cache dans la mise à jour incrémentale elle-même. L'ISO 32000-1 §7.5.5 (table 15) et §14.4 exigent que le /ID du trailer soit un tableau de deux chaînes d'octets. Le helper interne qui compose l'identifiant de mise à jour éparse ne renvoie que le contenu des deux chaînes, sans la clé /ID ni les crochets du tableau, et le trailer doit l'envelopper explicitement en /ID [ ... ]. Le loader laxiste de PDFium, comme une resauvegarde complète ultérieure, cachent tous les deux un wrapper manquant, donc le contrôle fiable est de parser les fichiers candidat et sauvegardé avec un parseur strict indépendant et de chercher deux chaînes de 16 octets. Si vous écrivez des mises à jour incrémentales vous-même, le même piège s'applique ; l'article sur les mises à jour incrémentales pour PDF chiffrés couvre la chaîne du trailer plus en profondeur
Que faire dans votre propre code de formulaires Delphi ?
- Relisez toujours la valeur avec
FormField[Index]après une écriture, et dans les tests sauvegardez aussi, rouvrez et relisez - Traitez
FalseplusErrorMessagecomme de l'information : un refus nommantNM, un conflit de parent ou un nom en doublon signifie que la possession du fichier est réellement ambiguë - Si vous générez des PDF, émettez champs et widgets comme objets indirects et donnez à chaque widget un
/NMunique ; cela supprime l'ambiguïté à la source - Utilisez
SetFormFieldValuespour les remplissages multi-champs ou entre pages, pour qu'un échec tardif ne puisse pas laisser la moitié du formulaire mise à jour - Ne lancez jamais
Trimou d'autres helpers RTLstringsur des octets PDF bruts portés par unAnsiString
Soyez clair sur les frontières aussi. Documents signés, formulaires XFA, instantanés chiffrés et fichiers dont la possession des widgets ne peut pas être résolue de façon unique restent en dehors du profil déclaré et se refusent au lieu d'être forcés. Si votre workflow déplace des champs entre documents, les questions de possession se corsent encore ; le greffage de champs AcroForm entre documents PDF traite ce cas
Le PDFium Component apporte cette écriture de formulaires typée et transactionnelle aux applications Delphi, C++Builder et Lazarus, aux côtés du rendu, de l'extraction de texte et de l'édition de documents. Historique des versions, licences et téléchargements sont sur la page produit du PDFium Component pour Delphi