PDFium Component teraz nastavuje FPDF_FORMFILLINFO.version na 2 pre každé form-fill prostredie, ktoré inicializuje, pretože verzia, ktorú natívny PDFium build akceptuje, je vlastnosťou toho buildu a nie otváraného dokumentu. pdfium.v8.dll s XFA rovno odmietne verziu 1, takže bežné AcroForm PDF otvorené cez ňu predtým zlyhalo v FPDFDOC_InitFormFillEnvironment, hoci XFA nebolo nikde na obzore. Oprava vo v3.116.0 je malá, ale chyba za ňou je všeobecná a stojí za pomenovanie: pole s verziou protokolu opisuje pamäťové rozloženie, ktoré druhá strana očakáva, a nikdy sa nesmie odvodzovať z toho, či náhodou potrebujete funkcie, ktoré to rozloženie nesie
Prečo FPDFDOC_InitFormFillEnvironment zlyhá pri bežnom PDF s pdfium.v8.dll?
Prostredie zlyhá preto, že PDFium build s XFA validuje pole version skôr, než spraví čokoľvek iné, a stará logika wrappera mu poslala 1 vždy, keď aktuálny dokument nebol XFA formulár. Symptómom v Delphi hoste je EPdfError vyhodená z TPdf.InitializeFormFill s hláškou Cannot initialize form fill environment, a to pri otváraní obyčajnej faktúry alebo daňového formulára, ktorý nemá nič okrem AcroForm textových polí. Ten istý súbor sa otvorí bez problémov proti obyčajnému pdfium.dll. Tá istá DLL otvorí skutočný XFA dokument bez problémov. Láme sa len kombinácia V8 buildu a ne-XFA dokumentu, čo je presne kombinácia, do ktorej sa host dostane, keď zapne EnableV8Engine kvôli AcroForm JavaScriptu, alebo keď auto-selekcia v LoadDocument už predtým zaviazala proces na pdfium.v8.dll kvôli skoršiemu XFA súboru. Ten záväzok je procesový: EnableV8Engine sa číta pred prvým LoadLibrary, a keď sa XFA build raz načíta, každé ďalšie bežné PDF prejde tým istým nastavením prostredia proti tomu istému binárnemu súboru. Host nespravil nič zle; wrapper sa opýtal nesprávnu otázku, keď vypĺňal ten record. Ak sa ešte len rozhodujete, ktorý binárny súbor vôbec dodať, naša poznámka o nasadení PDFium DLL a diagnostike zlyhaní načítania pokrýva výber medzi obyčajnou a V8 verziou, a tento článok predpokladá, že V8 build už v procese je
Čo pole version v FPDF_FORMFILLINFO naozaj sľubuje?
FPDF_FORMFILLINFO.version hovorí PDFiu, ktoré polia recordu smie čítať, a verejný header fpdf_formfill.h viaže prijateľné hodnoty na to, ako bola knižnica skompilovaná, a nie na dokument. Parafrázovane má ten kontrakt tri časti. Verzia 1 pokrýva stabilné callbacky od FFI_Invalidate po FFI_DoGoToAction plus ukazovateľ m_pJsPlatform. Build bez XFA modulu prijme 1 aj 2, a s dvojkou zavolá aj tie ďalšie experimentálne callbacky. Build s XFA modulom vyžaduje 2, bodka, a header to opakuje dvakrát, akoby čakal, že to ľudia prehliadnu. Nikde ten kontrakt nespomína dokument. Verzia je vyhlásenie o recorde, ktorý ste alokovali: s dvojkou sľubujete, že pamäť za m_pJsPlatform existuje a drží buď platné function pointery, alebo NULL
V oblasti verzie 2 žije celá XFA mašinéria. Začína sa xfa_disabled, čo je FPDF_BOOL, ktorý header opisuje ako ignorovaný pod verziou 2 a významný len vtedy, keď je XFA modul skompilovaný, a pokračuje sedemnástimi function pointermi, od FFI_DisplayCaret po FFI_DoURIActionWithKeyboardModifier. Každý z nich je dokumentovaný ako povinný pre XFA a inak nastavený na NULL. Práve tá formulácia je kľúčom k celej oprave. NULL nie je pre tie sloty chybový stav; je to dokumentovaný stav pre host, ktorý XFA nepoženie. Record, ktorý bol vyčistený cez FillChar a potom označený ako verzia 2, spĺňa kontrakt na ne-XFA build presne tak dobre ako record s verziou 1, a je to jediný record, ktorý XFA build prijme
Stará voľba zviazala ABI s dokumentom
Defekt bola jediná podmienka, ktorá vyzerala rozumne, keď sa pozerala izolovane. TPdf.InitializeFormFill počíta flag RuntimeReady z troch faktov: dokument hlási XFA form type cez TPdf.XFA, XFA string helpery sa rozlíšili cez XfaFeaturesAvailable a V8 exporty sa rozlíšili cez V8FeaturesAvailable. Pred v3.116.0 ten istý flag vyberal aj verziu
// v3.115.0 a staršie: verzia ABI išla za dokumentom
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
if RuntimeReady then
FFormFillInfo.Info.version := 2
else
FFormFillInfo.Info.version := 1;
// ... a vetva s chýbajúcim runtime ju pripínala znova
else if XFA then
begin
FFormFillInfo.Info.version := 1;
FFormFillInfo.Info.xfa_disabled := 1;
if Assigned(FOnXfaRuntimeMissing) then
FOnXfaRuntimeMissing(Self);
end;
Prečítajte si to s headerom v ruke a zlyhanie je očividné. RuntimeReady je false pre každý bežný AcroForm dokument, takže každý bežný dokument ohlásil verziu 1. Na pdfium.dll je to v poriadku. Na pdfium.v8.dll, teda na XFA builde, PDFium to pole skontroluje, nájde ho pod požadovanou dvojkou a vráti null FPDF_FORMHANDLE, z čoho CheckPdf spraví výnimku vyššie. Zámer starého kódu bol defenzívny: podržať verziu 1, aby XFA build nikdy nečítal nepriradené sloty verzie 2. Bránil sa problému, ktorý header už vylučuje, a vytvoril jeden, pred ktorým header výslovne varuje. Opravený kód rozhodne o verzii raz, dopredu, podľa toho, čím ten record fyzicky je
procedure TPdf.InitializeFormFill;
var
RuntimeReady: Boolean;
begin
FXfaRuntimeUsable := False;
FXfaPageCountOverride := -1; // sentinel: použi statický page tree
if not FormFill then
Exit;
FillChar(FFormFillInfo, SizeOf(FFormFillInfo), 0);
FFormFillInfo.Pdf := Self;
// Plný record verzie 2 je alokovaný a vyčistený vyššie. PDFium
// prijme verziu 2 bez XFA a vyžaduje ju v každom builde s XFA,
// a to aj vtedy, keď tento dokument XFA formulár neobsahuje.
FFormFillInfo.Info.version := 2;
FFormFillInfo.Info.xfa_disabled := 1;
// RuntimeReady bráni XFA callbacky a xfa_disabled, nikdy verziu.
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
...
Kde RuntimeReady ešte patrí: callbacky a xfa_disabled
RuntimeReady si drží svoju úlohu brány pre XFA správanie; len sa už nedotýka rozloženia recordu. Callbacky verzie 1, FFI_Invalidate, FFI_SetTimer, FFI_GetPage, FFI_DoURIAction, FFI_DoGoToAction a zvyšok toho bloku, sú zapojené bezpodmienečne, pretože AcroForm aj XFA na nich závisia. Sedemnásť pointerov verzie 2 sa priraďuje len vnútri vetvy RuntimeReady, spolu s xfa_disabled := 0. Keď je dokument XFA, ale runtime nie je k dispozícii, record zostáva na verzii 2 s xfa_disabled na 1 a sloty verzie 2 zostanú NULL, a wrapper vyvolá OnXfaRuntimeMissing, aby host mohol navrhnúť reštart na pdfium.v8.dll. Keď prostredie existuje, FPDF_LoadXFA sa volá len vtedy, keď bolo RuntimeReady true, a len true návratová hodnota nastaví FXfaRuntimeUsable, čo je to, čo hlási 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 až FFI_DoURIActionWithKeyboardModifier
end
else if XFA then
begin
// Runtime nedostupný: podrž verziu 2, nechaj XFA vypnuté, povedz to hostovi.
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 sa dajú ľahko pokaziť, keď si píšete vlastný binding. FXfaPageCountOverride sa pred všetkým ostatným vráti na -1 ako sentinel, takže PageCount padne späť na statický page tree, kým FFI_PageEvent nenahlási repagináciu; nula na tom mieste by ticho tvrdila prázdny dokument. A každý z callbackov verzie 2 je statická cdecl rutina, ktorá si z recordu vytiahne vlastniaci TPdf a pred návratom do PDFia prehltne akúkoľvek Pascal výnimku, čo je disciplína, ktorú naša poznámka o hardení PDFium ABI v Delphi vypisuje pre FFI_OpenFile. Nič na zmene verzie neuvoľňuje ani jedno z tých dvoch pravidiel
Je verzia 2 bezpečná, keď DLL nemá XFA modul?
Áno, a dôvod je v recorde, nie v sľube od knižnice. Na ne-XFA builde header hovorí, že verzia 2 spôsobí, že sa zavolajú aj experimentálne callbacky, takže otázkou je, čo PDFium nájde, keď sa pozrie. TPdfFormFillInfo je packed record, ktorého člen Info je kompletné FPDF_FORMFILLINFO vrátane každého poľa verzie 2, a InitializeFormFill celé to vyčistí cez FillChar skôr, než sa dotkne jediného bajtu. Takže na obyčajnom pdfium.dll s obyčajným dokumentom knižnica vidí verziu 2, nastavené xfa_disabled a NULL v každom experimentálnom slote, čo je presne stav, ktorý header predpisuje pre host, ktorý XFA neimplementuje. Neexistuje žiadny skrátený record, za ktorý by knižnica mohla čítať, pretože ten record nikdy nebol kratší než verzia 2. Stará logika bránila layout mismatch, ktorý Pascal deklarácia už odstránila
Hranica, ktorú stojí za to povedať poctivo, je tá, ktorú record pokryť nedokáže. Verzia 2 na obyčajnom dokumente nezapne JavaScript, XFA scripting ani žiadny z host eventov za tými callbackmi. m_pJsPlatform sa pripojí len vtedy, keď je V8FeaturesAvailable true, XFA zostane vypnuté, ak nebolo RuntimeReady true, a TPdf.XFA naďalej hlási form type z FPDF_GetFormType bez ohľadu na to, čo prostredie dohodlo. Host, ktorý chce vedieť, či sa dynamické XFA naozaj vyrenderuje, by mal po tom, čo Active prejde na true, čítať XfaRuntimeAvailable, ako odporúča naša poznámka o detekcii XFA formulárov a extrakcii XFA packetov, a nie odvodzovať čokoľvek z poľa s verziou
procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
// Vyvolá sa z InitializeFormFill, keď je dokument XFA, ale načítaná
// pdfium.dll nedokáže spustiť engine. Form prostredie sa aj tak otvorí,
// pretože verzia 2 bola poslaná v oboch prípadoch; vypnutý je len 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ž nevyhodí výnimku pri bežnom PDF pod pdfium.v8.dll
if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
ShowStaticXfaWarning;
end;
Verzia protokolu a dostupnosť funkcií sú dve rôzne osi
Všeobecné pravidlo, ktoré z tejto opravy vyplýva, je, že pole s verziou v callback štruktúre odpovedá na otázku „aký veľký je tento record a čo z neho smiete čítať“, kým feature detection odpovedá na „ktoré z tých slotov spravia niečo užitočné“. Prvé je dané natívnou binárkou a Pascal deklaráciou, proti ktorej ste kompilovali. Druhé sa mení podľa dokumentu, podľa exportnej tabuľky DLL a podľa konfigurácie hosta. Zbaliť obe do jedného booleanu je lákavé, pretože XFA prípad náhodou potrebuje obe, ale v momente, keď build vynucuje minimálnu verziu, to zbaliť zlomí každý dokument, ktorý tú funkciu nepotrebuje. XFA formuláre, opísané v ISO 32000-1 §12.7.8 ako XML payload žijúci vedľa AcroForm slovníka, sú tu tou funkciou; rozloženie recordu je protokol a PDFium má právo trvať na rozložení skôr, než sa vôbec pozrie na súbor. Ten istý tvar sa objavuje všade, kde C knižnica verzuje svoje štruktúry: viewer-info blok, render-options record, tabuľka platform callbackov. Bezpečný vzor je ten, ktorý nasleduje opravený InitializeFormFill. Deklarujte najnovšie rozloženie, ktorému rozumiete, úplne ho vyčistite, nastavte verziu tak, aby tomu rozloženiu bezpodmienečne zodpovedala, a potom nechajte capability kontroly rozhodnúť, ktoré sloty naplniť. Ak budúci PDFium header pridá verziu 3, zmena je v deklarácii a v tom jednom priradení, a nie v podmienke závislej od dokumentu, ktorá bude nesprávna pre tú kombináciu, ktorú nikto netestoval
Opravená inicializácia form fill vychádza v PDFium Component pre Delphi, Lazarus a C++Builder a platí rovnako na Win32 aj Win64, keďže oba buildy zdieľajú tú istú deklaráciu recordu. Ak vaša aplikácia už vyberá pdfium.v8.dll kvôli JavaScriptom riadeným AcroFormom, toto je zmena, ktorá jej dovolí otvoriť zvyšok PDF archívu tou istou binárkou bez špeciálneho ošetrovania form prostredia