Article technique

Régler les valeurs de champs d'un PDF chargé en Delphi

Dans la liste de champs de presque n'importe quel lecteur PDF, le champ rue d'un formulaire de réclamation en allemand lit Straße. Les octets derrière son entrée /T racontent une autre histoire : <FEFF005300740072006100DF0065>, une chaîne UTF-16BE hexadécimale avec une marque d'ordre d'octets devant. Du code qui apparie les noms de champs contre ces octets bruts rapporte le champ comme manquant, même si un utilisateur peut cliquer dedans directement. Le même formulaire peut stocker l'état on d'une case à cocher comme 2 au lieu de Yes et sa liste de pays comme paires [export display], ce qui vous donne trois façons séparées pour que la valeur que vous écrivez et la valeur qu'un lecteur peigne soient en désaccord

Les trois variations viennent du même endroit : un formulaire construit il y a des années dans l'outil d'auteur qu'avait le client, un formulaire que votre application Delphi doit maintenant remplir depuis une base de données et rendre comme un fichier qui s'affiche pareil dans chaque lecteur. Vous ne pouvez pas réécrire le formulaire, alors le code de remplissage doit suivre les règles sous lesquelles le formulaire a été écrit. L'ISO 32000-1 a une règle pour chacun des trois cas : section 7.9.2.2 pour les chaînes de texte, section 12.7.4.2.3 pour les états d'apparence des boutons, table 231 dans la section 12.7.4.4 pour les sélections de listes. Créer des champs qui n'existent pas encore est un autre travail, couvert dans ajouter des champs AcroForm à un PDF chargé en Delphi

Trouver le champ : HotPDF décode /T avant d'apparier les noms

THotPDF.SetFormFieldValue a deux surcharges, une prenant un index de champ zéro-basé et une prenant un nom entièrement qualifié, et les deux se résolvent contre une table de noms que le composant construit depuis l'arbre AcroForm à la première utilisation. Pendant que cette table se construit, chaque nom partiel passe par un décodeur. Une chaîne /T hexadécimale est ramenée en octets ; si ces octets commencent par FE FF et ont une longueur paire, le reste est lu comme UTF-16BE et réencodé en UTF-8. Chaque nom partiel décodé est ensuite joint à son parent avec un point, exactement comme le décrit la section 12.7.3.1, si bien qu'un champ enfant nommé City sous un parent nommé Address est enregistré comme Address.City. Seule la clé de recherche est décodée. L'objet /T dans le fichier garde son orthographe hexa, et enregistrer le document ne touche pas l'identité d'un champ que vous avez simplement rempli

Les clés sont mises en minuscules avant d'entrer dans la table, si bien que 'address.city' trouve Address.City. Cela va au-delà de la norme, qui traite les noms de champs comme sensibles à la casse, et cela signifie que deux champs ne différant que par la casse entreraient en collision dans la recherche. Pour du code qui lit des noms de champs depuis un fichier de configuration tapé à la main, une majuscule manquée est une panne bien plus courante que deux champs d'un même formulaire ne différant que par la casse

Comment HotPDF résout des noms AcroForm non ASCII : la chaîne /T hexadécimale est ramenée en octets, la marque d'ordre d'octets FE FF est décodée en UTF-16BE et réencodée en UTF-8, et le nom qualifié rejoint son parent si bien que Applicant.FullName et un champ nommé Straße atterrissent tous deux dans la table de recherche
Seule la clé de cache change : le dictionnaire de champ garde son encodage hexadécimal, les recherches normalisent en minuscules comme confort au-delà de la norme, et enregistrer le document ne réécrit jamais l'identité d'un champ que vous avez simplement rempli

Les noms décodés sont des octets UTF-8 portés dans un AnsiString, et c'est là que les conversions de chaînes propres de Delphi se mettent en travers. Un littéral comme 'Applicant.Straße' passé à un paramètre AnsiString est converti à travers la page de codes ANSI du système, si bien que ß arrive comme l'octet seul DF plutôt que la paire UTF-8 C3 9F, et la recherche rate. Les noms ASCII ne sont pas affectés. Pour tout le reste, énumérez les champs une fois, décodez chaque nom avec UTF8ToString, et écrivez à travers la surcharge par index :

