Een AcroForm-factuur met een handvol tekstvelden en geen XFA-inhoud opent prima via een gewone pdfium.dll, toch kan hetzelfde bestand FPDFDOC_InitFormFillEnvironment NULL laten teruggeven zodra het proces de XFA-achtige pdfium.v8.dll draait. De bytes van het document zijn in beide runs identiek. Wat verschilt is één geheel getal: XFA-builds van PDFium eisen dat FPDF_FORMFILLINFO.version 2 is, en een binding die 1 schrijft voor documenten zonder XFA wordt afgewezen voordat PDFium ook maar één callback aankijkt
Waarom weigert FPDFDOC_InitFormFillEnvironment een gewone AcroForm-PDF?
De afwijzing komt uit de binary, en het document heeft er niets over te zeggen. De form-fill-header, fpdf_formfill.h in PDFium en zijn Pascal-vertaling in FPdfFormFill.pas, stelt dat een build die met de XFA-module is gecompileerd versie 2 vereist, en hij zegt dat twee keer in hetzelfde commentaarblok. Een build zonder XFA accepteert 1 of 2. Onder een binding die versie 1 stuurt voor niet-XFA-bestanden faalt precies één van de vier combinaties van binary en document: een versie-1-record samen met een gewoon AcroForm-bestand naar pdfium.v8.dll. De gewone DLL neemt dat record aan. De V8-DLL neemt een versie-2-record aan voor elk document. Een echte XFA-form op de V8-DLL krijgt van een binding die de versie aan het document koppelt nooit versie 1, dus de falende cel is precies de ene waar niemand aan test denkt
Een handgeschreven binding ziet FPDFDOC_InitFormFillEnvironment op dat punt NULL teruggeven en heeft niets verder om op voort te borduren. TPdf in PDFium Component zou een geweigerde omgeving als een EPdfError melden tijdens Active := True, maar hij schrijft altijd versie 2, dus die cel komt daar nooit voor. In die cel terechtkomen is makkelijk. Een host zet de globale EnableV8Engine op True omdat hij AcroForm-JavaScript wil, en vanaf dat moment gaat elke PDF in het proces door de V8-binary. Of de host raakt de vlag nooit aan, maar het eerste bestand dat hij opent heeft een /XFA-entry in zijn AcroForm-woordenboek of /NeedsRendering true in zijn catalogus: als de PDFium-library nog niet is geladen, scant TPdf het bestand op die twee markeringen en zet hij EnableV8Engine aan vóórdat de DLL wordt geladen. In beide gevallen geldt de keuze voor de rest van het proces, en elk gewoon AcroForm-bestand dat daarna wordt geopend treft de XFA-build. Welke binary u in de eerste plaats meestuurt, behandelt de gids over het uitrollen van de PDFium-DLL en het diagnosticeren van laadfouten; alles hieronder gaat ervan uit dat de V8-build al is geladen
Wat het versieveld in FPDF_FORMFILLINFO belooft
FPDF_FORMFILLINFO.version vertelt PDFium hoeveel van het record hij mag lezen. Versie 1 dekt het stabiele deel: Release, de callbacks van FFI_Invalidate tot FFI_DoGoToAction, en de pointer m_pJsPlatform. Versie 2 voegt een experimentele staart toe. Een build zonder XFA die versie 2 ontvangen heeft, roept ook de experimentele callbacks aan die hij daar aantreft; een build met XFA neemt niets minder aan. De header noemt het document helemaal niet. Met een 2 belooft u dat het geheugen na m_pJsPlatform bestaat en dat elke plek erin ofwel een geldige cdecl-functie ofwel NULL bevat
Die staart begint met xfa_disabled, een FPDF_BOOL die onder versie 2 wordt genegeerd, gevolgd door zeventien functiepointers van FFI_DisplayCaret tot FFI_DoURIActionWithKeyboardModifier. Vijftien ervan, FFI_DisplayCaret tot FFI_PutRequestURL, zijn gedocumenteerd als "Required for XFA, otherwise set to NULL". De laatste twee, FFI_OnFocusChange en FFI_DoURIActionWithKeyboardModifier, zijn voor elke host optioneel, en de header voegt eraan toe dat een versie-2-embedder die FFI_DoURIActionWithKeyboardModifier leeg laat, zijn URI-kliks laat omleiden naar FFI_DoURIAction. Samengevat: NULL is de voorgeschreven waarde voor een host die XFA niet aanstuurt. Een record dat met FillChar is gewist en met versie 2 is gestempeld, voldoet op een gewone build precies zo aan het contract als een versie-1-record, en het is bovendien het enige record dat een XFA-build accepteert
De JavaScript-kant volgt hetzelfde patroon. m_pJsPlatform wordt door builds zonder V8 niet gebruikt; op een V8-build stopt een NULL-pointer de JavaScript-uitvoering terwijl het document rendert. Het record IPDF_JsPlatform waar hij naar wijst heeft een eigen veld version, en de header zegt dat dat er momenteel 2 moet zijn
De valkuil: het document de ABI-versie laten kiezen
De fout is één voorwaardelijke die op zichzelf zinnig oogt. Een handgeschreven binding moet toch al beslissen of de XFA-callbacks het bekabelen waard zijn, en het ligt voor de hand om dezelfde test het versienummer te laten kiezen
// Een verleidelijke vorm voor een handgeschreven binding: de versie volgt het 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; // elk niet-XFA-document krijgt 1
FormHandle := FPDFDOC_InitFormFillEnvironment(Doc, FormInfo);
// op pdfium.v8.dll met een gewoon AcroForm-bestand is FormHandle hier nil
De bedoeling van die tak is defensief: de versie op 1 houden zodat een XFA-build nooit slots leest die de host niet heeft gevuld. Maar de FillChar op de regel erboven heeft ze al met NULL gevuld, wat de header als de juiste waarde definieert voor een host die geen XFA draait. De tak bewaakt een gevaar dat de declaratie al heeft weggenomen en loopt ruggestuik recht het gevaar in waarvoor de header waarschuwt. Bovendien is hij niet te vangen door XFA-bestanden te testen, want die nemen elke keer het versie-2-pad
PDFium Component bouwt het record precies andersom. Zodra TPdf.FormFill True is, wist het openen van een document het volledige record, schrijft versie 2 en xfa_disabled := 1, en pas daarna vraagt hij wat het document nodig heeft. Als regel voor elke binding: stel de versie in vanuit het record dat u hebt gedeclareerd, en laat niets anders eraan komen. Hier is die vorm voor uw eigen code, met een wrapper THostFormFillInfo die verderop wordt gedeclareerd
// FormInfo is een THostFormFillInfo-veld van uw viewerobject, dus hij overleeft
// de form-handle; HostInvalidate, HostGetPage en de rest zijn uw eigen
// cdecl-callbacks
FillChar(FormInfo, SizeOf(FormInfo), 0);
FormInfo.Owner := Self;
FormInfo.Info.version := 2; // past bij de gedeclareerde layout, altijd
FormInfo.Info.xfa_disabled := 1; // XFA blijft uit totdat bruikbaarheid is bewezen
FormInfo.Info.FFI_Invalidate := HostInvalidate;
FormInfo.Info.FFI_GetPage := HostGetPage;
FormInfo.Info.FFI_GetCurrentPage := HostGetCurrentPage;
// ... de andere versie-1-callbacks
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;
// ... de overige versie-2-slots
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);
Waar RuntimeReady en V8FeaturesAvailable thuishoren
Capability-checks bepalen welke slots worden gevuld, en in PDFium Component raken ze de layout nooit aan. TPdf stelt gereedheid vast uit drie feiten: het document meldt via TPdf.XFA een XFA-formuliertype, de globale XfaFeaturesAvailable vindt de XFA-stringhelpers die de geladen DLL exporteert, en V8FeaturesAvailable vindt de V8-exports. De versie-1-callbacks worden voor elk document bekabeld, want AcroForm en XFA hangen er allebei van af. Alle zeventien versie-2-slots worden toegewezen, en xfa_disabled gewist, alleen als alle drie de feiten kloppen. Na het aanmaken van de omgeving draait FPDF_LoadXFA alleen in dat geval, en alleen een retour ongelijk nul laat TPdf.XfaRuntimeAvailable True melden. FPDF_LoadXFA wordt zelfs door builds zonder de XFA-engine geëxporteerd en geeft daar simpelweg false terug, wat de reden is dat de exportcontrole alleen niet genoeg is
Is het document XFA maar de runtime afwezig, dan blijft het record op versie 2 met xfa_disabled op 1 en de staart op NULL. De omgeving opent, de XFA-inhoud blijft statisch, en TPdf roept OnXfaRuntimeMissing aan zodat de host kan voorstellen om op pdfium.v8.dll te herstarten. m_pJsPlatform volgt een derde regel: hij wordt gekoppeld zodra V8FeaturesAvailable true is, ook voor gewone AcroForm-bestanden, en dat is wat documentscripts, veldberekeningen en app.alert onder de V8-build laten draaien. PageCount leest de statische paginaboom tot XFA het document via FFI_PageEvent herpagineert; op dat moment wordt het nieuwe totaal uit FPDF_GetPageCount gehaald en via OnXfaPageCountChanged gemeld
Aan de hostkant beantwoorden twee eigenschappen twee verschillende vragen. XFA vertelt u wat het bestand bevat, uit FPDF_GetFormType, ongeacht de binary. XfaRuntimeAvailable vertelt u of de engine voor dit document werkelijk is gestart. Lees de tweede nadat Active True is geworden, zoals het artikel over het detecteren van XFA-formulieren en het extraheren van XFA-pakketten doet, en houd het versieveld volledig buiten de redenering
procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
// Wordt aangeroepen terwijl het document opent wanneer het XFA is maar de geladen DLL
// de engine niet kan draaien. De formulieromgeving bestaat nog, met XFA uitgeschakeld
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; // gewone AcroForm-bestanden openen ook onder pdfium.v8.dll
if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
ShowStaticXfaNotice; // uw eigen UI-routine
end;
Layoutfouten die het record breken voordat de versie wordt gelezen
Een correct versienummer helpt alleen als de bytes erachter zitten waar de C-compiler ze heeft neergezet. Verschillende bindfouten leveren een record op dat PDFium verkeerd leest, welke versie het ook draagt, en één daarvan blijft op Win32 onzichtbaar. Dat is FPDF_FORMFILLINFO als packed record declareren. Op Win32 is elk veld vier bytes, dus packed en aligned layouts vallen toevallig samen. Op Win64 vult de C-struct na version vier bytes op zodat Release op een 8-byte-grens landt, en vult na xfa_disabled opnieuw; een packed Pascal-declaratie laat beide gaten weg en schuift elke callbackpointer vier bytes op. De declaratie in FPdfFormFill.pas is daarom een kale record
De tweede fout is een afgekapt record: een vertaling die bij m_pJsPlatform stopt maar versie 2 schrijft. PDFium leest de staart dan uit welk geheugen erop volgt. De derde is de levensduur. De header zegt dat het record dat aan FPDFDOC_InitFormFillEnvironment wordt doorgegeven geldig moet blijven tot de teruggegeven form-handle wordt gesloten, dus een record in een lokale variabele van de procedure die het document opent is een bungelende pointer op het moment dat die procedure terugkeert. TPdf houdt zijn record als veld van de component bij, verpakt in een buitenrecord dat de PDFium-struct op offset nul zet en een terugverwijzing naar de bezittende TPdf er direct achter, en zo vindt elke statische callback zijn object terug
type
// Fout op Win64: packing verwijdert de padding die de C-struct na version heeft,
// dus Release zit hier op offset 4 en in PDFium op offset 8
TBadFormFillInfo = packed record
version: Integer;
Release: Pointer;
// ... overige velden
end;
// Uw eigen wrapper: eerst het PDFium-record, daarna uw terugverwijzing
THostFormFillInfo = record
Info: FPDF_FORMFILLINFO; // offset 0, dus pThis cast direct terug
Owner: TObject;
end;
PHostFormFillInfo = ^THostFormFillInfo;
procedure HostInvalidate(pThis: PFPDF_FORMFILLINFO; page: FPDF_PAGE;
left, top, right, bottom: Double); cdecl;
begin
try
// TMyViewer en InvalidatePdfRect staan voor uw eigen viewerklasse
TMyViewer(PHostFormFillInfo(pThis).Owner).InvalidatePdfRect(
page, left, top, right, bottom);
except
// een Pascal-exception mag niet door de C++-frames van PDFium ontvouwen
end;
end;
Twee slots verdienen extra zorg. FFI_GetLocalTime geeft een 16-byte FPDF_SYSTEMTIME per waarde terug, en Delphi en MSVC zijn het op Win32 oneens over waar de verborgen resultaatpointer terechtkomt, dus een Pascal-implementatie zou de FPDF_FORMFILLINFO van de aanroeper corrupt maken. De huidige PDFium markeert het lid als ongebruikt, en de Pascal-declaratie draagt een commentaar om het op nil te laten; PDFium Component doet dat. Elke callback die de component wél installeert vangt Pascal-exceptions op en geeft in plaats daarvan de gedocumenteerde faalwaarde terug, want de C-ABI heeft geen manier om een exception terug over de grens te dragen. De notitie over het harden van de PDFium-ABI in Delphi loopt die discipline langs met FFI_OpenFile als voorbeeld
Protocolversie en featurebeschikbaarheid zijn twee assen
Een versieveld in een callbackstructuur beantwoordt hoe groot het record is en wat eruit mag worden gelezen. Featuredetectie beantwoordt welke van die slots iets zullen doen. Het eerste ligt vast in de native binary en in de declaratie waartegen u hebt gecompileerd; het tweede verschilt per document, per DLL-exporttabel en per hostinstelling. Het XFA-geval heeft beide antwoorden tegelijk nodig, wat het verleidelijk maakt ze in één boolean te vouwen, en dat vouwen breekt op het moment dat een build een minimumversie afdwingt. XFA-formulieren, die ISO 32000-1 §12.7.8 beschrijft als een XML-payload die naast het AcroForm-woordenboek reist, zijn hier de feature. De recordlayout is het protocol, en PDFium heeft het recht de layout te controleren voordat hij het bestand leest
Dezelfde structuur duikt overal op waar een C-library zijn structs versioneert: FPDF_LIBRARY_CONFIG, IPDF_JsPlatform, elke callbacktabel met een size- of versiekop. Declareer de nieuwste layout die u begrijpt, wis hem volledig, schrijf de bijpassende versie onvoorwaardelijk, en vul slots naar gelang wat het document en de DLL werkelijk kunnen. Voert een toekomstige fpdf_formfill.h versie 3 in, dan is het werk een nieuwe declaratie en één gewijzigde toewijzing. Een tak die op documentinhoud sleutelt, zou voor elke combinatie van build en bestand opnieuw moeten worden bekeken, en de combinatie die hem misgaat is meestal degene die niemand ooit opende
De recordafhandeling die hier wordt beschreven maakt deel uit van PDFium Component voor Delphi, Lazarus en C++Builder, met dezelfde declaratie voor Win32- en Win64-builds. Een applicatie die al pdfium.v8.dll meestuurt voor JavaScript-gedreven AcroForms, kan de rest van zijn archief, XFA of niet, door die ene binary openen zonder speciale afhandeling voor de formulieromgeving