Technický článek

FPDF_FORMFILLINFO verze 2 v Delphi: následujte ABI DLL

PDFium Component teď nastavuje FPDF_FORMFILLINFO.version na 2 pro každé form-fill prostředí, které inicializuje, protože verze, kterou akceptuje nativní build PDFium, je vlastnost toho buildu, ne otevíraného dokumentu. pdfium.v8.dll s XFA verzi 1 odmítá rovnou, takže obyčejné PDF s AcroForm otevřené skrz ni dřív padalo v FPDFDOC_InitFormFillEnvironment bez jediného viditelného XFA. Oprava ve v3.116.0 je malá, ale chyba za ní je obecná a stojí za pojmenování: pole verze protokolu popisuje rozložení paměti, které očekává druhá strana, a nikdy se nesmí odvozovat z toho, zda zrovna potřebujete funkce, které to rozložení nese

Proč FPDFDOC_InitFormFillEnvironment padá na obyčejném PDF s pdfium.v8.dll?

Prostředí padá, protože build PDFium s XFA validuje pole version, než udělá cokoli jiného, a stará logika wrapperu mu podávala 1, kdykoli aktuální dokument nebyl XFA formulář. Symptom v Delphi hostu je EPdfError hozený z TPdf.InitializeFormFill se zprávou Cannot initialize form fill environment během otvírání obyčejné faktury nebo daňového formuláře, který nemá nic kromě textových polí AcroForm. Tentýž soubor se proti obyčejnému pdfium.dll otevírá dobře. Tatáž DLL otevírá reálný XFA dokument dobře. Rozbije se jen kombinace V8 buildu a ne-XFA dokumentu, což je přesně kombinace, do níž se host dostane, když zapne EnableV8Engine kvůli AcroForm JavaScriptu, nebo když auto-volba v LoadDocument už zavázala proces k pdfium.v8.dll kvůli dřívějšímu XFA souboru. To zavázání je napříč procesem: EnableV8Engine se čte před prvním LoadLibrary a jakmile se XFA build nahraje, každé pozdější obyčejné PDF jde stejným nastavením prostředí proti stejné binárce. Host neudělal nic špatně; wrapper položil špatnou otázku, když vyplňoval záznam. Pokud se teprve rozhodujete, kterou binárku vůbec dodávat, naše poznámka o deployi PDFium DLL a diagnostice selhání načtení pokrývá volbu plain versus V8 a tenhle článek počítá s tím, že V8 build už je v procesu

Diagram PDFium Component čtyř kombinací obyčejného pdfium.dll a pdfium.v8.dll s XFA proti dokumentům AcroForm a XFA: záznam verze 1 rozbil jen V8 build s obyčejným formulářem, EPdfError v FPDFDOC_InitFormFillEnvironment, zatímco opravený záznam verze 2 otevírá všechny čtyři
Jedna podmínka svázala verzi ABI s dokumentem, takže volba V8 binárky napříč procesem měnila každé pozdější obyčejné PDF v selhavší inicializaci prostředí

Co pole version v FPDF_FORMFILLINFO doopravdy slibuje?

FPDF_FORMFILLINFO.version říká PDFium, která pole záznamu smí číst a veřejná hlavička fpdf_formfill.h váže přijatelné hodnoty na to, jak byla knihovna kompilovaná, ne na dokument. Parafrázován, kontrakt má tři části. Verze 1 pokrývá stabilní callbacky od FFI_Invalidate po FFI_DoGoToAction plus pointer m_pJsPlatform. Build bez modulu XFA akceptuje 1 i 2 a při 2 bude navíc volat dodatečné experimentální callbacky. Build s modulem XFA vyžaduje 2, tečka, a hlavička tu požadavek opakuje dvakrát, jako by čekala, že si ho lidé nevšimnou. Nikde kontrakt nezmiňuje dokument. Verze je výrok o záznamu, který jste alokovali: s 2 slibujete, že paměť za m_pJsPlatform existuje a drží buď platné function pointery, nebo NULL

Oblast verze 2 je místo, kde bydlí veškerý XFA stroj. Začíná xfa_disabled, FPDF_BOOL, který hlavička popisuje jako pod verzí 2 ignorovaný a smysluplný jen tehdy, když je modul XFA zkompilovaný, a pokračuje sedmnácti function pointery, FFI_DisplayCaret po FFI_DoURIActionWithKeyboardModifier. Každý z nich je dokumentovaný jako vyžadovaný pro XFA a jinak k nastavení na NULL. To znění je klíč k celé opravě. NULL není chybový stav těch slotů; je to dokumentovaný stav hosta, který XFA neřídí. Záznam vymazaný přes FillChar a pak označený jako verze 2 splňuje kontrakt na ne-XFA buildu přesně tak dobře jako záznam verze 1 a je to jediný záznam, který XFA build vezme