// FindFieldIndex est un utilitaire que vous ajoutez : il compare les
// noms UTF-8 décodés à une chaîne Unicode, si bien qu'aucune page
// de codes ANSI ne s'interpose entre votre littéral et la recherche
function FindFieldIndex(Pdf: THotPDF; const Name: string): Integer;
var
  I: Integer;
begin
  for I := 0 to Pdf.GetFormFieldCount - 1 do
    if SameText(UTF8ToString(Pdf.GetFormFieldName(I)), Name) then
      Exit(I);
  Result := -1;
end;

var
  Pdf: THotPDF;
  Street: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;

    // Les noms ASCII peuvent passer directement par la surcharge par nom
    Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');

    Street := FindFieldIndex(Pdf, 'Applicant.Straße');
    // La valeur est aussi un AnsiString, donc un littéral 'ß' dépendrait de
    // la page de codes système ; l'hexa FEFF (Hauptstraße 12) évite ça
    if Street >= 0 then
      Pdf.SetFormFieldValue(Street,
        'FEFF00480061007500700074007300740072006100DF0065002000310032');

    // Le même hexa UTF-16BE préfixé FEFF marche avec la surcharge
    // par nom (celui-ci est München) ; il est écrit comme chaîne hexa
    Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');

    Pdf.SaveLoadedDocument('claim-form-filled.pdf');
  finally
    Pdf.Free;
  end;
end;

Ce que SetFormFieldValue écrit, étape par étape

Les deux surcharges exécutent la même séquence après que le champ est trouvé : écrire /V, reconstruire l'index de sélection de liste, marquer le champ changé, basculer les états d'apparence des boutons, régénérer l'apparence de texte ou de liste, et noter le champ dans un ensemble sale. Cette dernière étape nourrit le RecalculateLoadedFormFieldsIncremental sans paramètre, qui ne réexécute que les scripts de calcul qui lisent un champ changé, directement ou à travers une chaîne comme ligne de commande, sous-total, total

Le type d'objet du nouveau /V dépend du champ. Pour une case à cocher ou un bouton radio, SetFormFieldValue écrit toujours un objet nom, parce que la section 12.7.4.2.3 stocke l'état sélectionné comme un nom et que les noms PDF sont des octets purs sans encodage de chaîne de texte. Tout autre champ obtient un objet chaîne, et la valeur que vous passez est inspectée d'abord. Une chaîne qui commence par FEFF, a une longueur paire et ne contient que des chiffres hexa est prise pour la forme UTF-16BE de la section 7.9.2.2 et sérialisée comme <FEFF...>. C'est sur quoi compte la ligne City ci-dessus. Tout le reste est stocké comme chaîne littérale avec exactement les octets que vous avez fournis, si bien que du texte latin simple entre comme texte simple

Pourquoi une case à cocher garde-t-elle son ancienne coche après le changement de valeur ?

Pour un champ bouton, la valeur ne décide pas de ce qui est dessiné. Un widget case à cocher porte une entrée /AS qui nomme quel flux dans /AP /N est en train de s'afficher, et les lecteurs peignent depuis /AS, pas depuis /V. Changez /V en Yes pendant que /AS dit encore Off et le fichier se contredit ; un aplatisseur cuira la case décochée dans la page pendant que les données du formulaire prétendent qu'elle est cochée. SetFormFieldValue visite donc le champ bouton et chaque entrée de son tableau /Kids, lit le nom d'état on depuis le /AP /N de chaque widget, et règle /AS sur ce nom quand il correspond à la valeur du champ ou sur Off sinon

Pourquoi une case à cocher HotPDF garde son ancienne coche quand seul /V change : les lecteurs peignent depuis l'état d'apparence /AS vers /AP /N, si bien que SetFormFieldValue visite le champ et chaque enfant, lit le nom d'état on comme première clé autre que Off, et réécrit /AS en cas de correspondance ou vers Off sinon
Les groupes radio comparent chaque enfant à la valeur héritée du champ parent à travers la chaîne /Parent, si bien que régler le groupe sur une valeur export allume exactement ce widget et éteint tous ses frères

Les vrais formulaires plient cette règle de deux façons. L'apparence off s'appelle Off dans la norme, mais les outils d'auteur laissent souvent son flux de côté entièrement et laissent le lecteur ne rien dessiner, si bien qu'un dictionnaire d'apparence normale à une seule entrée est parfaitement légal. Le nom d'état on est ce que l'auteur a choisi : Yes, On, 2, un mot dans la langue propre du formulaire. Alors l'état on est pris comme la première clé dans /AP /N qui n'est pas Off, et la comparaison avec votre valeur ignore la casse. Les boutons radio ajoutent l'agencement de la section 12.7.4.2.4 : la valeur sélectionnée vit dans /V sur le champ parent, pendant que chaque widget enfant n'a habituellement pas de /V propre. Chaque enfant est comparé contre la valeur héritée à travers sa chaîne /Parent

