Une valeur d'export de list box composée de Deep, d'un saut de ligne et de Blue est parfaitement légale en PDF, et une seconde option nommée Red l'est tout autant. Cochez les deux, aplatissez la sélection en une seule chaîne séparée par des LF pour un fichier d'échange, et le correspondant reçoit Deep\nBlue\nRed : deux sélections ou trois, et rien dans les octets ne dit laquelle. Les valeurs multi-sélection ne reviennent intactes de FDF ou XFDF que si elles restent des tableaux d'un bout à l'autre, du moment où elles quittent le PDF jusqu'à celui où elles y sont réécrites
Pourquoi un collage par saut de ligne ne peut-il jamais porter une valeur multi-sélection ?
Une chaîne collée perd les frontières entre valeurs, et comme une valeur a le droit de contenir le séparateur, aucun importateur ne peut les remettre en place. L'ISO 32000-1 §12.7.4.4 permet à l'entrée /V d'un champ de choix de porter une chaîne de texte unique ou un tableau de chaînes de texte ; une list box avec le drapeau MultiSelect (bit 22 de /Ff) utilise la forme tableau dès que plus d'une option est cochée. La même section définit /I, un tableau d'indices d'options à base zéro en ordre croissant, qui est la façon dont une visionneuse distingue deux options partageant une valeur d'export. Aucune des deux structures ne passe dans une API typée chaîne. GetFormFieldValue dans HotPDF renvoie un AnsiString et lit la forme chaîne de /V, donc tout export construit dessus ne peut écrire qu'un scalaire par champ
Le côté import a le problème en miroir. XFDF écrit chaque sélection comme son propre élément <value>, et un importateur qui les remet à un setter scalaire doit d'abord les recoller, en général avec LF. Avec l'option Deep/Blue ci-dessus, cette colle est indiscernable des données. HotPDF tient donc les scalaires hors du milieu de l'aller-retour : ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF et ExportLoadedFormToXFDF lisent le tableau directement dans le champ, et les méthodes d'import correspondantes construisent un nouveau tableau sans jamais passer par une chaîne collée
Ce que contiennent les fichiers FDF et XFDF exportés
Sur disque, les frontières restent visibles. En FDF, un champ multi-sélection devient un tableau typé, et chaque item garde l'orthographe qu'il avait dans le PDF source : les chaînes hexadécimales sortent en hex, les chaînes littérales sortent en littérales avec CR et LF échappés en \r et \n. Dans le fichier écrit par ExportLoadedFormToFDF, un /RV rich text ou un /Ff local non nul sur le champ voyage dans le même dictionnaire de champ. En XFDF, l'élément racine déclare xml:space="preserve", que l'ISO 19444-1 exige et qui fait de chaque caractère blanc à l'intérieur d'un élément texte une donnée. C'est un piège pour l'indentation esthétique. HotPDF écrit la balise ouvrante, le texte échappé et la balise fermante de chaque <value> sur une ligne, garde l'indentation hors de l'élément, et encode CR, LF et TAB en références de caractères pour qu'un analyseur XML appliquant la normalisation des fins de ligne ne puisse pas réécrire une paire CR LF en un LF solitaire
<!-- FDF : un tableau typé par champ -->
<< /T (options) /V [(Deep\nBlue) (Red)] >>
<< /T (region) /V [<45553132>] >>
<!-- XFDF : un <value> par sélection -->
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
<fields>
<field name="options">
<value>Deep
Blue</value>
<value>Red</value>
</field>
</fields>
</xfdf>
Deux cas limites d'export comptent quand vous écrivez le code appelant. ExportLoadedFormToFDF assemble le corps FDF complet en mémoire et ne crée le fichier cible qu'ensuite, donc une valeur non exportable (un tableau contenant autre chose que des chaînes, par exemple) lève pendant que le .fdf précédent sur disque est encore intact. Le second cas est un vrai trou du XFDF lui-même : une sélection vide sur une list box qui offre aussi une valeur d'export chaîne vide. <value/> peut vouloir dire « rien de sélectionné » ou « l'option vide est sélectionnée », et le format n'a pas de troisième orthographe. ExportLoadedFormToXFDF lève dans cette situation, avant l'ouverture du fichier cible. Le FDF n'a pas ce problème, car /V [] et /V [()] sont des objets différents. Les trois exporteurs sautent les terminaux widget seul sans nom /T, puisqu'aucun importateur ne pourrait jamais rattacher une telle entrée à un champ
var
Pdf: THotPDF;
Written: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
begin
// Les list box multi-sélection sont écrites en /V [(...) (...)]
Written := Pdf.ExportLoadedFormToFDF('order-form.fdf');
try
Pdf.ExportLoadedFormToXFDF('order-form.xfdf');
except
on E: Exception do
// Sélection vide plus option d'export vide : le XFDF ne peut pas
// les distinguer, et le fichier .xfdf existant reste intact
ShowMessage('XFDF export refused: ' + E.Message);
end;
end;
finally
Pdf.Free;
end;
end;
Comment une sélection importée est-elle vérifiée contre /Opt ?
HotPDF n'accepte un tableau entrant que si la cible est un champ de choix avec le drapeau MultiSelect positionné et si chaque valeur correspond à une valeur d'export du tableau /Opt du champ (pour une paire d'options à deux éléments, le premier élément). Chaque case d'option ne sert qu'une fois. Une liste de deux options exportant toutes deux b accepte donc [<62> <62>] comme deux sélections distinctes et rejette un troisième b, parce qu'il ne reste aucune case libre pour lui. Le /I reconstruit suit l'ordre de /Opt quel que soit l'ordre d'arrivée des valeurs, ce qui garde les indices croissants comme l'exige §12.7.4.4. Le nouveau /V et le nouveau /I sont construits comme objets détachés et ne sont affectés qu'après que chaque valeur a passé, donc une valeur rejetée ne peut laisser un demi-tableau ni des indices périmés sur le champ
Après l'affectation, le champ reçoit un flux d'apparence neuf. Cette étape est facile à oublier dans un importateur écrit à la main, et le symptôme déroute : les visionneuses dessinent le /AP stocké, pas le /V, donc l'ancienne sélection reste à l'écran et à l'impression bien que les données aient changé. La list box rejoint aussi l'ensemble des champs modifiés, ce qui veut dire qu'un appel sans paramètre à RecalculateLoadedFormFieldsIncremental relance exactement les calculs qui la lisent. Les orthographes hex arrivées du FDF restent hex jusqu'à la sauvegarde. Si vous n'avez besoin de poser qu'une seule valeur scalaire, poser une valeur de champ de formulaire dans un PDF chargé couvre ce chemin, qui par conception ne gère pas les sélections multiples
Un motif d'entrée mérite un traitement particulier. Un PDF peut stocker une valeur d'export ASCII simple comme chaîne hexadécimale sans marque d'ordre des octets, <416272> pour Abr, et un exporteur qui copie le corps de chaîne PDF verbatim produit un XFDF dont le texte d'élément est les chiffres bruts 416272 plutôt que le texte décodé. Un XFDF qui porte une valeur d'export encodée hex comme chiffres bruts ne peut pas correspondre à /Opt par orthographe : une comparaison stricte ne trouve aucune option nommée 416272 et l'import échoue. Quand l'importateur XFDF de HotPDF reçoit un tel fichier, il retente une fois la valeur sans correspondance en la décodant comme charge hexadécimale. La relance est délibérément étroite. Elle ne s'applique qu'à une valeur sans aucune correspondance exacte ; toute correspondance exacte est réservée d'abord, donc une orthographe secourue ne peut jamais prendre une case d'option dont une valeur exacte ultérieure aurait besoin ; et l'item /V stocké est la chaîne PDF propre de l'option, donc /V et /I décrivent la même sélection. C'est la bonne dose de tolérance : elle ne se déclenche que là où l'import aurait levé sinon, et une valeur qui correspondait déjà n'est jamais réinterprétée. La correspondance elle-même passe par un décodeur Unicode unique qui comprend PDFDocEncoding, UTF-16 avec l'une ou l'autre marque d'ordre des octets et UTF-8, donc le même texte logique correspond à la même option même dans des documents qui mélangent les encodages
Deux pièges d'analyse FDF : terminateurs hex et numéros d'objets étrangers
Le premier piège est lexical. Dans << /T (region) /V <416273>>>, le premier > ferme la chaîne hex et les deux suivants ferment le dictionnaire. Un scanneur qui ne cherche que des paires >> termine le dictionnaire un caractère trop tôt et perd le champ. L'importateur FDF au niveau fichier de HotPDF suit s'il se trouve dans une chaîne hex ou littérale pendant qu'il découpe le tableau /Fields, et le lecteur par flux derrière ImportLoadedInterchangeFromFDF tokenize tout le fichier comme syntaxe PDF, donc tous deux voient trois crochets fermants là où un scan naïf en voit deux
Le second piège concerne les références. Un fichier FDF est un petit document de syntaxe PDF avec sa propre numérotation d'objets (ISO 32000-1 §12.7.7), donc /V [11 0 R] désigne l'objet 11 du fichier FDF, jamais l'objet 11 du PDF en cours de remplissage. Le résoudre contre le document cible puiserait silencieusement l'objet qui porte par hasard ce numéro là-bas. Dans les builds Delphi 2009 et ultérieurs, le lecteur d'échange décode le FDF comme un graphe d'objets complet, suit la référence à l'intérieur de ce graphe, valide les chaînes résultantes comme tout tableau en ligne, et lève si le FDF ne contient pas l'objet 11. Le ImportLoadedFormFromFDF au niveau fichier lit chaque dictionnaire de champ pour son compte sans la table d'objets du FDF ; un tableau de références échoue à son contrôle chaînes-seulement, et ce champ est sauté
Choisir une route d'import : sauter, lever ou annuler
Les trois routes d'import valident les valeurs multi-sélection de la même façon. Elles diffèrent sur ce qui arrive quand une valeur échoue. ImportLoadedFormFromFDF saute tout champ en échec et renvoie le nombre de champs réellement appliqués, donc un compte inférieur au nombre d'entrées envoyées est le seul signe d'ennui. ImportLoadedFormFromXFDF et ImportLoadedInterchangeFromFDF lèvent au premier champ rejeté, et chaque appel s'exécute comme une transaction sur le document chargé : quand l'exception sort de la méthode, chaque champ (et, pour la route d'échange, chaque annotation) que l'appel avait déjà changés est annulé. Un import d'échange qui renvoie False est annulé aussi, avec Status et l'enregistrement d'information remis à zéro
Pour des données qu'une personne a relues et renvoyées, utilisez une des routes qui lèvent. Un bon de commande à moitié appliqué est pire qu'un rejeté, car rien dans le PDF sauvegardé ne montre quelles list box viennent du fichier relu et lesquelles ont gardé leurs anciennes valeurs. Le comportement sauter-et-compter de ImportLoadedFormFromFDF sert un autre travail : le préremplissage au mieux depuis un FDF peut-être généré pour une révision légèrement différente du formulaire
var
Pdf: THotPDF;
Source: TMemoryStream;
Status: AnsiString;
Info: THPDFFDFInterchangeInfo;
begin
Pdf := THotPDF.Create(nil);
Source := TMemoryStream.Create;
try
Source.LoadFromFile('order-form-reviewed.fdf');
if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
try
// Champs seulement. Une valeur hors /Opt, ou plusieurs valeurs visant une
// list box mono-sélection, lève et annule tout l'import
if Pdf.ImportLoadedInterchangeFromFDF(Source, True, False, Status, Info) then
begin
// Relance uniquement les calculs qui lisent les champs importés
Pdf.RecalculateLoadedFormFieldsIncremental;
Pdf.SaveLoadedDocument('order-form-filled.pdf');
end;
except
on E: Exception do
ShowMessage('Import rejected, document unchanged: ' + E.Message);
end;
finally
Source.Free;
Pdf.Free;
end;
end;
Brancher votre propre magasin de données sur les callbacks de tableau XFDF
L'unité HPDFXFDF, qui convertit entre XFDF et tout magasin de champs descriptible par callbacks, porte le support des tableaux dans un enregistrement séparé, THPDFXFDFArrayAccess, et dans des surcharges supplémentaires de HPDFXFDFExportFields et HPDFXFDFImportFields. Ajouter de nouvelles cases à l'enregistrement THPDFXFDFAccess existant aurait été plus court et faux. Les appelants déclarent souvent cet enregistrement comme variable locale et ne remplissent que les cases qu'ils connaissent, donc un nouveau pointeur de fonction à la fin porterait des ordures de pile que la bibliothèque prendrait pour un callback. Avec l'enregistrement séparé, le code existant garde son agencement et ses surcharges, et ces surcharges passent en interne un enregistrement de tableau tout nil
Les deux surcharges d'import se comportent différemment face à des éléments <value> répétés. La surcharge scalaire d'origine les colle toujours avec LF et appelle SetFormFieldValue, car les appelants existants dépendent de cette convention. La surcharge consciente des tableaux ne colle jamais : elle remet la liste à SetFormFieldValueArray, ne retombe sur le setter scalaire que pour une valeur unique que votre callback refuse, et lève quand votre callback refuse une liste de deux éléments ou plus. À l'export, partez de Default(THPDFXFDFArrayAccess) pour que les cases non affectées (dont la SetFormFieldRV optionnelle) restent nil. Renvoyez True depuis GetFormFieldValueArray pour tout champ à valeur de liste, y compris sans rien de sélectionné, et False pour laisser le callback scalaire traiter le champ. Un tableau vide sort comme un unique <value/> vide, donc un magasin qui offre une option chaîne vide devrait refuser ce cas à l'intérieur du callback, de la même façon que ExportLoadedFormToXFDF
uses HPDFXFDF;
// Pointeur de fonction simple, pas « of object » : Context porte votre propre magasin.
// TFormStore et ses méthodes sont votre code, pas une partie de HotPDF
function StoreGetSelections(Context: Pointer; FieldIndex: Integer;
out Values: THPDFXFDFValueArray): Boolean;
begin
Result := TFormStore(Context).IsListField(FieldIndex);
if Result then
Values := TFormStore(Context).Selections(FieldIndex);
end;
procedure ExportStore(Store: TFormStore; out Bytes: TBytes);
var
Access: THPDFXFDFAccess;
ArrayAccess: THPDFXFDFArrayAccess;
begin
Access := MakeStoreAccess(Store); // vos liaisons scalaires existantes
ArrayAccess := Default(THPDFXFDFArrayAccess); // chaque case inutilisée est nil
ArrayAccess.GetFormFieldValueArray := StoreGetSelections;
HPDFXFDFExportFields(Access, ArrayAccess, Bytes);
end;
Tout ce qui précède suppose des list box qui existent déjà et qui ont déjà le bit MultiSelect positionné dans /Ff. La création de champs de choix et le réglage de leurs bits de drapeaux sont traités dans l'ajout de ListBox et d'autres champs AcroForm à un PDF chargé, et le balisage de commentaires qui voyage par la partie <annots> du XFDF est traité dans l'import et l'export d'annotations XFDF dans HotPDF. La référence de méthode de chaque appel montré ici, plus un build d'essai pour les essayer sur vos propres formulaires, sont sur la page HotPDF Delphi PDF component