Article technique

Détecter et extraire les formulaires XFA dans Delphi avec PDFium

Oui — PDFium prend en charge les formulaires XFA, avec une distinction majeure. Le composant PDFium peut détecter un formulaire XFA et en extraire les paquets template, datasets et config sous forme de XML brut en utilisant la DLL standard, avec une dépendance nulle au moment de l'exécution. Exécuter un formulaire XFA dynamique — c'est-à-dire mettre en page sa description XML dans des pages interactives actives — constitue un problème distinct qui nécessite en outre la version optimisée pour V8. La communauté demande régulièrement si PDFium "gère le XFA" comme s'il s'agissait d'une fonctionnalité binaire, et la réponse honnête est que la lecture de données XFA et le rendu d'un formulaire XFA sont deux choses très différentes avec des empreintes de dépendance très distinctes

Cet article présente ces deux aspects avec le composant PDFium, l'enveloppe VCL native autour de PDFium pour Delphi et C++Builder. Tout d'abord le chemin de détection et d'extraction statique — FormType, la propriété XFA et les lecteurs GetXfaPacket* —, puis le chemin d'exécution dynamique avec sa contrainte V8, et enfin les sondes de capacité qui vous permettent de gérer les erreurs proprement lorsqu'une DLL déployée ne peut pas exécuter le moteur

PDFium prend-il en charge les formulaires XFA ?

Considérez cela comme deux fonctionnalités distinctes, et non une seule. L'extraction statique de données XFA est toujours disponible : les exportations de bas niveau FPDF_GetXFAPacket* qui sous-tendent GetXfaPacketCount, GetXfaPacketName et GetXfaPacketContent sont compilées dans chaque version raisonnablement moderne de pdfium.dll, même dans un binaire où le XFA est désactivé. C'est pourquoi extraire le XML brut des paquets template, datasets et config d'un formulaire ne nécessite aucune dépendance d'exécution — il s'agit d'une pure lecture structurelle, sans solliciter de moteur. Le XFA dynamique constitue la seconde fonctionnalité : exécuter le formulaire, évaluer ses calculs et laisser le moteur de mise en page XFA le paginer. Ce chemin n'existe que dans une version de PDFium compilée avec la prise en charge de XFA au-dessus du moteur JavaScript V8, et c'est le point sur lequel portent réellement la plupart des discussions concernant la prise en charge de XFA par PDFium

Les normes encadrent cette distinction de façon claire. Le XFA est défini en dehors de la grammaire PDF de base dans la spécification Adobe XFA 3.3 (2012), et la norme PDF 1.7 §8.6.7 décrit comment un formulaire XFA est transporté dans un fichier PDF : les paquets du formulaire résident sous l'entrée /XFA du dictionnaire AcroForm, et un document qui doit être mis en page par un processeur XFA avant toute utilisation définit l'indicateur /NeedsRendering true dans le catalogue. Ces deux marqueurs correspondent exactement à ce que le composant PDFium recherche, et c'est ce sur quoi vous pouvez vous baser pour analyser un fichier inconnu

En quoi le XFA diffère-t-il d'un AcroForm ?

Un AcroForm est le formulaire PDF classique : des annotations de type widget — champs de texte, cases à cocher, groupes d'options — ancrées à des coordonnées fixes sur des pages PDF standard. La page affichée correspond à la page du fichier, et les champs se superposent à celle-ci. Le XFA adopte la démarche inverse. Le formulaire interactif est entièrement décrit en XML, et dans un document XFA complet (dynamique), les pages PDF ne servent que d'espace réservé — le contenu réel est généré à l'ouverture par un moteur XFA qui lit le modèle XML et le le met en page, en l'étendant ou le réduisant pour l'adapter aux données. C'est ce qu'annonce l'indicateur /NeedsRendering true : sans processeur XFA, les pages statiques peuvent être vierges ou afficher uniquement un message demandant d'ouvrir le fichier dans un lecteur compatible

La charge utile XFA elle-même est un ensemble de paquets nommés, et trois d'entre eux contiennent tout ce dont la plupart des intégrations ont besoin. Le paquet template correspond à la définition du formulaire — champs, disposition, calculs et scripts. Le paquet datasets contient les données de l'instance, à savoir les valeurs saisies dans les champs sous forme d'arborescence XML. Le paquet config véhicule les instructions de traitement pour le moteur XFA. D'autres paquets existent — localeSet, connectionSet, l'enveloppe xdp —, mais pour auditer un formulaire ou en extraire les données, template et datasets couvrent l'essentiel des besoins. Ces données n'étant que du XML stocké dans un flux, leur lecture ne requiert aucune exécution de code, ce qui explique fondamentalement pourquoi l'extraction statique s'effectue sans dépendance

