Article technique

Formulaires PDF à calcul automatique dans Delphi avec HotPDF

Une facture ou un PDF de dépenses dont les postes s'additionnent d'eux-mêmes et dont la date se formate automatiquement nécessite trois éléments greffés sur son formulaire AcroForm, et HotPDF ajoute ces trois éléments à un fichier chargé dans Delphi : ApplyLoadedFieldCalculation pour les totaux AFSimple_Calculate, ApplyLoadedFieldValidation pour le formatage Acrobat AF et les assistants de validation, et EnsureLoadedFieldAppearanceStream pour figer la valeur qu'une liseuse dessine réellement

HotPDF est un composant PDF VCL natif pour Delphi et C++Builder, et ces trois méthodes opèrent sur un document ouvert via LoadFromFile plutôt que sur un document conçu à partir de zéro. Chacune prend un index de champ basé sur 0 dans le tableau /Fields de l'AcroForm — dans le même ordre que celui renvoyé par GetLoadedFormFieldNames — et écrit directement dans le dictionnaire du champ, de sorte que l'ensemble du travail consiste à charger, indexer, associer et appeler SaveLoadedDocument. La création du jeu de champs lui-même constitue une tâche distincte, abordée dans l'article sur l'ajout de champs AcroForm à un PDF chargé dans Delphi, tandis que cette page traite de l'attribution de fonctions arithmétiques et de formatage à un jeu de champs existant

Pourquoi un formulaire PDF a-t-il besoin de scripts pour calculer ses propres champs ?

Car le PDF ne dispose pas de couche de tableur. Un champ de texte contient une chaîne, pas une formule, et rien dans le fichier ne recalcule un total lorsqu'un poste change, à moins qu'un script ne soit associé pour le faire. Le mécanisme utilisé est le dictionnaire des actions supplémentaires du champ, /AA (ISO 32000-1 §12.7.5), qui fait correspondre des événements déclencheurs à des actions : un déclencheur de frappe (K) à l'arrivée des caractères, un déclencheur de format pour l'affichage, un déclencheur de validation (V) lors de la validation, et un déclencheur de calcul (C) qui se lance dès qu'un champ du formulaire est modifié. Chaque action est une action JavaScript, /S /JavaScript (ISO 32000-1 §12.6.4.14), et the script exécuté effectue concrètement la somme ou le reformatage. Acrobat propose une bibliothèque d'assistants prêts à l'emploi — AFSimple_Calculate, AFNumber_Format, AFDate_FormatEx, etc. — ainsi, le script associé consiste généralement en un appel d'une ligne à cette bibliothèque plutôt qu'en un code personnalisé

Un détail détermine si un formulaire comportant plusieurs étapes fournit le bon résultat : l'ordre de calcul. Acrobat traite chaque champ qui comporte une action de calcul, et il les exécute dans l'ordre répertorié par le tableau /CO de l'AcroForm, et non dans l'ordre des pages. ApplyLoadedFieldCalculation associe l'action de calcul au champ que vous nommez ; lorsqu'un champ calculé en alimente un autre — un sous-total lu par une ligne de taxe, puis un total général qui lit les deux — vous devez vous assurer que l'ordre /CO répertorie chaque entrée avant le champ qui la consomme, sinon le total général lira un sous-total obsolète et affichera un retard d'une modification

Comment configurer un formulaire PDF pour calculer automatiquement les totaux ?

ApplyLoadedFieldCalculation prend l'index du champ cible, une opération et les noms des champs sources qu'il doit agréger. HotPDF écrit AFSimple_Calculate("SUM", ["lineTotal.1", "lineTotal.2", ...]) dans l'action de calcul du champ cible, et l'énumération THPDFFieldCalcOp sélectionne la fonction : fccSum, fccAverage, fccProduct, fccMinimum et fccMaximum correspondent aux fonctions Acrobat SUM, AVG, PRD, MIN et MAX. Les noms sources sont des noms de champs AcroForm, les mêmes chaînes renvoyées par GetLoadedFormFieldNames, de sorte que le calcul d'un total sur trois postes et des frais d'expédition s'effectue en un seul appel

var
  Pdf: THotPDF;
  Names: THPDFAnsiStringArray;
  i, TotalIdx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf');
    Names := Pdf.GetLoadedFormFieldNames;
    TotalIdx := -1;
    for i := 0 to High(Names) do
      if Names[i] = 'grandTotal' then TotalIdx := i;
    if TotalIdx >= 0 then
    begin
      Pdf.ApplyLoadedFieldCalculation(TotalIdx, fccSum,
        ['lineTotal.1', 'lineTotal.2', 'lineTotal.3', 'shipping']);
      Pdf.EnsureLoadedFieldAppearanceStream(TotalIdx);
    end;
    Pdf.SaveLoadedDocument('invoice-live.pdf');
  finally
    Pdf.Free;
  end;
end;

L'appel à EnsureLoadedFieldAppearanceStream sur le total n'est pas cosmétique. Le calcul s'exécute au sein de la liseuse, ainsi, tant qu'un lecteur prenant en charge les scripts n'a pas ouvert le fichier, le champ de total ne possède pas de contenu rendu propre. Figer un flux d'apparence lui fournit une valeur à afficher en attendant, ce qu'un travail d'impression ou un générateur de vignettes récupérera s'il n'exécute jamais le script

Formatage et validation des champs avec la suite d'assistants AF

