Article technique

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

HotPDF fait faire des allers-retours aux valeurs de list box multi-sélection à travers FDF et XFDF en gardant la valeur du champ comme tableau de bout en bout. Depuis la version 2.755.0, ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF et ExportLoadedFormToXFDF écrivent chaque option sélectionnée comme sa propre chaîne FDF ou élément <value> XFDF, et les méthodes d'import correspondantes vérifient chaque valeur contre les options du champ et reconstruisent les indices de sélection /I avant de changer quoi que ce soit. Rien ne se fait coller en une seule chaîne en route

L'échec corrigé ici est facile à reproduire. Prenez un bon de commande avec une list box multi-sélection d'options produit, laissez un utilisateur en choisir deux, exportez les données du formulaire pour un système back-office, puis réimportez le fichier édité dans le PDF. Avant ce changement, la list box revenait vide ou fausse. La raison, c'est que l'une des valeurs d'export contenait un retour à la ligne, et l'ancien chemin avait aplati les sélections en une unique chaîne séparée par des lignes. Sortir plusieurs sélections de cette chaîne n'a jamais été fiable, et avec une valeur d'export qui contient elle-même un retour à la ligne, cela ne peut pas marcher du tout

Pourquoi coller des valeurs multi-sélection avec des retours à la ligne casse-t-il l'aller-retour ?

Coller les sélections en une seule chaîne jette les frontières entre valeurs, et une valeur peut contenir le séparateur, donc aucun importateur ne peut redécouper la chaîne correctement. ISO 32000-1 §12.7.4.4 permet à l'entrée /V d'un champ de choix d'être une chaîne de texte unique ou un tableau de chaînes de texte, et une list box avec le drapeau MultiSelect (bit 22 de /Ff) utilise la forme tableau dès que plus d'une option est choisie. La même section définit /I comme un tableau d'indices d'options à base zéro en ordre croissant, ce que les lecteurs emploient pour distinguer deux options qui partagent par hasard une valeur d'export. Dans HotPDF, le getter scalaire GetFormFieldValue ne lit que la forme chaîne, donc faire passer un tableau par lui dégradait l'export en chaîne vide, et l'ancien import XFDF collait les éléments <value> répétés avec LF. Imaginez une option exportée Deep, saut de ligne, Blue : après collage, Deep\nBlue\nRed peut être deux sélections ou trois, et le fichier ne donne aucun moyen de savoir lequel. Le correctif a été d'arrêter complètement d'utiliser un scalaire au milieu de l'aller-retour

Ancien aller-retour multi-sélection HotPDF où deux options de list box choisies, dont une contenant un retour à la ligne embarqué, sont aplaties par le chemin scalaire GetFormFieldValue en l'unique chaîne Deep, saut de ligne, Blue, saut de ligne, Red, que les lecteurs en aval peuvent analyser 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ë

Que contiennent les fichiers FDF et XFDF exportés ?

HotPDF écrit une valeur multi-sélection comme tableau typé en FDF et comme un élément <value> par sélection en XFDF, si bien que les frontières restent visibles sur disque. En FDF, chaque item garde l'orthographe qu'il avait dans le PDF source : les chaînes hexadécimales sortent en hex, et les chaînes littérales sont échappées par un unique helper qui transforme CR et LF en \r et \n. En XFDF, la racine porte xml:space="preserve" comme ISO 19444-1 l'exige, ce qui veut dire que tout blanc à l'intérieur d'un élément texte compte comme donnée. HotPDF écrit donc la balise ouvrante, le texte échappé et la balise fermante de chaque <value> d'un seul tenant, 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 changer les octets d'origine

Formes d'export qu'HotPDF écrit pour une list box multi-sélection depuis 2.755.0 : 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 valent d'être connus avant d'écrire le code appelant. D'abord, ExportLoadedFormToFDF bâtit tout le corps FDF en mémoire avant de créer le fichier cible (corrigé en 2.755.1), donc une valeur non exportable, comme un tableau contenant autre chose que des chaînes, lève sans tronquer un fichier existant. Ensuite, une sélection vide sur une list box qui offre aussi une valeur d'export chaîne vide est ambiguë en XFDF, parce que <value/> peut signifier que rien n'est sélectionné ou que l'option vide est sélectionnée. ExportLoadedFormToXFDF lève dans ce cas plutôt que de deviner, et il lève avant que le fichier cible soit ouvert. Le FDF n'a pas cette ambiguïté, puisque /V [] et /V [()] sont distincts. Les deux exporteurs FDF sautent aussi les terminaux widget seul sans nom /T, à l'instar de l'exporteur XFDF, parce qu'aucun importateur ne pourrait jamais rattacher ces entrées à 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 HotPDF valide-t-il une valeur multi-sélection à l'import ?

HotPDF n'accepte un tableau importé que si la cible est un champ de choix avec le drapeau MultiSelect positionné et si chaque valeur du tableau correspond à une valeur d'export du tableau /Opt du champ. Chaque case d'option ne sert qu'une fois, donc une liste de deux options partageant la valeur d'export b accepte [<62> <62>] comme deux sélections distinctes et rejette un troisième b. Le /I reconstruit suit l'ordre de /Opt et pas celui des valeurs entrantes, puisque §12.7.4.4 exige des indices croissants. HotPDF bâtit le nouveau /V et le nouveau /I comme objets détachés et ne les affecte qu'après que chaque valeur a passé la validation, si bien qu'une valeur rejetée ne laisse jamais derrière elle un demi-tableau ni des indices périmés. La copie est écrite vers le champ importé plutôt que dans un tableau d'ancêtre partagé, les orthographes hexadécimales arrivant du FDF restent hex jusqu'à la sauvegarde, et les champs dont les calculs dépendent de la list box sont marqués pour recalcul. Si vous n'avez besoin que de poser une seule valeur, poser une valeur de champ de formulaire dans un PDF chargé passe par le chemin scalaire, qui ne gère par conception 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