Détecter un formulaire XFA dans Delphi

La détection commence comme toute tâche du composant PDFium : spécifiez le nom de fichier FileName, activez Active := True, puis vérifiez que l'ouverture a bien fonctionné. Active := True ne lève jamais d'exception — un fichier manquant, un mot de passe incorrect ou une DLL absente laisse simplement Active à False —, ce contrôle est donc obligatoire. Une fois le document ouvert, FormType indique le modèle de formulaire utilisé. Deux des valeurs de TPdfFormType concernent le XFA : ftXfaFull correspond à un formulaire XFA complet et dynamique (nécessitant un rendu), tandis que ftXfaForeground correspond au XFAF, le sous-ensemble de premier plan où le contenu XFA se superpose à des pages AcroForm par ailleurs normales pour que les pages statiques restent exploitables. Le booléen XFA est un raccourci pour "ce document est au format XFA, quel qu'en soit le type". Si vous manipulez également les champs de niveau widget sur un document XFAF ou AcroForm, le fonctionnement est détaillé dans le guide sur la navigation dans les champs de formulaires PDFium

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'application-form.pdf';
    Pdf.Active := True;
    if not Pdf.Active then
      Exit;                        // damaged, encrypted, or DLL missing
    case Pdf.FormType of
      ftXfaFull:
        Log('Full (dynamic) XFA form');
      ftXfaForeground:
        Log('XFAF: XFA layered over static AcroForm pages');
      ftAcroForm:
        Log('Classic AcroForm');
    else
      Log('No interactive form');
    end;
    if Pdf.XFA then
      Log('Document carries an XFA packet payload');
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Extraire les paquets XFA sous forme de XML brut

L'extraction constitue la partie à forte valeur ajoutée, exempte de dépendances. Protégez l'appel par XfaStaticReadable — ce qui permet de confirmer que les fonctions d'exportation de lecture de paquets sont résolues dans la DLL chargée —, puis parcourez les paquets par index. GetXfaPacketCount renvoie le nombre de paquets du formulaire, GetXfaPacketName fournit le nom de chacun, et GetXfaPacketContent renvoie les octets bruts, qui correspondent à du XML UTF-8 pour les paquets connus. Lorsque vous ciblez déjà un paquet spécifique, les assistants nommés GetXfaTemplate, GetXfaDatasets et GetXfaConfig résolvent le nom pour vous et renvoient directement un TBytes. Pour un travail de migration de données, le paquet utile est GetXfaDatasets : il fournit les valeurs de champs saisies sous forme de document XML à analyser et à associer à votre propre schéma, un travail similaire à celui effectué après l'extraction de texte d'un PDF

procedure ExtractXfaData(Pdf: TPdf; const Dir: string);
var
  I, Count: Integer;
  PacketName: string;
  Content, Datasets: TBytes;
begin
  if not Pdf.XfaStaticReadable then
    Exit;                          // packet reader exports not present
  Count := Pdf.GetXfaPacketCount;
  for I := 0 to Count - 1 do
  begin
    PacketName := Pdf.GetXfaPacketName(I);
    Content := Pdf.GetXfaPacketContent(I);   // raw UTF-8 XML
    TFile.WriteAllBytes(
      TPath.Combine(Dir, PacketName + '.xml'), Content);
  end;

  // Or jump straight to the filled-in field values by name:
  Datasets := Pdf.GetXfaDatasets;
  if Length(Datasets) > 0 then
    ParseAndMap(Datasets);
end;

Pourquoi le rendu XFA dynamique nécessite-t-il le moteur V8 ?

En conclusion : parce qu'un formulaire XFA dynamique est un petit programme et non un dessin statique. Le paquet template contient des scripts d'événement, des validations et des calculs FormCalc et JavaScript, et le moteur de mise en page XFA doit les exécuter pour déterminer l'apparence même des pages. PDFium n'intègre ce moteur que dans une version compilée avec prise en charge de XFA au-dessus de V8, le moteur JavaScript de Google — c'est pourquoi la DLL optimisée V8 (pdfium32v8.dll ou pdfium64v8.dll) est nettement plus volumineuse que la version standard. Sans V8, personne ne peut exécuter les scripts, de sorte qu'il n'y a pas de pages générées à afficher, seulement le XML brut lisible de manière statique