// Case à cocher : la valeur doit égaler la clé d'état on dans /AP /N
// ('Yes' ici ; le concepteur du formulaire a peut-être choisi '2' ou 'On')
Pdf.SetFormFieldValue('Consent', 'Yes');

// Groupe radio : /V va sur le parent comme nom ; chaque widget enfant
// obtient /AS réglé sur son propre nom d'état on ou sur Off
Pdf.SetFormFieldValue('PaymentMethod', 'Card');

// Effacer une case : une valeur qui n'apparie aucun état on donne /AS Off
Pdf.SetFormFieldValue('Newsletter', 'Off');

Le revers est qu'une coquille ne produit pas d'exception. 'yes ' avec une espace finale n'apparie rien, si bien que la case finit en Off et le fichier reste parfaitement valide. Si vos valeurs viennent d'une colonne de base de données, rognez-les avant l'appel

Champs de liste : garder /I en accord avec /V

Une combo box ou une list box note sa sélection à deux endroits. À côté de /V, la table 231 définit /I comme un tableau d'indices zéro-basés dans /Opt, et un lecteur qui trouve /I pointant l'option 0 pendant que /V nomme l'option 3 peut surligner la mauvaise ligne. SetFormFieldValue reconstruit /I à chaque appel vers un champ de liste. Il supprime l'entrée /I propre du champ avant toute chose et n'édite jamais l'ancien tableau en place : un tableau indirect peut être partagé avec un autre champ, et le muter changerait aussi la sélection de ce champ. Il résout ensuite /Opt à travers la chaîne /Parent, puisque les options peuvent être héritées, et parcourt les entrées. Une option chaîne nue est comparée directement, une paire [export display] sur son élément export, et une paire de moins de deux éléments est sautée

Comment HotPDF garde un champ de liste cohérent : SetFormFieldValue supprime le tableau /I local avant de le toucher, résout /Opt à travers la chaîne /Parent, compare la moitié export de chaque option comme texte décodé, écrit un /I à un élément à la première correspondance et n'écrit rien quand une valeur de combo éditable n'a pas d'index
Une option chaîne nue est comparée directement et une paire export display sur son élément export, tandis qu'une valeur hors de /Opt ne laisse correctement aucun index — un /I périmé pointant la mauvaise ligne serait pire qu'aucun

La comparaison court sur du texte décodé, si bien qu'une option stockée en PDFDocEncoding apparie une valeur écrite en hexa UTF-16BE et vice versa. Une valeur scalaire produit au plus un index ; quelle que soit la multi-sélection que le champ portait avant, elle est remplacée. Quand rien n'apparie, aucun /I n'est écrit du tout. Pour une combo box éditable c'est l'issue correcte, parce que la section 12.7.4.4 laisse l'utilisateur taper une valeur hors de la liste et une telle valeur n'a pas d'index où pointer

La même règle est derrière la plainte la plus courante sur les combo box : l'appel réussit, /V porte le nouveau texte, et le lecteur ne montre rien de sélectionné. D'habitude l'appelant a passé le libellé d'affichage (Canada) à une liste construite depuis des paires [CA Canada]. Le composant ne peut pas traiter ça comme une erreur sans casser les combos éditables, alors le contrôle appartient à votre code. GetFormFieldOptionValue renvoie l'élément export de chaque option, ce qui rend une garde courte :

// SetChoiceByExport est votre propre garde, pas une partie du composant :
// il refuse les valeurs qui n'apparient à aucun élément export dans /Opt
procedure SetChoiceByExport(Pdf: THotPDF; const FieldName,
  ExportValue: AnsiString);
var
  I: Integer;
begin
  for I := 0 to Pdf.GetFormFieldOptionCount(FieldName) - 1 do
    if Pdf.GetFormFieldOptionValue(FieldName, I) = ExportValue then
    begin
      // /Opt est [[US United States] [CA Canada] [MX Mexico]] :
      // 'CA' apparie l'élément export et /I devient [1]
      Pdf.SetFormFieldValue(FieldName, ExportValue);
      Exit;
    end;
  raise Exception.CreateFmt('%s is not an export value of %s',
    [string(ExportValue), string(FieldName)]);