Certains autres outils écrivent des valeurs d'export ASCII simples comme chaînes hexadécimales sans marque d'ordre des octets, par exemple <416272>, puis exportent le XFDF en écrivant ces chiffres hexadécimaux comme texte. Une comparaison littérale stricte au retour échoue, et l'import abandonne. La version 2.755.1 ajoute une relance : quand une valeur ne correspond à aucune option, HPDFHexSpellingText décode le texte comme charge hexadécimale et compare le résultat à nouveau. La relance ne s'applique qu'à une entrée qui aurait levé sinon, donc elle ne change jamais une valeur qui correspondait déjà. La même version a aussi fait utiliser le même décodeur Unicode aux chemins scalaire et tableau, qui comprend PDFDocEncoding, UTF-16 avec l'une ou l'autre marque d'ordre des octets et UTF-8. Avant, une même valeur logique pouvait correspondre sur un chemin et échouer sur l'autre dans des documents qui mélangeaient les encodages

Pourquoi un fichier FDF valide peut-il encore perdre des champs pendant l'analyse ?

Un analyseur FDF qui ne suit pas les chaînes hexadécimales peut couper un dictionnaire de champ en deux quand une valeur hexadécimale se termine juste à côté du terminateur du dictionnaire. Dans << /T (region) /V <416273>>>, le premier > ferme la chaîne hexadécimale, mais un analyseur naïf le lit avec le > suivant comme la fin du dictionnaire et abandonne le champ en silence. L'importateur FDF au niveau fichier suivait déjà s'il se trouvait dans une chaîne hexadécimale, et en 2.755.1 les analyseurs de tableau et de dictionnaire derrière ImportLoadedInterchangeFromFDF font de même. Un second point concerne les références indirectes. Un fichier FDF est un petit document de syntaxe PDF avec sa propre numérotation d'objets (ISO 32000-1 §12.7.7), donc une valeur telle que /V [11 0 R] désigne l'objet 11 du fichier FDF, pas l'objet 11 du PDF que vous remplissez. L'analyseur FDF simplifié de HotPDF ne résout pas les références à l'intérieur du fichier, donc il rejette un tel tableau au lieu de lire ce que l'objet 11 est par hasard dans le document cible

Les imports fichier, flux et XFDF rapportent les erreurs différemment

Les trois routes d'import valident de la même façon mais rapportent les échecs différemment, et il vaut la peine d'en choisir une à dessein. ImportLoadedFormFromFDF saute tout champ qui échoue à la validation et renvoie le nombre de champs réellement appliqués, donc un compte inférieur aux attentes est le seul signe d'un problème. ImportLoadedInterchangeFromFDF et ImportLoadedFormFromXFDF lèvent au premier champ rejeté. Chaque champ est engagé pour son compte, donc les champs traités avant l'exception gardent leurs nouvelles valeurs. Ne traitez aucun de ces imports comme une transaction sur tout le fichier d'échange : si vous avez besoin d'un comportement tout-ou-rien, abandonnez le document chargé quand une exception survient au lieu de le sauvegarder

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 une cible non multi-sélection lève
      if Pdf.ImportLoadedInterchangeFromFDF(Source, True, False, Status, Info) then
        Pdf.SaveLoadedDocument('order-form-filled.pdf');
    except
      on E: Exception do
        ShowMessage('Import rejected, nothing saved: ' + E.Message);
    end;
  finally
    Source.Free;
    Pdf.Free;
  end;
end;

Étendre les callbacks XFDF sans casser les appelants existants

Le support des tableaux dans l'unité XFDF de plus bas niveau vit dans un enregistrement séparé, THPDFXFDFArrayAccess, et dans de nouvelles surcharges de HPDFXFDFExportFields et HPDFXFDFImportFields, pas dans des champs supplémentaires ajoutés à la fin de l'enregistrement THPDFXFDFAccess existant. La raison, c'est la compatibilité binaire. Du code qui remplit THPDFXFDFAccess comme variable locale ne règle souvent que les cases qu'il connaît et n'efface jamais le reste, donc un nouveau pointeur de fonction ajouté à cet enregistrement contiendrait des ordures de pile, et la bibliothèque le prendrait pour un vrai callback. Avec un enregistrement séparé, les anciens appelants gardent l'ancien agencement et les anciennes surcharges, et ces surcharges passent en interne un enregistrement de tableau tout nil. La surcharge d'import scalaire d'origine colle toujours les valeurs répétées avec LF pour compatibilité, et seule la surcharge consciente des tableaux les garde séparées. Quand vous branchez votre propre magasin de données, partez de Default(THPDFXFDFArrayAccess). Renvoyez True depuis GetFormFieldValueArray pour tout champ à valeur de liste, y compris sans rien de sélectionné, et False pour retomber sur le callback scalaire

uses HPDFXFDF;

// Pointeur de fonction simple, pas « of object » : Context porte votre propre magasin
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;

L'échange multi-sélection fonctionne sur des list box qui existent déjà et ont le bit MultiSelect positionné dans /Ff. Pour la création des champs de choix et de leurs bits de drapeaux à l'origine, voyez ajouter des ListBox et autres champs AcroForm à un PDF chargé. Pour le balisage d'annotations qui passe par l'arbre <annots> du XFDF, voyez l'import et l'export d'annotations XFDF dans HotPDF. La référence API complète et le téléchargement d'essai sont sur la page du composant PDF HotPDF pour Delphi