Diagram záznamu FPDF_FORMFILLINFO v Delphi podle PDFium Component: verze 1 pokrývá callbacky FFI_Invalidate po FFI_DoGoToAction plus m_pJsPlatform, verze 2 přidává xfa_disabled a sedmnáct pointerů éry FFI_DisplayCaret, FillChar vymaže každý bajt a NULL sloty jsou dokumentovaný stav hosta, který neřídí XFA
Pascal záznam je vždy kompletní rozložení verze 2, takže build s XFA ho akceptuje a obyčejný build prostě nikdy nevolá experimentální sloty, které zůstávají NULL

Stará volba svázala ABI s dokumentem

Vada byla jediná podmínka, která v izolaci vypadala rozumně. TPdf.InitializeFormFill počítá příznak RuntimeReady ze tří faktů: dokument hlásí typ formuláře XFA přes TPdf.XFA, string helpery XFA se rozřešily přes XfaFeaturesAvailable a V8 exporty se rozřešily přes V8FeaturesAvailable. Před v3.116.0 tatáž příznaková volila i verzi

// v3.115.0 a dřív: verze ABI následovala dokument
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;

if RuntimeReady then
  FFormFillInfo.Info.version := 2
else
  FFormFillInfo.Info.version := 1;

// ... a větev chybějícího runtime ji připnula znovu
else if XFA then
begin
  FFormFillInfo.Info.version := 1;
  FFormFillInfo.Info.xfa_disabled := 1;
  if Assigned(FOnXfaRuntimeMissing) then
    FOnXfaRuntimeMissing(Self);
end;

Přečtěte si to s hlavičkou v ruce a selhání je zjevné. RuntimeReady je false pro každý obyčejný dokument AcroForm, takže každý obyčejný dokument ohlašoval verzi 1. Na pdfium.dll je to v pořádku. Na pdfium.v8.dll, což je build s XFA, PDFium pole zkontroluje, najde ho pod požadovanou 2 a vrátí null FPDF_FORMHANDLE, který CheckPdf mění ve výjimku výše. Záměr starého kódu byl defenzivní: držet verzi 1, aby XFA build nikdy nečetl nepřiřazené sloty verze 2. Bránil se problému, který hlavička už vylučuje, a vytvořila jeden, před kterým hlavička výslovně varuje. Opravený kód rozhoduje verzi jednou, předem, z toho, co záznam fyzicky je

procedure TPdf.InitializeFormFill;
var
  RuntimeReady: Boolean;
begin
  FXfaRuntimeUsable := False;
  FXfaPageCountOverride := -1;   // sentinel: použij statický strom stránek
  if not FormFill then
    Exit;

  FillChar(FFormFillInfo, SizeOf(FFormFillInfo), 0);
  FFormFillInfo.Pdf := Self;

  // Kompletní záznam verze 2 je alokovaný a vymazaný výše. PDFium
  // akceptuje verzi 2 bez XFA a vyžaduje ji v každém buildu s XFA,
  // včetně případu, kdy tenhle dokument neobsahuje žádný XFA formulář.
  FFormFillInfo.Info.version := 2;
  FFormFillInfo.Info.xfa_disabled := 1;

  // RuntimeReady hlídkuje XFA callbacky a xfa_disabled, nikdy verzi.
  RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
  ...

Kde RuntimeReady pořád patří: callbacky a xfa_disabled

RuntimeReady si drží svoji práci brány chování XFA; prostě se už nedotýká rozložení záznamu. Callbacky verze 1, FFI_Invalidate, FFI_SetTimer, FFI_GetPage, FFI_DoURIAction, FFI_DoGoToAction a zbytek toho bloku, se vedou bezpodmínečně, protože AcroForm i XFA na nich závisí. Sedmnáct pointerů verze 2 se přiřazuje jen uvnitř větve RuntimeReady, spolu s xfa_disabled := 0. Když je dokument XFA, ale runtime není tu, zůstává záznam na verzi 2 s xfa_disabled na 1 a sloty verze 2 ponechanými na NULL a wrapper hodí OnXfaRuntimeMissing, aby mohl host navrhnout restart na pdfium.v8.dll. Poté, co prostředí existuje, volá se FPDF_LoadXFA jen tehdy, když byl RuntimeReady true, a jen true návrat nastaví FXfaRuntimeUsable, což hlásí TPdf.XfaRuntimeAvailable

  if RuntimeReady then
  begin
    FFormFillInfo.Info.xfa_disabled := 0;   // 0 = XFA zapnuté
    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 po FFI_DoURIActionWithKeyboardModifier
  end
  else if XFA then
  begin
    // Runtime nedostupný: drž verzi 2, nech XFA vypnuté, řekni hostu.
    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;

Dva detaily v tom bloku se snadno spletou, když si píšete vlastní binding. FXfaPageCountOverride se resetuje na -1 jako sentinel, než se cokoli jiného stane, takže PageCount propadá ke statickému stromu stránek, dokud FFI_PageEvent nenahlásí repaginaci; nula tam by potichu tvrdila prázdný dokument. A každý z callbacků verze 2 je statická rutina cdecl, která vyloví vlastnící TPdf ze záznamu a spolkne jakoukoli Pascal výjimku, než se vrátí k PDFium, což je disciplína, kterou naše poznámka o tvrzení PDFium ABI v Delphi vykládá pro FFI_OpenFile. Nic ve změně verze z toho pravidla nijak neuvolňuje