end;

// Combo éditable avec texte libre : écrivez-le directement. /V est réglé,
// /I est retiré, et aucun index n'est inventé
Pdf.SetFormFieldValue('Title', 'Principal Engineer');

La nouvelle valeur apparaît-elle sans /NeedAppearances ?

Oui, pour les champs de texte et de liste. Un lecteur dessine le flux /AP stocké et ne regarde pas /V, si bien qu'une nouvelle valeur avec une vieille apparence laisse l'ancien texte à l'écran et à l'impression tant que le dictionnaire AcroForm ne porte pas /NeedAppearances true (section 12.7.3.3) et que le lecteur ne choisit pas de l'honorer. Les aplatisseurs et les générateurs de vignettes ignorent fréquemment ce drapeau. SetFormFieldValue régénère le flux d'apparence pour les champs de texte et de liste dans le cadre de l'appel, en utilisant le même constructeur que le EnsureLoadedFieldAppearanceStream public. Le flux est construit depuis la chaîne /DA héritée, l'alignement /Q, le /MaxLen pour les champs comb et la valeur elle-même. La police nommée dans /DA est résolue à travers les ressources /DR de l'AcroForm, si bien qu'une police Type0 garde ses propres glyphes au lieu de retomber sur Helvetica. Les boutons gardent les apparences d'état dessinées par leur concepteur ; pour eux seul /AS bouge

SetFormFieldValue est une procédure et ne vous dit pas si quelque chose a été peint. Un widget sans /Rect utilisable n'obtient pas de flux, et des formulaires cassés avec des widgets orphelins ou sans rectangle existent. Quand le résultat rendu compte, appelez EnsureLoadedFieldAppearanceStream vous-même après avoir réglé la valeur. Il renvoie True quand au moins un widget a reçu un flux. GetFormField vous remet un THPDFLoadedFormField dont la propriété Index est ce que l'appel attend ; vous possédez cet objet et devez le libérer

var
  Field: THPDFLoadedFormField;
begin
  Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
  Field := Pdf.GetFormField('Applicant.FullName');
  try
    // False signifie qu'aucun widget de ce champ n'avait de rectangle
    // où peindre ; la valeur est stockée, mais rien ne la montrera
    if not Pdf.EnsureLoadedFieldAppearanceStream(Field.Index) then
      raise Exception.Create('No widget rectangle to paint into');
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('claim-form-filled.pdf');
end;

Vérifier que /V a changé prouve très peu de choses sur ce qu'un utilisateur verra. Un test qui veut dire quelque chose rend la page avant et après et confirme que les pixels à l'intérieur du rectangle du widget ont changé pendant que ceux de l'extérieur non. Nous gardons ce contrôle parce que les seuls octets d'apparence nous ont déjà induits en erreur : un flux portait chaque bon octet pendant qu'un lecteur honorant /NeedAppearances true reconstruisait l'affichage depuis /V et n'en montrait rien

Relire les valeurs, et ce qui vient d'habitude ensuite

GetFormFieldValue renvoie le /V hérité du champ. Une valeur stockée en hexa préfixé FEFF revient décodée en UTF-8, si bien que le champ City ci-dessus se relit comme les octets UTF-8 de München et UTF8ToString en fait une chaîne Delphi. Une chaîne littérale revient avec les octets avec lesquels elle a été stockée. Pour des contrôles d'aller-retour, comparez le texte décodé des deux côtés plutôt que l'orthographe que vous avez passée

Déplacer des données de champs en bloc entre le formulaire et un système externe, au lieu d'un appel par champ, est le sujet d'import et export XFDF en Delphi. Verrouiller un formulaire fini pour que personne ne puisse l'éditer est l'aplatissement des champs AcroForm et XFA en Delphi, qui transforme exactement les états /AS et les flux d'apparence décrits ici en contenu de page statique. Ce qui est incohérent à ce moment reste incohérent pour de bon, alors remplissez, vérifiez, puis aplatissez

SetFormFieldValue, EnsureLoadedFieldAppearanceStream et l'API de recalcul incrémental font partie du HotPDF Delphi Component, à côté des appels qui renomment, retirent et aplatissent des champs dans un document chargé depuis Delphi ou C++Builder