Article Technique

Running AcroForm JavaScript in Delphi with PDFium Component

Un formulaire dont les totaux se recalculent dans Acrobat mais restent figés dans votre propre visualiseur Delphi n'est presque jamais un bug de rendu. PDFium désactive tout JavaScript de document à moins que l'hôte n'y associe une plateforme JS, et PDFium Component configure automatiquement cette plateforme à partir de la version 2.13.2. Ainsi, le JavaScript AcroForm, les scripts de niveau document et les calculs de champs s'exécutent dès que la DLL incluant le moteur V8 est chargée. L'hôte garde le contrôle grâce à un ensemble d'événements qui peuvent afficher, rediriger ou bloquer tout ce que le script tente de faire

La situation qui rend ces notions importantes est récurrente : « mon formulaire de commande calcule les totaux des lignes dans Acrobat mais pas dans mon application », « app.alert ne se déclenche jamais », « le champ de date ne se formate pas automatiquement ». Toutes ces anomalies proviennent du même mécanisme, et l'étudier dix minutes en vaut la peine car il constitue également votre barrière de sécurité face aux documents malveillants

Pourquoi les calculs de formulaires PDF ne fonctionnent-ils pas dans PDFium ?

PDFium traite le JavaScript du document de manière strictement facultative (opt-in) : si le membre m_pJsPlatform de l'environnement de remplissage de formulaire est NULL, tout script dans le document est silencieusement ignoré. Pas d'erreur, pas de ligne de journal, pas d'exécution partielle. Les champs calculés conservent leur dernière valeur stockée, app.alert disparaît, les scripts de formatage ne s'exécutent pas. Acrobat, qui intègre toujours son propre moteur JavaScript, exécute le même fichier sans problème. C'est pourquoi cette divergence ressemble à s'y méprendre à un bug dans votre code, alors qu'il s'agit d'un comportement par défaut délibéré de la bibliothèque sous-jacente

PDFium Component présentait une seconde particularité historique. Jusqu'à la version 2.13.1, la liaison n'associait m_pJsPlatform qu'au sein de la branche d'initialisation XFA. Ainsi, les documents AcroForm classiques, qui constituent l'écrasante majorité des PDF scriptés, ne disposaient d'aucune plateforme JavaScript, même en présence de la DLL V8. La version 2.13.2 a extrait cette association de l'initialisation XFA : InitializeFormFill construit désormais la table IPDF_JsPlatform dès que V8FeaturesAvailable renvoie True, que le document soit au format XFA ou non. Concrètement, les scripts de niveau document, les chaînes de calcul de champs et app.alert s'exécutent désormais également dans les documents AcroForm classiques

De quelle DLL V8 le JavaScript AcroForm a-t-il besoin dans Delphi ?

Le JavaScript AcroForm requiert un binaire PDFium compilé avec le moteur V8, que PDFium Component charge sous le nom de pdfium.v8.dll lorsque l'indicateur global EnableV8Engine est à True avant le premier chargement de la bibliothèque. Il y a ici un piège à connaître : le composant détecte automatiquement les documents XFA en recherchant des marqueurs /XFA et active EnableV8Engine pour vous. Cependant, un document AcroForm classique contenant du JavaScript ne porte pas de marqueur XFA, aucune détection automatique n'a donc lieu. Si votre visualiseur doit exécuter des scripts de formulaires, définissez l'indicateur vous-même, une fois, avant le chargement du premier document ; une fois que la DLL pdfium.dll classique est chargée, le processus ne peut plus changer de moteur

uses PDFium;

procedure TViewerForm.FormCreate(Sender: TObject);
begin
  // Must run before the singleton PDFium library first loads;
  // once plain pdfium.dll is in the process it cannot be swapped
  EnableV8Engine := True;
end;

procedure TViewerForm.OpenDocument(const FileName: string);
begin
  FPdf.LoadDocument(FileName);
  if not V8FeaturesAvailable then
    StatusBar.SimpleText :=
      'JavaScript disabled: pdfium.v8.dll not deployed';