Je verze 2 bezpečná, když DLL nemá modul XFA?

Ano a důvod je v záznamu, ne ve slibu od knihovny. Na ne-XFA buildu říká hlavička, že verze 2 způsobí, že se budou volat i experimentální callbacky, takže otázka je, co PDFium najde, když se podívá. TPdfFormFillInfo je packed záznam, jehož člen Info je kompletní FPDF_FORMFILLINFO včetně každého pole verze 2 a InitializeFormFill vymaže celé přes FillChar, než sahne k jedinému bajtu. Na obyčejném pdfium.dll s obyčejným dokumentem tedy vidí knihovna verzi 2, nastavené xfa_disabled a NULL v každém experimentálním slotu, což je přesně ten stav, který hlavička předepisuje hostu, který XFA neimplementuje. Neexistuje žádný useknutý záznam, za který by si knihovna četla, protože záznam nebyl nikdy kratší než verze 2 už od začátku. Stará logika bránila nesourodost rozložení, kterou Pascal deklarace už zlikvidovala

Hranice, kterou stojí za poctivě říct, je ta, kterou záznam nepokryje. Verze 2 na obyčejném dokumentu nezapíná JavaScript, XFA skriptování ani žádnou z host eventů za těmi callbacky. m_pJsPlatform se připojuje jen tehdy, když je V8FeaturesAvailable true, XFA zůstává vypnuté, pokud nebyl RuntimeReady true, a TPdf.XFA pořád hlásí typ formuláře z FPDF_GetFormType bez ohledu na to, co si prostředí vyjednalo. Host, který chce vědět, zda se dynamické XFA doopravdy vykreslí, má pořád číst XfaRuntimeAvailable poté, co Active projde na true, jak doporučuje naše poznámka o detekci XFA formulářů a extrakci XFA paketů, místo inferring něčeho z pole verze

procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Vznítí z InitializeFormFill, když je dokument XFA, ale načtené
  // pdfium.dll nedokáže pustit engine. Prostředí formuláře se pořád otevře,
  // protože verze 2 byla podána oběma směry; vypnutý je jen XFA runtime.
  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;   // už nehodí na obyčejném PDF pod pdfium.v8.dll
  if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
    ShowStaticXfaWarning;
end;

Verze protokolu a dostupnost funkcí jsou dvě různé osy

Obecné pravidlo, které z téhle opravy vypadne, zní: pole verze v callback struktuře odpovídá na otázku „jak velký je tenhle záznam a co z něj smíte číst", zatímco detekce funkcí odpovídá na „které z těch slotů budou cokoliv platit". První je fixní nativní binárkou a Pascal deklarací, proti níž jste kompilovali. Druhá se liší na dokument, na exportní tabulku DLL a na konfiguraci hosta. Sliít obojí do jednoho booleánského je lákavé, protože případ XFA náhodou potřebuje obojí, ale v momentě, kdy build vynucuje minimální verzi, ten slepek rozbije každý dokument, který funkci nepotřebuje. XFA formuláře, popsané v ISO 32000-1 §12.7.8 jako XML payload bydlící po boku slovníku AcroForm, jsou tady tou funkcí; rozložení záznamu je protokol a PDFium je oprávněno trvat na rozložení, než se vůbec podívá na soubor. Tentýž tvar se objevuje kdekoliv, kde si C knihovna verzijuje své struktury: blok viewer-info, záznam render-voleb, tabulka platform callbacků. Bezpečný vzor je ten, který následuje opravené InitializeFormFill. Deklarujte nejnovější rozložení, kterému rozumíte, vymažte ho úplně, nastavte verzi odpovídající tomuto rozložení bezpodmínečně a pak nechte kontroly schopností rozhodovat, které sloty naplnit. Přidá-li budoucí hlavička PDFium verzi 3, změna patří do deklarace a do toho jediného přiřazení, ne do dokumentu závislé větve, která bude špatná pro jakoukoli kombinaci, kterou nikdo netestoval

Diagram PDFium Component oddělující dvě osy za FPDF_FORMFILLINFO: verzi protokolu fixní rozložením záznamu a nativní binárkou a dostupnost funkcí, kde RuntimeReady hlídkuje xfa_disabled, sedmnáct slotů verze 2, FPDF_LoadXFA a m_pJsPlatform na dokument a na host
Pole verze popisuje paměť, kterou smí druhá strana číst, kontroly schopností rozhodují, které sloty budou cokoliv platit, a slepení obojího do jednoho booleánského rozbije build, který vynucuje minimum

Opravená inicializace form-fill se dodává v PDFium Component pro Delphi, Lazarus a C++Builder a platí na Win32 i Win64 stejně, protože oba buildy sdílejí tutéž deklaraci záznamu. Pokud vaše aplikace už vybírá pdfium.v8.dll pro AcroForm řízené JavaScriptem, tahle změna je ta, které dovolí otevřít zbytek vašeho PDF archivu skrz tutéž binárku bez special-casingu prostředí formulářů