PDFium Component now sets FPDF_FORMFILLINFO.version to 2 for every form-fill environment it initializes, because the version a native PDFium build accepts is a property of that build, not of the document being opened. An XFA-enabled pdfium.v8.dll refuses version 1 outright, so a plain AcroForm PDF opened through it used to fail in FPDFDOC_InitFormFillEnvironment with no XFA anywhere in sight. The v3.116.0 fix is small, but the mistake behind it is a general one and worth naming: a protocol version field describes the memory layout the other side expects, and it must never be derived from whether you happen to need the features that layout carries
Why does FPDFDOC_InitFormFillEnvironment fail on a plain PDF with pdfium.v8.dll?
The environment fails because an XFA-enabled PDFium build validates the version field before it does anything else, and the old wrapper logic handed it a 1 whenever the current document was not an XFA form. The symptom in a Delphi host is an EPdfError raised from TPdf.InitializeFormFill with the message Cannot initialize form fill environment, thrown while opening an ordinary invoice or tax form that has nothing but AcroForm text fields. The same file opens fine against the plain pdfium.dll. The same DLL opens a real XFA document fine. Only the combination of the V8 build and a non-XFA document breaks, which is exactly the combination a host lands in after it turns on EnableV8Engine to get AcroForm JavaScript, or after the auto-selection in LoadDocument has already committed the process to pdfium.v8.dll for an earlier XFA file. That commitment is process-wide: EnableV8Engine is read before the first LoadLibrary, and once the XFA build is loaded every later plain PDF goes through the same environment setup against the same binary. The host did nothing wrong; the wrapper asked the wrong question when it filled in the record. If you are still deciding which binary to ship at all, our note on deploying the PDFium DLL and diagnosing load failures covers the plain versus V8 selection, and this article assumes the V8 build is already in the process
What does the version field in FPDF_FORMFILLINFO actually promise?
FPDF_FORMFILLINFO.version tells PDFium which fields of the record it is allowed to read, and the public header fpdf_formfill.h ties the acceptable values to how the library was compiled rather than to the document. In paraphrase, the contract has three parts. Version 1 covers the stable callbacks from FFI_Invalidate through FFI_DoGoToAction plus the m_pJsPlatform pointer. A build without the XFA module accepts either 1 or 2, and with 2 it will also call the additional experimental callbacks. A build with the XFA module requires 2, full stop, and the header repeats that requirement twice as if it expected people to miss it. Nowhere does the contract mention the document. The version is a statement about the record you allocated: with a 2 you are promising that the memory after m_pJsPlatform exists and holds either valid function pointers or NULL
The version-2 region is where all of the XFA machinery lives. It starts with xfa_disabled, an FPDF_BOOL the header describes as ignored below version 2 and meaningful only when the XFA module is compiled in, and continues with seventeen function pointers, FFI_DisplayCaret through FFI_DoURIActionWithKeyboardModifier. Each of those is documented as required for XFA and otherwise to be set to NULL. That phrasing is the key to the whole fix. NULL is not an error state for those slots; it is the documented state for a host that is not driving XFA. A record that has been cleared with FillChar and then marked as version 2 satisfies the contract on a non-XFA build exactly as well as a version-1 record does, and it is the only record an XFA build will take
The old selection tied the ABI to the document
The defect was a single conditional that looked reasonable in isolation. TPdf.InitializeFormFill computes a RuntimeReady flag from three facts: the document reports an XFA form type through TPdf.XFA, the XFA string helpers resolved through XfaFeaturesAvailable, and the V8 exports resolved through V8FeaturesAvailable. Before v3.116.0 that same flag also chose the version
// v3.115.0 and earlier: the ABI version followed the document
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
if RuntimeReady then
FFormFillInfo.Info.version := 2
else
FFormFillInfo.Info.version := 1;
// ... and the runtime-missing branch pinned it again
else if XFA then
begin
FFormFillInfo.Info.version := 1;
FFormFillInfo.Info.xfa_disabled := 1;
if Assigned(FOnXfaRuntimeMissing) then
FOnXfaRuntimeMissing(Self);
end;
Read it with the header in hand and the failure is obvious. RuntimeReady is false for every plain AcroForm document, so every plain document announced version 1. On pdfium.dll that is fine. On pdfium.v8.dll, which is the XFA-enabled build, PDFium checks the field, finds it below the required 2, and returns a null FPDF_FORMHANDLE, which CheckPdf turns into the exception above. The intent of the old code was defensive: keep version 1 so an XFA build never reads the unassigned version-2 slots. It defended against a problem the header already rules out and created one the header explicitly warns about. The corrected code decides the version once, up front, from what the record physically is
procedure TPdf.InitializeFormFill;
var
RuntimeReady: Boolean;
begin
FXfaRuntimeUsable := False;
FXfaPageCountOverride := -1; // sentinel: use the static page tree
if not FormFill then
Exit;
FillChar(FFormFillInfo, SizeOf(FFormFillInfo), 0);
FFormFillInfo.Pdf := Self;
// The full version-2 record is allocated and cleared above. PDFium
// accepts version 2 without XFA and requires it in every XFA-enabled
// build, including when this document contains no XFA form.
FFormFillInfo.Info.version := 2;
FFormFillInfo.Info.xfa_disabled := 1;
// RuntimeReady gates the XFA callbacks and xfa_disabled, never version.
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
...
Where RuntimeReady still belongs: the callbacks and xfa_disabled
RuntimeReady keeps its job as the gate for XFA behavior; it simply no longer touches the record layout. The version-1 callbacks, FFI_Invalidate, FFI_SetTimer, FFI_GetPage, FFI_DoURIAction, FFI_DoGoToAction, and the rest of that block, are wired unconditionally because AcroForm and XFA both depend on them. The seventeen version-2 pointers are assigned only inside the RuntimeReady branch, together with xfa_disabled := 0. When the document is XFA but the runtime is not there, the record stays at version 2 with xfa_disabled at 1 and the version-2 slots left NULL, and the wrapper raises OnXfaRuntimeMissing so the host can suggest restarting on pdfium.v8.dll. After the environment exists, FPDF_LoadXFA is called only when RuntimeReady was true, and only a true return sets FXfaRuntimeUsable, which is what TPdf.XfaRuntimeAvailable reports
if RuntimeReady then
begin
FFormFillInfo.Info.xfa_disabled := 0; // 0 = XFA enabled
FFormFillInfo.Info.FFI_DisplayCaret := FormFillDisplayCaret;
FFormFillInfo.Info.FFI_GetCurrentPageIndex := FormFillGetCurrentPageIndex;
FFormFillInfo.Info.FFI_SetCurrentPage := FormFillSetCurrentPage;
FFormFillInfo.Info.FFI_GotoURL := FormFillGotoURL;
FFormFillInfo.Info.FFI_GetPageViewRect := FormFillGetPageViewRect;
FFormFillInfo.Info.FFI_PageEvent := FormFillPageEvent;
FFormFillInfo.Info.FFI_PopupMenu := FormFillPopupMenu;
FFormFillInfo.Info.FFI_OpenFile := FormFillOpenFile;
FFormFillInfo.Info.FFI_EmailTo := FormFillEmailTo;
// ... FFI_UploadTo through FFI_DoURIActionWithKeyboardModifier
end
else if XFA then
begin
// Runtime unavailable: keep version 2, leave XFA disabled, tell the host.
if Assigned(FOnXfaRuntimeMissing) then
FOnXfaRuntimeMissing(Self);
end;
FFormHandle := FPDFDOC_InitFormFillEnvironment(FDocument, FFormFillInfo.Info);
CheckPdf(FFormHandle <> nil, 'Cannot initialize form fill environment');
if RuntimeReady then
FXfaRuntimeUsable := FPDF_LoadXFA(FDocument) <> 0;
Two details in that block are easy to get wrong when you write your own binding. FXfaPageCountOverride is reset to -1 as a sentinel before anything else happens, so PageCount falls back to the static page tree until FFI_PageEvent reports a repagination; a zero there would silently claim an empty document. And each of the version-2 callbacks is a static cdecl routine that recovers the owning TPdf from the record and swallows any Pascal exception before returning to PDFium, which is the discipline our note on hardening the PDFium ABI in Delphi spells out for FFI_OpenFile. Nothing about the version change relaxes either rule
Is version 2 safe when the DLL has no XFA module?
Yes, and the reason is in the record, not in a promise from the library. On a non-XFA build the header says version 2 causes the experimental callbacks to be called as well, so the question is what PDFium finds when it looks. TPdfFormFillInfo is a packed record whose Info member is the complete FPDF_FORMFILLINFO including every version-2 field, and InitializeFormFill clears the whole thing with FillChar before touching a byte. So on a plain pdfium.dll with a plain document the library sees version 2, xfa_disabled set, and NULL in every experimental slot, which is precisely the state the header prescribes for a host that is not implementing XFA. There is no truncated record for the library to read past, because the record was never shorter than version 2 in the first place. The old logic was defending a layout mismatch that the Pascal declaration had already eliminated
The boundary worth stating honestly is the one the record cannot cover. Version 2 on a plain document does not turn on JavaScript, XFA scripting, or any of the host events behind those callbacks. m_pJsPlatform is attached only when V8FeaturesAvailable is true, XFA stays disabled unless RuntimeReady was true, and TPdf.XFA continues to report the form type from FPDF_GetFormType regardless of what the environment negotiated. A host that wants to know whether dynamic XFA will actually render should keep reading XfaRuntimeAvailable after Active goes true, as our note on detecting XFA forms and extracting XFA packets recommends, rather than inferring anything from the version field
procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
// Fires from InitializeFormFill when the document is XFA but the loaded
// pdfium.dll cannot run the engine. The form environment still opens,
// because version 2 was passed either way; only the XFA runtime is off.
StatusBar.SimpleText :=
'XFA form detected; restart with pdfium.v8.dll to enable 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; // no longer throws on a plain PDF under pdfium.v8.dll
if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
ShowStaticXfaWarning;
end;
Protocol version and feature availability are two different axes
The general rule that falls out of this fix is that a version field in a callback structure answers the question "how big is this record and what may you read from it", while feature detection answers "which of those slots will do anything useful". The first is fixed by the native binary and by the Pascal declaration you compiled against. The second varies per document, per DLL export table, and per host configuration. Collapsing the two into one boolean is tempting because the XFA case happens to need both, but the moment a build enforces a minimum version the collapse breaks for every document that does not need the feature. XFA forms, described in ISO 32000-1 §12.7.8 as an XML payload living alongside the AcroForm dictionary, are the feature here; the record layout is the protocol, and PDFium is entitled to insist on the layout before it ever looks at the file. The same shape shows up anywhere a C library versions its structures: a viewer-info block, a render-options record, a platform callback table. The safe pattern is the one the corrected InitializeFormFill follows. Declare the newest layout you understand, clear it completely, set the version to match that layout unconditionally, and then let capability checks decide which slots to populate. If a future PDFium header adds a version 3, the change is to the declaration and to that one assignment, not to a document-dependent branch that will be wrong for whichever combination nobody tested
The corrected form-fill initialization ships in PDFium Component for Delphi, Lazarus, and C++Builder, and it applies on Win32 and Win64 alike since both builds share the same record declaration. If your application already selects pdfium.v8.dll for JavaScript-driven AcroForms, this is the change that lets it open the rest of your PDF archive through the same binary without special-casing the form environment