end;

V8FeaturesAvailable constitue le test d'exécution fiable : il indique si le binaire chargé expose réellement les fonctions dépendantes de V8, de sorte que la vérification fonctionne quel que soit le fichier DLL déployé. Le même moteur et le même indicateur régissent le rendu XFA dynamique ; le fonctionnement de la détection XFA et de la sélection automatique de V8 est traité dans la détection des formulaires XFA et l'extraction des paquets XFA avec PDFium

Événements hôtes pour les alertes, l'impression, la messagerie et la soumission de formulaires

Toute action visible d'un script est renvoyée à votre code via des événements de TPdf, et chacun d'eux se traduit par un comportement neutre par défaut s'il n'est pas assigné. OnJavaScriptAlert reçoit le message, le titre, le type de bouton et l'icône de app.alert et vous permet de renvoyer le bouton cliqué ; OnJavaScriptResponse gère les saisies app.response, y compris l'indicateur de mot de passe ; OnJavaScriptBeep, OnJavaScriptPrint, OnJavaScriptMail et OnJavaScriptSubmitForm transmettent les demandes de bip, d'impression, d'envoi d'e-mail et de soumission de formulaire avec leurs paramètres complets. Un visualiseur qui n'assigne aucun de ces événements effectue tout de même les rendus et les calculs correctement ; le document ne peut simplement pas ouvrir de boîtes de dialogue, imprimer ou envoyer de données

procedure TViewerForm.PdfJavaScriptAlert(Sender: TObject;
  const Msg, Title: WString; ButtonType, Icon: Integer;
  var Result_: Integer; var Handled: Boolean);
begin
  // Route app.alert through the VCL so it is owned by our window
  Result_ := MessageBox(Handle, PWideChar(Msg), PWideChar(Title),
    MB_OK or MB_ICONINFORMATION);
  Handled := True;
end;

procedure TViewerForm.PdfJavaScriptSubmitForm(Sender: TObject;
  const Url: WString; const Data: TBytes);
begin
  // Nothing is transmitted unless this handler chooses to send it
  LogAudit(Format('Document requested form submit to %s (%d bytes)',
    [Url, Length(Data)]));
end;

Cette distinction de comportement est importante pour la modélisation des menaces. OnJavaScriptMail et OnJavaScriptSubmitForm sont de simples notifications : PDFium Component n'effectue aucune action réseau ou MAPI par lui-même. Ainsi, un document malveillant appelant this.submitForm ou this.mailDoc contre un hôte non préparé ne produit strictement rien. L'hôte doit choisir d'intervenir en assignant un gestionnaire et en implémentant lui-même le transport, ce qui signifie que la décision de transmettre des données hors de la machine vous appartient toujours, gérée dans votre code avec l'URL et le contenu sous les yeux

Comment empêcher un PDF malveillant de lancer des URL ?

Les actions URI constituent le seul point où le comportement historique par défaut était actif plutôt que passif, c'est pourquoi elles ont fait l'objet d'un droit de veto dédié. Avant la version 2.13.1, le callback FFI_DoURIActionWithKeyboardModifier exécutait sans condition toute URI fournie par un champ XFA, offrant à un document malveillant la possibilité de lancer des URL arbitraires sur simple clic. OnXfaUriAction corrige cela : le composant consulte l'événement avant l'exécution, et un gestionnaire définissant Handled à True supprime complètement l'appel ShellExecuteW par défaut. Laisser l'événement non assigné conserve l'ancien comportement pour des raisons de compatibilité, un visualiseur soucieux de sécurité doit donc toujours l'assigner

procedure TViewerForm.PdfXfaUriAction(Sender: TObject;
  const Uri: WString; Modifiers: Integer; var Handled: Boolean);
begin
  // Veto anything that is not plain https; the default would
  // otherwise ShellExecuteW the URI as-is
  if not SameText(Copy(Uri, 1, 8), 'https://') then
  begin
    LogAudit('Blocked URI action: ' + Uri);
    Handled := True;
  end;
