PDFium Component pour Delphi remplit un AcroForm atomiquement avec TPdf.SetFormFieldValues : vous passez de 1 à 256 requêtes typées adressées par page et widget, et le composant applique soit chaque valeur en un seul remplacement de document, soit aucune. Quand une requête est invalide, TPdfFormValuesWriteReport.FailedRequest nomme la requête qui a coulé le lot
Cette garantie compte le plus dans les jobs que personne ne surveille. Un import nocturne pousse des lignes CRM dans un formulaire de sinistre, ou un service d'impression superpose des données de police sur un modèle pré-imprimé. Exécutez ces derniers champ par champ et la première valeur fautive, un code qui n'est pas un export radio valide, une chaîne plus longue que le MaxLen du champ, vous laisse avec un PDF à moitié vieilles données et à moitié neuves. Rien n'a crashé, donc rien n'a alerté personne. L'API par lot existe pour qu'un échec soit un refus propre au lieu d'un document à moitié écrit
Pourquoi le remplissage champ par champ est-il dangereux pour l'import de données ?
Le remplissage champ par champ est dangereux parce que chaque écriture committe pour son propre compte, donc une erreur au milieu de la boucle laisse un document qui ne correspond ni à l'enregistrement source ni au modèle d'origine. Le schéma classique est une boucle d'affectations FormField[Index], chacune passant par l'environnement natif de remplissage de formulaires de PDFium un widget à la fois. Les valeurs 1 à 17 atterrissent, la valeur 18 lève, et l'appelant doit maintenant savoir exactement quelles valeurs sont entrées pour les annuler
La plupart des appelants n'écrivent jamais ce code d'annulation, et ceux qui le font se trompent en général subtilement : un champ partagé a déjà changé sur toutes les pages où il apparaît, et les états de cases à cocher ont déjà basculé. Si la sauvegarde tourne dans un bloc finally, le fichier à moitié rempli part sur le disque avec des airs de complétude. Dans un job de surimpression, c'est pire que pas de fichier, parce qu'il s'imprime et quelqu'un le signe
L'autre problème silencieux est le focus. Le setter classique pilote l'éditeur interactif de PDFium : il tue tout focus existant, focusse le widget cible, sélectionne tout le texte, le remplace et tue à nouveau le focus. C'est très bien pour un job headless, mais dans un viewer ça termine aussi la session d'édition que l'utilisateur avait ouverte dans un autre champ. Le chemin d'écriture typé arrivé dans PDFium Component 3.231.0 ne passe pas du tout par le focus natif des champs, et 3.233.0 l'a étendu des champs simples aux lots entre pages
Comment SetFormFieldValues applique un lot en un seul remplacement
TPdf.SetFormFieldValues met en scène chaque requête contre un seul instantané capturé du document, écrit tous les dictionnaires de champs en une passe, et échange le résultat fini en un seul remplacement. Le document d'origine reste vivant jusqu'à la toute dernière étape, donc tout échec avant cette étape le laisse intact
La séquence explique à la fois la garantie et le coût :
- Les contrôles preflight tournent d'abord : le document doit être actif, le nombre de requêtes doit être 1..256, aucune transaction d'objet de page ou de journal d'édition ne doit être ouverte, aucun champ natif ne doit détenir le focus, et le document doit accorder la permission de remplissage de formulaires ou de modification
- Chaque requête est validée pour plage de pages et budget d'entrée, et
FailedRequestsuit l'index en cours de contrôle - Le composant capture un instantané d'édition, normalise l'AcroForm dans cette copie, et applique chaque requête en ordre, en écrivant
/V,/Ipour les sélections de choix et/ASpour chaque widget qui partage le champ - Les changements sont ajoutés à l'instantané comme une seule mise à jour incrémentale éparse au sens de l'ISO 32000-1 §7.5.6, donc les objets sans rapport sont transportés octet pour octet
- Une instance vérificatrice séparée charge d'abord les octets candidats ; seulement après une ouverture propre le composant décharge le document vivant et charge le nouveau, en retombant sur l'instantané d'origine si ce chargement échoue
L'étape de remplacement interne est la même machinerie que le composant utilise pour ses instantanés de journal d'édition, voilà pourquoi le journal d'annulation et de récupération après crash et une écriture par lot ne peuvent pas se chevaucher : terminez ou annulez d'abord la transaction du journal. Les actions de cycle de vie automatiques sont supprimées pendant l'échange du document, pour qu'un script d'ouverture de document ne puisse pas réécrire des champs ni saisir le focus au milieu du remplacement
Construire un lot de requêtes typées en Delphi
Un lot est un tableau dynamique d'enregistrements TPdfFormValueRequest, chacun portant une Page à base 1, un FieldIndex local à la page à base 0 et une TPdfFormValue typée. L'enregistrement de valeur porte un Kind valant fvkText, fvkChoice, fvkCheckBox ou fvkRadioButton, plus des membres Text, ChoiceIndices et Checked utilisés selon ce kind
Les données d'import arrivent en général indexées par nom de champ, donc le premier travail est de traduire les noms en adresses de page et widget. FieldIndex compte les annotations widget d'une page dans le même ordre que FormFieldCount et FormFieldInfo, donc un simple scan marche. Le helper prend le premier widget qu'il trouve pour un nom, ce qui compte pour les champs partagés, comme l'explique la section suivante
type
TFieldAddress = record
Page, FieldIndex: Integer;
end;
function FindFieldAddress(Pdf: TPdf; const FieldName: WString;
out Address: TFieldAddress): Boolean;
var
P, I: Integer;
begin
Result := False;
for P := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := P;
for I := 0 to Pdf.FormFieldCount - 1 do
if Pdf.FormFieldInfo[I].Name = FieldName then
begin
Address.Page := P;
Address.FieldIndex := I;
Exit(True); // premier widget seulement : une requête par champ
end;
end;
end;
procedure AddRequest(var Requests: TPdfFormValueRequests;
const FieldName: WString; Pdf: TPdf; const Value: TPdfFormValue);
var
Address: TFieldAddress;
N: Integer;
begin
if not FindFieldAddress(Pdf, FieldName, Address) then
raise Exception.CreateFmt('Template has no field "%s"', [FieldName]);
N := Length(Requests);
SetLength(Requests, N + 1);
Requests[N].Page := Address.Page;
Requests[N].FieldIndex := Address.FieldIndex;
Requests[N].Value := Value;
end;
Notez que AddRequest lève sur un nom inconnu au lieu de le sauter. Le lot valide des adresses, pas votre mappage, donc un champ de modèle manquant doit échouer ici ou n'échouera jamais du tout. Les adresses résolues, construire les valeurs et les committer est court
var
Pdf: TPdf;
Requests: TPdfFormValueRequests;
Report: TPdfFormValuesWriteReport;
V: TPdfFormValue;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'claim-template.pdf';
Pdf.Active := True;
V := Default(TPdfFormValue);
V.Kind := fvkText;
V.Text := 'Maria Lopez';
AddRequest(Requests, 'Claimant.Name', Pdf, V);
V := Default(TPdfFormValue);
V.Kind := fvkCheckBox;
V.Checked := True;
AddRequest(Requests, 'Consent', Pdf, V);
V := Default(TPdfFormValue);
V.Kind := fvkRadioButton;
V.Text := 'Express'; // valeur d'export du bouton à sélectionner
AddRequest(Requests, 'Shipping', Pdf, V);
V := Default(TPdfFormValue);
V.Kind := fvkChoice;
SetLength(V.ChoiceIndices, 1);
V.ChoiceIndices[0] := 2; // index à base 0 dans le tableau Opt du champ
AddRequest(Requests, 'Region', Pdf, V);
if Pdf.SetFormFieldValues(Requests, Report) then
Pdf.SaveAs('claim-filled.pdf')
else
LogBatchFailure(Requests, Report);
finally
Pdf.Free;
end;
end;
Les kinds typés sont confrontés au type du champ avant que quoi que ce soit ne soit écrit. Une valeur texte visant une case à cocher, une valeur radio visant une case à cocher simple, ou une écriture vers un bouton poussoir, tout se refuse. Les index de choix doivent être valides, uniques et croissants ; plus d'un index exige le drapeau MultiSelect, et le texte libre dans un champ de choix n'est accepté que par une combo box éditable. Une valeur texte plus longue que le MaxLen du champ se refuse au lieu d'être tronquée, et les champs read-only se refusent carrément. Pour les groupes radio avec le drapeau NoToggleToOff, une valeur d'export vide, signifiant « vider le groupe », se refuse aussi
Pourquoi écrire un champ partagé deux fois fait-il échouer tout le lot ?
TPdf.SetFormFieldValues refuse deux requêtes qui se résolvent vers le même champ terminal parce qu'elles écriraient toutes deux une valeur unique, et le composant refuse de laisser une requête écraser silencieusement une autre. Le contrôle porte sur le propriétaire du champ, pas le widget, donc deux widgets différents d'un même champ sur deux pages différentes comptent quand même comme doublon
La raison est le modèle de champ de l'ISO 32000-1 §12.7.3.1. Un champ terminal possède la valeur ; ses annotations widget ne sont que des endroits où cette valeur est affichée. Un champ nommé PolicyNumber qui apparaît dans l'en-tête de chaque page est un champ avec plusieurs widgets, et chacun de ces widgets montre le même /V. Un groupe radio marche pareil : chaque bouton est un widget, le groupe est le champ. Écrire PolicyNumber via le widget de la page 1 puis encore via celui de la page 9, c'est un champ écrit deux fois, et toute règle qui choisit un gagnant cache un bug dans votre mappage
Le lot le traite donc comme une erreur et nomme la seconde requête dans FailedRequest. En pratique la réparation est en amont : résolvez chaque nom de champ vers exactement une adresse, comme le fait FindFieldAddress, et laissez le composant propager la valeur vers les autres widgets. Il le fait dans le cadre de l'écriture, en posant /AS sur chaque widget d'une case à cocher ou d'un groupe radio partagé pour que l'état d'apparence concorde avec la valeur du champ, le même découpage que couvre l'article sur les valeurs de champs case à cocher face aux états de widget
Lire le rapport d'échec
Un lot qui échoue renvoie False, ne lève pas, et laisse le document d'origine et chaque valeur de champ exactement tels quels. TPdfFormValuesWriteReport rapporte alors Succeeded = False, zéro pour UpdatedFields comme pour UpdatedWidgets, l'index à base 0 de la requête fautive dans FailedRequest, et une raison lisible par un humain dans ErrorMessage
procedure LogBatchFailure(const Requests: TPdfFormValueRequests;
const Report: TPdfFormValuesWriteReport);
var
R: Integer;
begin
R := Report.FailedRequest;
if R >= 0 then
Log(Format('Request %d (page %d, widget %d) rejected: %s',
[R, Requests[R].Page, Requests[R].FieldIndex, Report.ErrorMessage]))
else
Log('Batch rejected as a whole: ' + Report.ErrorMessage);
end;
Un FailedRequest de -1 signifie que le refus relève de la transaction plutôt que d'une requête : un focus natif était actif, le document est signé, le formulaire est XFA, ou un contrôle de permission a échoué. Une valeur non négative pointe une entrée. Pour un dépassement du budget global, elle pointe la requête qui a poussé le lot au-delà de la limite, qui n'est pas forcément la requête fautive. Gardez un tableau parallèle de noms de colonnes sources à côté de Requests et vous pourrez transformer l'index 14 directement en « ligne 2087, colonne ShipDate » dans le log d'import
En succès, UpdatedFields égale le nombre de requêtes et UpdatedWidgets compte chaque widget touché, partagés compris, un contrôle de santé bon marché face au modèle
Préconditions et limites que vous rencontrerez
Le refus le plus courant dans une application viewer est le focus natif : si un utilisateur saisit dans un champ, SetFormFieldValues refuse avant même de capturer l'instantané, et le tampon d'éditeur en attente survit intact. Le composant ne lâchera pas le focus dans votre dos, car cela committerait ou perdrait du texte que l'utilisateur n'a pas fini. Appelez ClearFormFieldFocus délibérément, ce qui termine la session d'éditeur comme le ferait un clic en dehors du champ, puis retentez
Les budgets sont fixes et documentés, pour que vous puissiez calibrer les imports dessus :
- 1 à 256 requêtes par lot ; découpez les enregistrements plus gros en plusieurs lots et acceptez que l'atomicité vienne alors par lot
- Au plus 262 144 unités UTF-16 de texte et 4 096 index de choix par requête
- Au plus 1 048 576 unités UTF-16 de texte et 16 384 index de choix sur tout le lot
- Les budgets de document habituels pour le parseur d'instantanés s'appliquent toujours, dont une source de 64 MiB et 65 536 objets
Un remplacement réussi invalide aussi les handles natifs, les recherches et les sélections obtenus avant l'appel, car le document en dessous est neuf. L'activation de page et de vue et les curseurs logiques de champs sont conservés ; tout le reste que vous avez mis en cache, comme des handles d'annotation ou d'objet de page, doit être récupéré à nouveau
En quoi est-ce différent de l'import XFDF ?
TPdf.ImportXFDF et TPdf.SetFormFieldValues mettent tous deux des données dans des champs, mais l'import XFDF est une fusion tolérante tandis que le lot est une transaction stricte. L'import XFDF apparie les valeurs aux champs par nom, saute les noms qu'il ne trouve pas, ignore les doublons, et renvoie combien d'entrées il a appliquées. C'est exactement ce qu'il faut pour fusionner les commentaires d'un relecteur, où un résultat partiel reste utile
Le lot fait l'inverse sur chaque point. Il adresse les widgets explicitement, refuse les cibles inconnues ou mal typées, traite les doublons comme des erreurs, et n'a pas d'issue partielle. Utilisez XFDF quand les données sont un fichier d'échange venant d'un autre outil ; utilisez SetFormFieldValues quand votre propre code possède le mappage et qu'un enregistrement à moitié appliqué serait un défaut
Ce que l'écriture par lot ne couvre pas
Le lot est un profil AcroForm borné, et il dit non plutôt que de deviner en dehors de ce profil. Les formulaires XFA se refusent et exigent leur éditeur dédié. Les documents signés se refusent, puisque réécrire des dictionnaires de champs sous une signature existante est un workflow de signature, pas une opération de remplissage ; les instantanés chiffrés sont hors profil aussi. Les tableaux d'export de boutons malformés ou ambigus se refusent, et les valeurs d'export radio qui correspondent à plus d'un bouton sont refusées sauf si le groupe pose RadiosInUnison. La transaction normalise aussi les dictionnaires de champs directs avant d'écrire, le cas derrière les valeurs de champs qui semblent revenir en arrière
Les apparences méritent une phrase d'attention. Pour les widgets texte et choix modifiés, le lot jette le /AP périmé au lieu de livrer une apparence montrant l'ancienne valeur, et il coupe NeedAppearances pour que PDFium ne reconstruise pas chaque widget sans rapport du formulaire. PDFium régénère les apparences manquantes quand il rend. Si le PDF rempli part vers un pipeline d'impression strict qui ne synthétise jamais d'apparences, rendez-le ou aplatissez-le via PDFium d'abord, et testez ce chemin avec vos propres modèles
SetFormFieldValues et les enregistrements de valeurs typées se livrent dans le PDFium Component pour Delphi et Lazarus, où la page produit liste l'API complète de remplissage de formulaires et AcroForm aux côtés du rendu, de l'édition et des contrôles viewer