ApplyLoadedFieldValidation associe les scripts de formatage, de frappe et de validation en un seul appel, et le type THPDFFieldValidationKind transmis détermine l'assistant Acrobat à connecter. Transmettez fvkCurrency et HotPDF écrit AFNumber_Format(2,0,0,0,"$ ",true) comme script de formatage avec l'instruction AFNumber_Keystroke correspondante ; fvkNumber utilise "." pour l'argument de séparation, fvkPercentage émet AFPercent_Format, et fvkDate émet AFDate_FormatEx("dd/mm/yyyy") à moins que vous ne passiez votre propre masque dans l'argument DateFormat. Les formats spéciaux — fvkZipCode, fvkSSN et fvkPhone — correspondent à AFSpecial_Format avec le code qu'Acrobat réserve à chacun, et fvkArbitraryMask prend une chaîne de masque via l'argument Mask pour tout ce que ces préréglages ne couvrent pas. Pour les types numériques, HotPDF associe également un script AFRange_Validate afin que les saisies hors limites soient refusées lors de la validation

// Format the invoice date as ISO, the amount as currency, and mask a tax id
Pdf.ApplyLoadedFieldValidation(DateIdx,   fvkDate,          'yyyy-mm-dd', '');
Pdf.ApplyLoadedFieldValidation(AmountIdx, fvkCurrency,      '', '');
Pdf.ApplyLoadedFieldValidation(TaxIdIdx,  fvkArbitraryMask, '', '999-99-9999');
Pdf.EnsureLoadedFieldAppearanceStream(AmountIdx);

Les assistants AF couvrent les cas d'utilisation courants afin que vous n'ayez jamais à écrire de JavaScript manuellement pour eux. Lorsqu'un champ requiert une logique qu'aucun préréglage n'exprime — une somme de contrôle, une règle entre champs, une expression régulière personnalisée — associez vous-même le script brut à l'aide des méthodes d'action au niveau du champ décrites dans la création de champs et d'actions AcroForm avec HotPDF, qui reçoivent directement une chaîne JavaScript. Les deux voies écrivent dans le même dictionnaire /AA, de sorte qu'un format de devise prédéfini sur un champ et une validation personnalisée sur un autre coexistent dans le même document

Pourquoi devez-vous régénérer le flux d'apparence ?

Parce que la chaîne de valeur et les pixels dessinés par la liseuse sont deux choses distinctes en PDF, et que modifier l'une ne change pas l'autre. Un champ stocke sa valeur dans /V, mais ce qu'un lecteur affiche provient du flux d'apparence du champ, /AP /N. Modifiez /V via le graphe d'objets et un lecteur strict affichera toujours l'ancienne apparence jusqu'à ce qu'un élément la régénère. Le dictionnaire du formulaire comporte un indicateur /NeedAppearances qui demande au lecteur de recréer chaque apparence à l'ouverture, et Acrobat le respecte — mais les moteurs de rendu intégrés aux navigateurs, les liseuses mobiles, les serveurs d'impression et les générateurs de vignettes l'ignorent fréquemment et affichent le flux déjà présent, ou rien du tout. EnsureLoadedFieldAppearanceStream comble cette lacune en créant lui-même le Form XObject /AP /N, à partir du champ /DA, /V et /Rect, afin que la valeur affichée voyage au sein du fichier plutôt que de dépendre de la liseuse pour la redessiner

// Bake an appearance for every field before saving, so values render everywhere
Names := Pdf.GetLoadedFormFieldNames;
for i := 0 to High(Names) do
  Pdf.EnsureLoadedFieldAppearanceStream(i);
Pdf.SaveLoadedDocument('invoice-flat-appearance.pdf');

Là où les scripts AF cessent de fonctionner

Chaque script AF* dépend de la présence d'un moteur JavaScript dans la liseuse, et la plupart des liseuses n'en ont pas. Acrobat et une poignée de produits de bureau exécutent ces assistants ; les lecteurs PDF intégrés à Chrome, Edge, Firefox et aux principales plateformes mobiles, ainsi que les trameurs côté serveur et les flux d'impression, n'exécutent aucun JavaScript de formulaire. Dans ces lecteurs, le calcul ne se lance jamais et le format ne s'applique pas — le champ affiche simplement le flux d'apparence avec lequel il a été enregistré pour la dernière fois, c'est pourquoi l'intégration de /AP /N est cruciale. Considérez la couche AF* comme une commodité pour le sous-ensemble d'utilisateurs disposant d'un lecteur prenant en charge les scripts, jamais comme une garantie. Tout total devant être exact sur le serveur — un montant de facture à facturer, un chiffre fiscal à déclarer — doit être recalculé de votre côté après le retour du formulaire, car vous ne pouvez pas supposer que le lecteur qui l'a rempli a exécuté une seule ligne de votre script

Un autre cas mérite d'être cité : les formulaires fournis sous forme d'XFA. Un formulaire XFA dynamique intègre son propre moteur de calcul dans une charge utile XML dont les assistants AF* ne savent rien, de sorte que l'association de AFSimple_Calculate à un champ XFA ne produit aucun effet. Aplatissez d'abord le formulaire XFA vers un formulaire AcroForm standard, comme décrit dans l'article sur l'aplatissement XFA en AcroForm dans Delphi, et les trois méthodes présentées ici s'appliqueront alors aux champs AcroForm obtenus exactement comme pour tout autre formulaire chargé

Les formulaires AcroForm à calcul automatique et auto-formatage se résument à trois appels sur un document chargé — associer le calcul, associer le formatage et la validation, figer l'apparence — suivis de SaveLoadedDocument. L'API pour formulaires chargés présentée ici fait partie du composant HotPDF pour Delphi et C++Builder, qui documente les énumérations complètes THPDFFieldValidationKind and THPDFFieldCalcOp ainsi que le reste de la structure AcroForm