end;

Une liste d'autorisation sur le protocole est un minimum ; un visualiseur plus strict demandera confirmation à l'utilisateur ou redirigera tout le trafic vers son propre composant de navigation isolé. Cette logique de veto s'étend à tout le jeu d'événements de la version v2.13.1 : OnXfaEmail, OnXfaHttpRequest et OnXfaOpenFile exposent chacun les paramètres textuels de l'action demandée tandis que le composant n'effectue aucun transport réseau ou fichier. La manière dont ces gardes s'intègrent dans une stratégie plus large face aux documents non approuvés est traitée dans la mise en place d'une prévisualisation PDF sécurisée dans Delphi

Écriture dans le champ actif avec SetFocusedFormFieldText

TPdf.SetFocusedFormFieldText, ajouté à la version 2.13.2, est le pendant d'écriture de FocusedFormFieldText : il sélectionne tout le texte du champ actif via FORM_SelectAllText et le remplace via FORM_ReplaceSelection, exactement comme si l'utilisateur l'avait saisi. Les données entrant par le canal de saisie interactive, les scripts de touche, de format et de calcul associés au champ se déclenchent de la même manière que lors d'une saisie manuelle. C'est donc la primitive appropriée pour le remplissage programmatique dans un visualiseur où le JavaScript est actif. Le parcours des champs et la gestion du focus sont détaillés dans la navigation dans les champs de formulaire avec PDFium Component

if FPdf.FocusedFormFieldIndex >= 0 then
begin
  if not FPdf.SetFocusedFormFieldText('42.50') then
    ShowMessage('No focused field accepts text on this page');
  // AcroForm: value commits to /V when focus leaves the field.
  // XFA: the write stays in the in-memory edit buffer only and
  // will not survive a save
end;

L'asymétrie de persistance signalée dans ce commentaire constitue une limite technique réelle. Pour les champs de texte et les listes déroulantes AcroForm, la valeur modifiée est écrite dans l'entrée /V du champ lors de la perte de focus et persiste après l'enregistrement. Pour les champs de texte XFA, l'écriture ne reste que dans le tampon de saisie en mémoire car PDFium ne propose aucune API publique pour réinjecter les valeurs de widgets dans le paquet de jeux de données (datasets) XFA ; enregistrer le document ne conservera pas la modification. Si l'édition durable des données XFA est requise, il convient de modifier le XML des jeux de données directement plutôt que le widget

Limites à connaître avant le déploiement

Deux limites réelles subsistent. Premièrement, l'API JavaScript implémentée par PDFium est un sous-ensemble fonctionnel du modèle d'objet d'Acrobat, ciblé sur le cœur de la gestion de formulaires : accès aux champs, événements de calcul et de format, app.alert et app.response, demandes d'impression, d'e-mail et de soumission. Les documents s'appuyant sur des objets exotiques propres à Acrobat perdront en fonctionnalités, souvent silencieusement, testez donc les formulaires réels de vos utilisateurs au lieu de présumer d'une parité complète. Deuxièmement, activer V8 impose de déployer pdfium.v8.dll, un binaire sensiblement plus lourd que la version classique ; un visualiseur qui n'a pas besoin de scripts ou de XFA peut tout à fait conserver la DLL plus légère et ne pas attacher m_pJsPlatform, ce qui constitue en soi une posture de sécurité valide

La démarche qui en découle est des plus simples : définir EnableV8Engine au démarrage, assigner les événements d'alerte et de réponse pour que les boîtes de dialogue s'intègrent nativement, assigner OnXfaUriAction et consigner dans les journaux les événements de messagerie et de soumission, puis laisser le reste à son comportement par défaut neutre jusqu'à l'apparition d'un besoin spécifique. La référence complète des événements et l'API de remplissage de formulaires sont documentées sur la page du produit PDFium Component