Une facture AcroForm avec une poignée de champs texte et aucun contenu XFA s’ouvre sans problème à travers un pdfium.dll simple, pourtant le même fichier peut faire renvoyer NULL à FPDFDOC_InitFormFillEnvironment dès que le process fait tourner le pdfium.v8.dll avec XFA. Les octets du document sont identiques dans les deux exécutions. Ce qui diffère tient en un entier : les builds XFA de PDFium exigent que FPDF_FORMFILLINFO.version vaille 2, et une liaison qui écrit 1 pour les documents sans XFA est retournée avant que PDFium ait regardé un seul callback
Pourquoi FPDFDOC_InitFormFillEnvironment rejette-t-il un PDF AcroForm simple ?
Le rejet vient du binaire, et le document n’y est pour rien. L’en-tête de remplissage de formulaire, fpdf_formfill.h dans PDFium et sa traduction Pascal dans FPdfFormFill.pas, stipule qu’une compilation avec le module XFA exige la version 2, et le dit deux fois dans le même bloc de commentaires. Une compilation sans XFA accepte 1 ou 2. Sous une liaison qui envoie la version 1 pour les fichiers non XFA, une seule des quatre combinaisons binaire-document échoue : un enregistrement en version 1 confié à pdfium.v8.dll avec un fichier AcroForm ordinaire. La DLL simple prend cet enregistrement. La DLL V8 prend un enregistrement en version 2 pour n’importe quel document. Un vrai formulaire XFA sur la DLL V8 ne reçoit jamais la version 1 d’une liaison qui déduit la version du document, donc la case qui échoue est précisément celle à laquelle personne ne pense à tester
Une liaison écrite à la main voit FPDFDOC_InitFormFillEnvironment renvoyer NULL à ce moment et n’a plus rien pour avancer. TPdf de PDFium Component signalerait un environnement refusé par une EPdfError pendant Active := True, mais il écrit toujours la version 2, donc cette case ne s’y présente jamais. Tomber dans cette case est facile. Un hôte met le global EnableV8Engine à True parce qu’il veut le JavaScript AcroForm, et à partir de là chaque PDF du process passe par le binaire V8. Ou l’hôte ne touche jamais au drapeau, mais le premier fichier qu’il ouvre porte une entrée /XFA dans son dictionnaire AcroForm ou /NeedsRendering true dans son catalogue : quand la bibliothèque PDFium n’est pas encore chargée, TPdf balaie le fichier à la recherche de ces deux marqueurs et active EnableV8Engine avant de charger la DLL. Dans les deux cas, le choix tient pour le reste du process, et chaque fichier AcroForm simple ouvert ensuite rencontre la build XFA. Quel binaire livrer en premier lieu est couvert dans le guide du déploiement de la DLL PDFium et du diagnostic des échecs de chargement ; tout ce qui suit suppose que la build V8 est déjà chargée
Ce que le champ version de FPDF_FORMFILLINFO promet
FPDF_FORMFILLINFO.version indique à PDFium quelle part de l’enregistrement il peut lire. La version 1 couvre la partie stable : Release, les callbacks de FFI_Invalidate à FFI_DoGoToAction, et le pointeur m_pJsPlatform. La version 2 ajoute une queue expérimentale. Une build sans XFA qui reçoit la version 2 appellera aussi les callbacks expérimentaux qu’elle y trouve ; une build avec XFA n’accepte rien de moins. L’en-tête ne mentionne jamais le document. En écrivant 2, vous promettez que la mémoire après m_pJsPlatform existe et que chaque emplacement y contient soit une fonction cdecl valide, soit NULL
Cette queue commence par xfa_disabled, un FPDF_BOOL ignoré en dessous de la version 2, suivi de dix-sept pointeurs de fonction de FFI_DisplayCaret à FFI_DoURIActionWithKeyboardModifier. Quinze d’entre eux, de FFI_DisplayCaret à FFI_PutRequestURL, sont documentés comme "Required for XFA, otherwise set to NULL". Les deux derniers, FFI_OnFocusChange et FFI_DoURIActionWithKeyboardModifier, sont optionnels pour tout hôte, et l’en-tête ajoute qu’un hôte en version 2 qui laisse FFI_DoURIActionWithKeyboardModifier non réglé voit ses clics URI redirigés vers FFI_DoURIAction. Tout bien pesé, NULL est la valeur prescrite pour un hôte qui ne pilote pas XFA. Un enregistrement remis à zéro avec FillChar et estampillé version 2 respecte le contrat sur une build simple exactement comme un enregistrement en version 1, et c’est aussi le seul enregistrement qu’une build XFA acceptera
Le côté JavaScript suit le même schéma. m_pJsPlatform est inutilisé par les builds sans V8 ; sur une build V8, un pointeur NULL arrête le JavaScript pendant que le document se rend. L’enregistrement IPDF_JsPlatform vers lequel il pointe a son propre champ version, et l’en-tête dit que celui-ci doit actuellement valoir 2
Le piège : laisser le document choisir la version de l’ABI
L’erreur tient en un conditionnel qui se lit de façon sensée tout seul. Une liaison écrite à la main doit déjà décider si les callbacks XFA valent la peine d’être câblés, et il est tentant de laisser le même test choisir le numéro de version
// Une forme tentante pour une liaison écrite à la main : la version suit le document
FillChar(FormInfo, SizeOf(FormInfo), 0);
IsXfa := FPDF_GetFormType(Doc) in [FORMTYPE_XFA_FULL, FORMTYPE_XFA_FOREGROUND];
if IsXfa and XfaFeaturesAvailable and V8FeaturesAvailable then
FormInfo.version := 2
else
FormInfo.version := 1; // tout document non XFA reçoit 1
FormHandle := FPDFDOC_InitFormFillEnvironment(Doc, FormInfo);
// sur pdfium.v8.dll avec un fichier AcroForm simple, FormHandle vaut nil ici
L’intention derrière cette branche est défensive : garder la version à 1 pour qu’une build XFA ne lise jamais des emplacements que l’hôte n’a pas remplis. Mais le FillChar de la ligne au-dessus les a déjà remplis avec NULL, ce que l’en-tête définit comme la valeur correcte pour un hôte qui ne fait pas tourner XFA. La branche se prémunit contre un danger que la déclaration a éliminé et marche droit dans celui contre lequel l’en-tête met en garde. Elle ne peut en plus pas être attrapée en testant des fichiers XFA, puisque ceux-ci prennent le chemin version 2 à chaque fois
PDFium Component construit l’enregistrement dans l’autre sens. Dès que TPdf.FormFill est True, l’ouverture d’un document remet l’enregistrement complet à zéro, écrit la version 2 et xfa_disabled := 1, et seulement ensuite demande ce dont le document a besoin. Énoncé comme règle pour toute liaison : réglez la version d’après l’enregistrement que vous avez déclaré, et ne laissez rien d’autre y toucher. Voici cette forme pour votre propre code, avec une enveloppe THostFormFillInfo déclarée plus bas
// FormInfo est un champ THostFormFillInfo de votre objet visionneuse, donc il survit
// au handle de formulaire ; HostInvalidate, HostGetPage et le reste sont vos propres
// callbacks cdecl
FillChar(FormInfo, SizeOf(FormInfo), 0);
FormInfo.Owner := Self;
FormInfo.Info.version := 2; // correspond à la disposition déclarée, toujours
FormInfo.Info.xfa_disabled := 1; // XFA reste désactivé tant qu’il n’est pas prouvé utilisable
FormInfo.Info.FFI_Invalidate := HostInvalidate;
FormInfo.Info.FFI_GetPage := HostGetPage;
FormInfo.Info.FFI_GetCurrentPage := HostGetCurrentPage;
// ... les autres callbacks de version 1
IsXfa := FPDF_GetFormType(Doc) in [FORMTYPE_XFA_FULL, FORMTYPE_XFA_FOREGROUND];
RuntimeReady := IsXfa and XfaFeaturesAvailable and V8FeaturesAvailable;
if RuntimeReady then
begin
FormInfo.Info.xfa_disabled := 0;
FormInfo.Info.FFI_DisplayCaret := HostDisplayCaret;
FormInfo.Info.FFI_PageEvent := HostPageEvent;
// ... les autres emplacements de version 2
end;
FormHandle := FPDFDOC_InitFormFillEnvironment(Doc, FormInfo.Info);
if FormHandle = nil then
raise Exception.Create('Cannot initialize form fill environment');
XfaRunning := RuntimeReady and (FPDF_LoadXFA(Doc) <> 0);
Où RuntimeReady et V8FeaturesAvailable ont leur place
Les vérifications de capacité décident quels emplacements sont remplis, et dans PDFium Component elles ne touchent jamais à la disposition. TPdf calcule la disponibilité à partir de trois faits : le document rapporte un type de formulaire XFA via TPdf.XFA, le global XfaFeaturesAvailable trouve les aides de chaînes XFA exportées par la DLL chargée, et V8FeaturesAvailable trouve les exports V8. Les callbacks de version 1 sont câblés pour chaque document, car AcroForm et XFA en dépendent tous les deux. Les dix-sept emplacements de version 2 sont affectés, et xfa_disabled remis à zéro, seulement quand les trois faits tiennent. Une fois l’environnement existant, FPDF_LoadXFA ne tourne que pour ce cas, et seul un retour non nul fait rapporter True par TPdf.XfaRuntimeAvailable. FPDF_LoadXFA est exporté même par les builds sans le moteur XFA et y renvoie simplement false, voilà pourquoi la vérification d’export seule ne suffit pas
Quand le document est XFA mais que le runtime manque, l’enregistrement reste en version 2 avec xfa_disabled à 1 et la queue laissée NULL. L’environnement s’ouvre, le contenu XFA reste statique, et TPdf lève OnXfaRuntimeMissing pour que l’hôte puisse suggérer de redémarrer sur pdfium.v8.dll. m_pJsPlatform suit une troisième règle : il est attaché dès que V8FeaturesAvailable est vrai, pour les fichiers AcroForm simples aussi, ce qui permet aux scripts au niveau document, aux calculs de champs et à app.alert de tourner sous la build V8. PageCount lit l’arbre de pages statique jusqu’à ce que XFA repagine le document via FFI_PageEvent ; à ce moment, le nouveau total est pris dans FPDF_GetPageCount et signalé via OnXfaPageCountChanged
Côté hôte, deux propriétés répondent à deux questions différentes. XFA dit ce que contient le fichier, via FPDF_GetFormType, quel que soit le binaire. XfaRuntimeAvailable dit si le moteur a réellement démarré pour ce document. Lisez la seconde après que Active est passé à True, comme le fait l’article sur la détection des formulaires XFA et l’extraction des paquets XFA, et laissez le champ version entièrement hors du raisonnement
procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
// Levé pendant l’ouverture quand le document est XFA mais que la DLL chargée ne peut
// pas faire tourner le moteur. L’environnement de formulaire existe toujours, XFA désactivé
StatusBar.SimpleText :=
'XFA form detected; restart with pdfium.v8.dll for dynamic rendering';
end;
procedure TMainForm.OpenDocument(const FileName: string);
begin
Pdf.Active := False;
Pdf.OnXfaRuntimeMissing := PdfXfaRuntimeMissing;
Pdf.FormFill := True;
Pdf.FileName := FileName;
Pdf.Active := True; // les fichiers AcroForm simples s’ouvrent aussi sous pdfium.v8.dll
if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
ShowStaticXfaNotice; // votre propre routine d’interface
end;
Les erreurs de disposition qui cassent l’enregistrement avant la lecture de la version
Un numéro de version correct ne sert que si les octets derrière lui siègent là où le compilateur C les a mis. Plusieurs erreurs de liaison produisent un enregistrement que PDFium lit de travers quelle que soit sa version, et l’une reste invisible en Win32. C’est déclarer FPDF_FORMFILLINFO comme packed record. En Win32 chaque champ fait quatre octets, donc les dispositions packed et alignées coïncident par hasard. En Win64 la struct C bourre quatre octets après version pour que Release atterrisse sur une frontière de 8 octets, et bourre encore après xfa_disabled ; une déclaration Pascal packed supprime les deux trous et décale chaque pointeur de callback de quatre octets. La déclaration dans FPdfFormFill.pas est un record simple pour cette raison
La seconde erreur est un enregistrement tronqué : une traduction qui s’arrête à m_pJsPlatform mais écrit la version 2. PDFium lit alors la queue dans la mémoire qui suit l’enregistrement. La troisième est la durée de vie. L’en-tête dit que l’enregistrement passé à FPDFDOC_InitFormFillEnvironment doit rester valide jusqu’à la fermeture du handle de formulaire renvoyé, donc un enregistrement dans une variable locale de la procédure qui ouvre le document est un pointeur pendant dès que cette procédure retourne. TPdf garde son enregistrement comme champ du composant, enveloppé dans un enregistrement externe qui place la struct PDFium à l’offset zéro et un pointeur arrière vers le TPdf propriétaire juste après, voilà comment chaque callback statique retrouve son objet
type
// Faux en Win64 : le packing supprime le bourrage que la struct C a après
// version, donc Release siège à l’offset 4 ici et à l’offset 8 dans PDFium
TBadFormFillInfo = packed record
version: Integer;
Release: Pointer;
// ... champs restants
end;
// Votre propre enveloppe : l’enregistrement PDFium d’abord, votre pointeur arrière après
THostFormFillInfo = record
Info: FPDF_FORMFILLINFO; // offset 0, donc pThis se reconvertit directement
Owner: TObject;
end;
PHostFormFillInfo = ^THostFormFillInfo;
procedure HostInvalidate(pThis: PFPDF_FORMFILLINFO; page: FPDF_PAGE;
left, top, right, bottom: Double); cdecl;
begin
try
// TMyViewer et InvalidatePdfRect représentent votre propre classe visionneuse
TMyViewer(PHostFormFillInfo(pThis).Owner).InvalidatePdfRect(
page, left, top, right, bottom);
except
// une exception Pascal ne doit pas se dérouler à travers les frames C++ de PDFium
end;
end;
Deux emplacements de plus méritent du soin. FFI_GetLocalTime renvoie une FPDF_SYSTEMTIME de 16 octets par valeur, et Delphi et MSVC sont en désaccord en Win32 sur la destination du pointeur de résultat caché, donc une implémentation Pascal corromprait le FPDF_FORMFILLINFO de l’appelant. PDFium actuel marque le membre inutilisé, et la déclaration Pascal porte un commentaire disant de le laisser à nil ; PDFium Component le fait. Chaque callback que le composant installe attrape les exceptions Pascal et renvoie la valeur d’échec documentée à la place, parce que l’ABI C n’a aucun moyen de faire repasser une exception par-dessus la frontière. L’article sur le durcissement de l’ABI PDFium en Delphi parcourt cette discipline avec FFI_OpenFile en exemple
Version de protocole et disponibilité des fonctionnalités sont deux axes
Un champ de version dans une structure de callbacks répond à la question de la taille de l’enregistrement et de ce qui peut y être lu. La détection de fonctionnalités répond à quels emplacements feront quelque chose. La première est fixée par le binaire natif et par la déclaration contre laquelle vous avez compilé ; la seconde varie par document, par table d’exports de DLL et par réglage de l’hôte. Le cas XFA a besoin des deux réponses à la fois, ce qui rend tentant de les replier en un seul booléen, et ce repli casse dès qu’une build impose une version minimale. Les formulaires XFA, que ISO 32000-1 §12.7.8 décrit comme une charge XML transportée à côté du dictionnaire AcroForm, sont la fonctionnalité de cette histoire. La disposition de l’enregistrement est le protocole, et PDFium est en droit de vérifier la disposition avant de lire le fichier
La même structure se retrouve partout où une bibliothèque C versionne ses structures : FPDF_LIBRARY_CONFIG, IPDF_JsPlatform, toute table de callbacks avec un en-tête de taille ou de version. Déclarez la disposition la plus récente que vous comprenez, remettez-la à zéro complètement, écrivez la version correspondante sans condition, et remplissez les emplacements d’après ce que le document et la DLL savent réellement faire. Si un futur fpdf_formfill.h introduit une version 3, le travail est une nouvelle déclaration et une affectation changée. Une branche dépendante du contenu du document devrait être revisitée pour chaque combinaison de build et de fichier, et celle qu’elle se trompe est d’habitude celle que personne n’a ouverte
La gestion d’enregistrement décrite ici fait partie de PDFium Component pour Delphi, Lazarus et C++Builder, avec la même déclaration servant les builds Win32 et Win64. Une application qui livre déjà pdfium.v8.dll pour les AcroForms pilotés par JavaScript peut ouvrir le reste de son archive, XFA ou non, à travers ce seul binaire sans traitement spécial de l’environnement de formulaire