Article technique

Champs PDF multi-sélection en allers-retours FDF et XFDF

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

Aller-retour multi-sélection naïf où deux options de list box cochées, dont une contenant un retour à la ligne embarqué, sont aplaties par un chemin scalaire GetFormFieldValue en l'unique chaîne Deep, saut de ligne, Blue, saut de ligne, Red, que les lecteurs en aval peuvent lire comme deux sélections ou comme trois
Coller des valeurs multi-sélection en une seule chaîne détruit les frontières entre valeurs, et une valeur d'export qui contient elle-même un retour à la ligne rend la forme aplatie ambiguë

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

Formes d'export qu'HotPDF écrit pour une list box multi-sélection : le FDF porte un tableau typé par champ avec /V [(Deep retour à la ligne Blue) (Red)] et une valeur de région en hex, tandis que le XFDF porte un élément value par sélection sous xml:space preserve pour que les blancs comptent comme données
Les frontières restent visibles sur disque : le FDF garde chaque sélection comme son propre item de tableau et le XFDF écrit chacune dans un élément value séparé, donc aucun importateur n'a à deviner
<!-- 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&#xA;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

Validation d'import HotPDF pour les valeurs multi-sélection : la cible doit être un champ de choix avec MultiSelect positionné dans /Ff, chaque valeur entrante doit correspondre à une valeur d'export de /Opt avec chaque case servie une fois, /I est reconstruit croissant dans l'ordre de /Opt, et /V et /I détachés ne sont affectés qu'après que toutes les valeurs passent
Chaque valeur entrante est confrontée aux options du champ avant que quoi que ce soit soit écrit, si bien qu'une valeur rejetée ne laisse jamais derrière elle un demi-tableau ni des indices de sélection périmés

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