Le choix de cette version impose une contrainte incontournable : il s'agit d'une décision unique par processus. Un même processus ne peut pas charger à la fois la DLL pdfium.dll standard et la version optimisée V8 — elles exportent les mêmes symboles et V8 conserve un état global d'isolate — ; le composant PDFium doit donc choisir la version V8 avant le tout premier chargement de la bibliothèque. Pour automatiser cela, LoadDocument pré-analyse le fichier à la recherche des marqueurs /XFA et /NeedsRendering true (le même contrôle que celui exposé par la fonction autonome PdfFileLooksXfa) et définit EnableV8Engine := True avant le chargement s'ils sont trouvés. Une fois qu'un fichier pdfium.dll standard est chargé en mémoire, le commutateur ne peut plus s'appliquer, ce que signale le gestionnaire d'absence de runtime

begin
  // Peek for XFA before PDFium is loaded for the first time, so the
  // V8-enabled build can still be selected. PdfFileLooksXfa scans for
  // /XFA and /NeedsRendering true without parsing the whole file.
  if PdfFileLooksXfa('dynamic-form.pdf') then
    EnableV8Engine := True;

  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'dynamic-form.pdf';
    Pdf.Active := True;
    if Pdf.Active and Pdf.XFA then
    begin
      if Pdf.XfaRuntimeAvailable then
        RenderXfaPages(Pdf)          // engine is live: dynamic layout
      else
        ExtractXfaData(Pdf, 'C:\out'); // fall back to static packets
    end;
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Gérer proprement l'absence de runtime XFA

Trois contrôles indiquent précisément le niveau de prise en charge du XFA, et un code rigoureux les vérifie au lieu de faire des suppositions. XfaFeaturesAvailable is a global check that the XFA/V8 helper exports are present in the loaded DLL. XfaStaticReadable confirme, pour chaque document, que les lecteurs de paquets sont résolus — le garde-fou pour l'extraction. XfaRuntimeAvailable est le contrôle le plus déterminant : il n'est vrai que si ce document spécifique a effectivement activé le moteur XFA, ce qui implique qu'il s'agit de XFA, que la version V8 est chargée et que FPDF_LoadXFA a réussi. Un document peut indiquer XFA = True tout en ayant XfaRuntimeAvailable = False si la DLL déployée n'intègre pas le XFA ou si la version standard était déjà chargée

Lorsqu'un document XFA s'ouvre mais que le moteur ne peut pas s'exécuter, le composant PDFium ne prend pas de risques. Il maintient le formulaire dans le mode statique sécurisé plutôt que de requérir un environnement d'exécution dynamique qu'il ne peut pas honorer, et lève l'événement OnXfaRuntimeMissing une fois afin que l'hôte puisse réagir — généralement en invitant l'utilisateur à redémarrer avec la version compatible V8, tout en proposant les données lisibles de manière statique dans l'intervalle. L'intégration de ce gestionnaire fait la différence entre une solution de repli propre et un formulaire qui reste silencieusement vierge

procedure TForm1.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Fires once when an XFA document opens but the engine cannot run:
  // a plain pdfium.dll was already loaded, or the build lacks XFA/V8.
  ShowMessage('This is a dynamic XFA form. Restart with the ' +
    'V8-enabled build to render it; its packet data is still readable.');
end;

Soyez réaliste concernant ces limites dans votre propre produit. Si vous ne livrez pas la DLL compatible V8, les formulaires XFA dynamiques ne s'afficheront jamais pour vos utilisateurs — mais vous pouvez toujours les détecter, en extraire chaque paquet et en récupérer les données saisies, ce qui suffit pour de nombreuses tâches consistant à extraire des données de formulaires administratifs ou bancaires existants plutôt qu'à les présenter de manière interactive. Réservez la version V8 plus lourde aux cas nécessitant réellement un formulaire actif saisissable, et laissez les contrôles de capacité aiguiller chaque document vers la méthode adaptée. Si votre pipeline d'extraction gère également des fichiers intégrés, la même logique s'applique aux pièces jointes PDF dans Delphi

Les API FormType, XFA, GetXfaPacketCount, GetXfaPacketName, GetXfaPacketContent, GetXfaTemplate, GetXfaDatasets, GetXfaConfig et les sondes de capacité présentées ici font partie du composant PDFium pour Delphi et C++Builder