Un formulaire d'enquête montre deux listes déroulantes dans Acrobat, group.a et group.b. Parcourez /AcroForm /Fields avec du code qui traite tout nœud portant son propre /FT comme un champ fini, et le compte revient à 1 : un unique champ nommé group avec deux widgets. Demandez group.b par nom et la recherche échoue. Ouvrez le dictionnaire de l'enfant : il n'y a aucun /FT dedans, aucun /Ff, aucune liste d'options. Rien n'est cassé. Le type, les drapeaux et les options se trouvent un niveau plus haut sur le parent, et la spécification PDF les autorise explicitement à vivre là
La disposition est légale et économique. Un outil d'auteur écrit un nœud groupe avec /FT /Ch, /Ff 131078 et un tableau /Opt une seule fois, puis suspend deux enfants nommés dessous, chacun un dictionnaire fusionné champ-plus-widget ne portant que /T, /Parent, /Rect et son propre /V. Lire correctement un tel formulaire demande trois choses : suivre /Parent pour chaque clé héritable, décider où l'arbre de champs se termine réellement, et s'assurer qu'un reset écrit une valeur du bon type PDF. Cette pièce reprend là où définir des valeurs de champs de formulaire dans un PDF chargé avec Delphi s'arrête
Quelles entrées AcroForm un champ peut-il hériter de son parent ?
L'ISO 32000-1 §12.7.3.1 (Table 220) marque /FT, /Ff, /V et /DV comme héritables, et la Table 229 en §12.7.4.3 en fait autant pour le /MaxLen d'un champ texte. Les tableaux d'options reçoivent ici le même traitement, car les producteurs qui partagent une liste de choix entre enfants stockent /Opt sur le groupe. Un lecteur qui ne vérifie que le dictionnaire local rapportera donc le mauvais type, des drapeaux à 0 et une valeur vide pour un enfant parfaitement valide
HotPDF Delphi Component répond à chacune de ces lectures de la même façon. Il cherche la clé dans le dictionnaire propre du champ et, si la clé y est, résout la référence indirecte et s'arrête. Sinon il suit /Parent et réessaie, pour au plus 128 niveaux. Ce plafond n'est pas décoratif. Des pointeurs /Parent dans des fichiers endommagés peuvent former une boucle qui n'a rien à voir avec /Kids, et un lecteur qui leur fait confiance sans condition se bloque sur le premier fichier de ce genre qu'il rencontre
Une règle de cette marche est facile à prendre à l'envers : le premier dictionnaire qui contient la clé gagne, même quand la valeur stockée y est une chaîne vide. Un /V () local sur group.b est l'auteur qui dit « cet enfant est vide », et la marche s'arrête là au lieu de grimper vers une valeur que le parent porterait peut-être. Traiter vide comme « absent » ferait afficher à un enfant effacé la sélection de son parent, l'exact contraire de ce que l'auteur a écrit
Les getters publics s'appuient tous sur cette recherche : GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue, et les helpers d'options GetLoadedFormFieldOptionCount et GetLoadedFormFieldOptions. Lire l'enfant du groupe d'enquête ne demande donc aucun code particulier :
var
Pdf: THotPDF;
Field: THPDFLoadedFormField;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
// 'group' porte /FT /Ch, /Ff 131078 et /Opt ; l'enfant
// 'group.b' ne porte que /T, /Parent, /Rect et son propre /V
Field := Pdf.GetFormField('group.b');
try
if Pdf.GetFormFieldType(Field.Index) = lfftChoice then
begin
// 131078 = Combo (bit 18) + NoExport (bit 3) + Required (bit 2)
Writeln(Pdf.GetLoadedFormFieldFlags(Field.Index));
Writeln(Pdf.IsFormFieldRequired(Field.Index)); // TRUE
Writeln(Pdf.GetLoadedFormFieldOptionCount(Field.Index));
Writeln(Pdf.GetFormFieldValue(Field.Index)); // le /V local
end;
finally
Field.Free;
end;
finally
Pdf.Free;
end;
end;
GetFormField renvoie un nouveau wrapper THPDFLoadedFormField à chaque appel et lève une exception quand le nom est inconnu : gardez l'appel hors du bloc try du wrapper et libérez le wrapper vous-même
Trouver où l'arbre de champs se termine
Un /FT local est le mauvais test pour un champ terminal, car un parent peut fournir le type et posséder quand même des champs enfants nommés. Le parcours tentant cesse de descendre dès qu'un nœud a son propre /FT ou n'a pas de /Kids. Dans le formulaire d'enquête, group a à la fois /FT /Ch et /Kids, donc ce parcours l'enregistre comme un champ unique à deux annotations widget. Les noms group.a et group.b disparaissent. Le compte de champs vaut 1. Une recherche par nom d'enfant échoue, et une écriture de valeur ne peut atterrir que sur le parent partagé
HotPDF regarde les kids à la place. Un kid compte comme champ enfant s'il a son propre /T, a son propre /Kids, ou n'est pas un dictionnaire /Subtype /Widget. Seulement quand aucun kid ne qualifie, le nœud est terminal, et ses kids sont alors traités comme ses widgets. Les deux cas limites derrière cette règle viennent des dictionnaires fusionnés, que §12.7.3.1 autorise quand un champ a exactement un widget :
- Un dictionnaire fusionné nommé porte
/Subtype /Widgetet reste un champ enfant. Le sous-type à lui seul ne peut pas l'envoyer dans la liste de widgets anonymes du parent ; c'est le/Tqui décide - Certains producteurs répètent le
/FTdu parent sur chaque widget anonyme. Donc un/FTsur un widget ne peut pas se lire « un nouveau champ commence ici » non plus
La même classification pilote l'index de champs, FormFieldExists et RemoveFormField. Chacun de ces parcours enregistre aussi les dictionnaires déjà visités et abandonne au-delà de 128 niveaux. Supposons que le groupe est l'objet 5 et se liste deux fois parmi ses propres kids : /Kids [5 0 R 5 0 R 6 0 R 7 0 R]. Il rapporte quand même exactement deux champs, les enfants 6 et 7. Pas de récursion infinie, et pas de troisième champ fantôme issu de l'auto-référence
Comment RemoveFormField évite-t-il de supprimer les champs frères ?
RemoveFormField ne supprime que l'enfant que vous nommez parce que découverte et suppression partagent une seule définition du champ terminal. La surcharge par nom résout un index de champ depuis la liste des champs, puis parcourt /AcroForm /Fields une seconde fois en comptant les champs terminaux pour trouver le dictionnaire à supprimer. Si le second parcours utilisait la règle /FT tentante pendant que le premier utilisait la règle par kids, les index se contrediraient : le parcours de suppression verrait group comme champ 0, et retirer group.a emporterait le parent, le frère et les deux widgets
Le parcours de suppression applique donc le même test de champ enfant et le même ensemble de visités que la découverte. Il collecte les annotations widget appartenant à l'enfant retiré, ne les ôte que des /Annots de chaque page, et ne retire le parent que quand son tableau /Kids finit vide. Avec group.b toujours listé, le parent reste, et le type, les drapeaux et les options que group.b hérite restent avec lui
// Retirer un enfant nommé ; son frère et le parent partagé survivent
Pdf.RemoveFormField('group.a');
// Vérifications que vous ajouteriez vous-même, pas une partie de la bibliothèque
Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// Type, drapeaux et options restent résolus via le parent
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');
Quand vous vérifiez un retrait sur vos propres formulaires, regardez à trois endroits, car chacun attrape une erreur différente : les /Kids du parent, les /Annots de la page, et la valeur et l'apparence du frère survivant. Faites-le une fois après SaveLoadedDocument et une fois après SaveIncrementalUpdate, car une sauvegarde incrémentale n'ajoute que les objets changés et les /Kids du parent doivent en faire partie
Ce que ResetLoadedFormField copie d'un /DV hérité
ResetLoadedFormField écrit un /V local qui est une copie neuve du /DV hérité, avec le même type d'objet PDF, et il valide le défaut complet avant de toucher au champ. Un défaut de case à cocher est un nom tel que /Yes. Un défaut de list box multi-sélection est un tableau de chaînes. Un défaut texte peut être une chaîne UTF-16 hexadécimale comme <FEFF...>. Faites passer l'un d'eux par GetLoadedFormFieldDefaultValue et SetFormFieldValue et vous obtenez une chaîne là où un nom devait être, une chaîne vide là où il y avait un tableau, ou des chiffres hex littéraux
Le reset se branche donc sur ce que le /DV hérité est réellement :
- Les champs texte et choix avec un défaut chaîne reçoivent un nouvel objet chaîne qui garde le drapeau hexadécimal de l'original
- Les champs de choix avec un défaut tableau reçoivent un nouveau tableau de nouvelles chaînes ; un tableau contenant autre chose que des chaînes lève une exception
- Les cases à cocher et boutons radio reçoivent un nouvel objet nom
- Un défaut de tout autre type lève une exception, et
/Vet/Irestent exactement comme ils étaient
Copier au lieu de référencer est délibéré, et c'est la seule option sûre. Un /V enfant qui pointerait vers le tableau /DV du parent partagerait un objet entre « valeur courante » et « valeur par défaut » ; le prochain SetFormFieldValue sur cet enfant réécrirait en silence le défaut de chaque frère. Quelques octets de plus par champ remis à zéro est un prix bon marché pour garder les deux séparés. Les pushbuttons, qui n'ont pas de valeur (Table 226, bit 17), et les champs de signature restent sur un chemin simple qui écrit une copie chaîne de leur propre /DV
Quand aucun /DV n'existe nulle part dans la chaîne, la méthode écrit une chaîne vide locale, ou /Off pour un champ case à cocher ou radio. Supprimer le /V de l'enfant semblerait plus propre et donnerait la mauvaise réponse. Si le parent porte une valeur courante, retirer la surcharge de l'enfant ramène cette valeur en droite ligne, et le champ « remis à zéro » affiche quelque chose que l'utilisateur n'a jamais saisi
var
Field: THPDFLoadedFormField;
begin
// Le parent 'palette' est une list box MultiSelect avec
// /Opt [(b) (g) (r)] et /DV [(b) (r)] ; l'enfant ne porte que /T et /V
Field := Pdf.GetFormField('palette.primary');
try
try
Pdf.ResetLoadedFormField(Field.Index);
// L'enfant a maintenant son propre /V [(b) (r)] et /I [0 2] ;
// le tableau /DV du parent reste intact
except
on E: Exception do
// Défaut malformé (mauvais type, item de tableau non chaîne) :
// /V et /I sont restés tels quels
Writeln('Reset skipped: ', E.Message);
end;
// Le getter scalaire ne peut pas représenter un défaut tableau
Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // vide
finally
Field.Free;
end;
Pdf.SaveLoadedDocument('survey-reset.pdf');
end;
Un reset d'un seul champ est aussi autre chose que l'action ResetForm du §12.7.5.3, qu'une visionneuse exécute sur un ensemble de champs quand l'utilisateur clique un bouton ; cette action est traitée dans construire des champs et actions AcroForm avec HotPDF. ResetLoadedFormField édite un seul champ chargé, suit sa propre règle pour le cas sans défaut, et marque le champ comme changé pour que le recalcul incrémental le reprenne
Garder /V, /I et /AS d'accord
Un reset n'est correct que si l'index de sélection et l'état d'apparence suivent la valeur, et ResetLoadedFormField finit avec les deux mêmes pas de suivi que SetFormFieldValue. Pour les champs de choix, il retire le /I local de l'enfant, confronte chaque valeur à la moitié export de chaque entrée /Opt, et écrit un nouveau /I trié. Remettre à [(b) (r)] contre les options b, g, r donne /I [0 2]. Le /I hérité du parent, s'il existe, reste intact
Les boutons travaillent depuis le type hérité. Une case à cocher enfant dont le /FT /Btn vit sur le parent reçoit quand même sa mise à jour de /AS. Côté écriture, SetFormFieldValue et SetLoadedFormFieldDefaultValue stockent un objet nom pour un bouton non pushbutton hérité même quand l'enfant n'a aucune entrée locale d'où copier le type. Et quand EnsureLoadedFieldAppearanceStream reconstruit les apparences de boutons, il écrit /AS /Off sauf si la valeur correspond à l'état actif, et donne à chaque flux d'état /Type /XObject, /Subtype /Form et un /BBox. Sans ce contrôle /Off, régénérer les apparences après un reset recocherait la case avant même que le fichier soit sauvegardé
Getters scalaires et autres angles à anticiper
Les getters chaînes restent scalaires, et le code qui compare leur sortie doit savoir ce qu'ils aplatissent. GetFormFieldValue et GetLoadedFormFieldDefaultValue renvoient une chaîne vide pour une valeur tableau, transforment nombres et booléens en 42 ou true, et rapportent une chaîne encodée hex sous son orthographe hexadécimale. Si vous devez savoir si un défaut de list box porte deux sélections ou aucune, le getter de défaut ne peut pas le dire : les deux reviennent comme chaîne vide
Un cycle /Parent termine la recherche sans lever, donc un champ dont le type est perdu dans un cycle rapporte lfftUnknown et des drapeaux à 0. Traitez lfftUnknown sur un champ nommé comme le signe d'un fichier endommagé qui mérite d'être journalisé. SetFormFieldValue et ResetLoadedFormField écrivent toujours l'enfant que vous adressez et ne promeuvent jamais une valeur vers le parent partagé. C'est juste pour des enfants indépendants comme les deux listes déroulantes de l'enquête. Les groupes radio sont différents : la sélection appartient au champ qui possède le groupe, donc adressez ce champ plutôt que l'un de ses kids
Chaque appel engage aussi un champ à son compte. Une boucle de resets sur vingt champs, c'est vingt éditions indépendantes ; si la cinquième lève sur un défaut malformé, les quatre premières sont déjà appliquées. Pour du travail par lots, lisez d'abord le type et le défaut de chaque cible, décidez quels champs vous toucherez, et seulement alors commencez à écrire
La recherche d'attributs hérités, la classification de l'arbre de champs par kids et le reset typé appartiennent tous à l'API de formulaires chargés du HotPDF Delphi Component, qui couvre aussi les projets Delphi et C++Builder devant ajouter de nouveaux champs à un document existant, comme montré dans l'ajout de champs AcroForm à un PDF chargé en Delphi