PDFium Component sada postavlja FPDF_FORMFILLINFO.version na 2 za svako form-fill okruženje koje inicijalizuje, jer je verzija koju nativni PDFium build prihvata svojstvo tog builda, a ne dokumenta koji se otvara. pdfium.v8.dll sa uključenim XFA odbija verziju 1 odmah, pa je običan AcroForm PDF otvoren kroz njega padao u FPDFDOC_InitFormFillEnvironment bez ijednog XFA-a na vidiku. Ispravka u v3.116.0 je mala, ali greška iza nje je opšta i vredi je imenovati: polje verzije protokola opisuje raspored memorije koji druga strana očekuje, i nikad se ne sme izvoditi iz toga da li vam slučajno trebaju funkcionalnosti koje taj raspored nosi
Zašto FPDFDOC_InitFormFillEnvironment pada na običnom PDF-u sa pdfium.v8.dll?
Okruženje pada jer PDFium build sa uključenim XFA validira polje version pre nego što uradi bilo šta drugo, a stara logika omotača predavala mu je 1 kad god tekući dokument nije bio XFA formular. Simptom u Delphi host-u je EPdfError podignut iz TPdf.InitializeFormFill sa porukom Cannot initialize form fill environment, bačen dok se otvara obična faktura ili poreski formular koji ima samo AcroForm tekstualna polja. Isti fajl se otvara bez problema uz obični pdfium.dll. Isti DLL bez problema otvara pravi XFA dokument. Kvario se samo spoj V8 builda i ne-XFA dokumenta, što je upravo spoj u koji host dospe kada uključi EnableV8Engine da dobije AcroForm JavaScript, ili kada je automatski izbor u LoadDocument već opredelio proces za pdfium.v8.dll zbog nekog ranijeg XFA fajla. Ta opredeljenost je na nivou celog procesa: EnableV8Engine se čita pre prvog LoadLibrary, a jednom kada je XFA build učitan, svaki kasniji obični PDF prolazi kroz isto podešavanje okruženja nad istim binary-jem. Host nije uradio ništa pogrešno; omotač je postavio pogrešno pitanje kada je punio zapis. Ako još odlučujete koji binary uopšte da isporučite, naša beleška o isporuci PDFium DLL-a i dijagnostici otkaza pri učitavanju pokriva izbor običnog naspram V8 builda, a ovaj članak pretpostavlja da je V8 build već u procesu
Šta polje version u FPDF_FORMFILLINFO zaista obećava?
FPDF_FORMFILLINFO.version govori PDFium-u koja polja zapisa sme da čita, a javni header fpdf_formfill.h vezuje prihvatljive vrednosti za to kako je biblioteka kompajlirana, a ne za dokument. Prepričano, ugovor ima tri dela. Verzija 1 pokriva stabilne callback-ove od FFI_Invalidate do FFI_DoGoToAction plus pokazivač m_pJsPlatform. Build bez XFA modula prihvata i 1 i 2, a sa 2 će pozvati i dodatne eksperimentalne callback-ove. Build sa XFA modulom zahteva 2, bez izuzetka, i header to zahteva ponavlja dvaput kao da očekuje da će ljudi to propustiti. Nigde u ugovoru nema pomena o dokumentu. Verzija je izjava o zapisu koji ste alocirali: sa 2 obećavate da memorija posle m_pJsPlatform postoji i drži ili važeće pokazivače na funkcije ili NULL
Region verzije 2 je mesto gde živi sva XFA mašinerija. Počinje sa xfa_disabled, FPDF_BOOL-om koji header opisuje kao ignorisan ispod verzije 2 i smislen samo kada je XFA modul kompajliran unutra, i nastavlja se sa sedamnaest pokazivača na funkcije, od FFI_DisplayCaret do FFI_DoURIActionWithKeyboardModifier. Svaki od njih dokumentovan je kao obavezan za XFA a inače postavljen na NULL. Ta formulacija je ključ cele ispravke. NULL nije stanje greške za te slotove; to je dokumentovano stanje za host koji ne vozi XFA. Zapis koji je očišćen sa FillChar i zatim označen kao verzija 2 zadovoljava ugovor na ne-XFA buildu tačno onoliko dobro koliko i zapis verzije 1, i to je jedini zapis koji će XFA build prihvatiti
Stara selekcija vezala je ABI za dokument
Defekt je bio jedan uslov koji je izolovano izgledao razumno. TPdf.InitializeFormFill računa flag RuntimeReady iz tri činjenice: dokument prijavljuje tip XFA formulara preko TPdf.XFA, XFA string helper-i su se razrešili preko XfaFeaturesAvailable, i V8 exporti su se razrešili preko V8FeaturesAvailable. Pre v3.116.0 taj isti flag birao je i verziju
// v3.115.0 i ranije: ABI verzija je pratila dokument
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
if RuntimeReady then
FFormFillInfo.Info.version := 2
else
FFormFillInfo.Info.version := 1;
// ... a grana za nedostajući runtime prikovala ju je ponovo
else if XFA then
begin
FFormFillInfo.Info.version := 1;
FFormFillInfo.Info.xfa_disabled := 1;
if Assigned(FOnXfaRuntimeMissing) then
FOnXfaRuntimeMissing(Self);
end;
Pročitajte to sa headerom u ruci i otkaz je očigledan. RuntimeReady je false za svaki obični AcroForm dokument, pa je svaki obični dokument najavljivao verziju 1. Na pdfium.dll to je u redu. Na pdfium.v8.dll, koji je XFA-enabled build, PDFium proveri polje, nađe ga ispod zahtevane 2, i vrati null FPDF_FORMHANDLE, što CheckPdf pretvara u gornji izuzetak. Namera starog koda bila je odbrambena: zadrži verziju 1 da XFA build nikad ne čita nedodeljene slotove verzije 2. Branio se od problema koji header već isključuje i stvorio onaj na koji header eksplicitno upozorava. Ispravljeni kod odlučuje o verziji jednom, unapred, iz onoga što zapis fizički jeste
procedure TPdf.InitializeFormFill;
var
RuntimeReady: Boolean;
begin
FXfaRuntimeUsable := False;
FXfaPageCountOverride := -1; // sentinel: koristi statično stablo stranica
if not FormFill then
Exit;
FillChar(FFormFillInfo, SizeOf(FFormFillInfo), 0);
FFormFillInfo.Pdf := Self;
// Pun zapis verzije 2 alociran je i očišćen iznad. PDFium
// prihvata verziju 2 bez XFA i zahteva je u svakom XFA-enabled
// buildu, uključujući i kada ovaj dokument ne sadrži XFA formular.
FFormFillInfo.Info.version := 2;
FFormFillInfo.Info.xfa_disabled := 1;
// RuntimeReady kontroliše XFA callback-ove i xfa_disabled, nikad verziju.
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
...
Gde RuntimeReady i dalje pripada: callback-ovi i xfa_disabled
RuntimeReady zadržava svoju ulogu kapije za XFA ponašanje; samo više ne dira raspored zapisa. Callback-ovi verzije 1, FFI_Invalidate, FFI_SetTimer, FFI_GetPage, FFI_DoURIAction, FFI_DoGoToAction i ostatak tog bloka, povezani su bezuslovno jer i AcroForm i XFA zavise od njih. Sedamnaest pokazivača verzije 2 dodeljuje se samo unutar grane RuntimeReady, zajedno sa xfa_disabled := 0. Kada je dokument XFA ali runtime nije tu, zapis ostaje na verziji 2 sa xfa_disabled na 1 i slotovima verzije 2 ostavljenim na NULL, a omotač podiže OnXfaRuntimeMissing da host može da predloži restart na pdfium.v8.dll. Pošto okruženje postoji, FPDF_LoadXFA poziva se samo kada je RuntimeReady bio true, i samo true povratna vrednost postavlja FXfaRuntimeUsable, što TPdf.XfaRuntimeAvailable i prijavljuje
if RuntimeReady then
begin
FFormFillInfo.Info.xfa_disabled := 0; // 0 = XFA uključen
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 do FFI_DoURIActionWithKeyboardModifier
end
else if XFA then
begin
// Runtime nedostupan: zadrži verziju 2, ostavi XFA isključen, obavesti 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;
Dva detalja u tom bloku lako je pogrešno napisati kada pišete sopstveni binding. FXfaPageCountOverride se resetuje na -1 kao sentinel pre nego što se bilo šta drugo dogodi, pa PageCount pada na statično stablo stranica dok FFI_PageEvent ne prijavi repaginaciju; nula bi tamo tiho tvrdila prazan dokument. I svaki od callback-ova verzije 2 je statična cdecl rutina koja iz zapisa izvlači TPdf koji ga poseduje i guta svaki Pascal izuzetak pre nego što se vrati u PDFium, što je disciplina koju naša beleška o očvršćavanju PDFium ABI-ja u Delphiju propisuje za FFI_OpenFile. Ništa u vezi sa izmenom verzije ne popušta ni jedno od tih pravila
Da li je verzija 2 bezbedna kada DLL nema XFA modul?
Jeste, i razlog je u zapisu, ne u obećanju biblioteke. Na ne-XFA buildu header kaže da verzija 2 uzrokuje da se pozovu i eksperimentalni callback-ovi, pa je pitanje šta PDFium nađe kada pogleda. TPdfFormFillInfo je packed zapis čiji je član Info kompletan FPDF_FORMFILLINFO uključujući svako polje verzije 2, a InitializeFormFill briše celu stvar sa FillChar pre nego što dotakne ijedan bajt. Pa na običnom pdfium.dll sa običnim dokumentom biblioteka vidi verziju 2, xfa_disabled postavljen, i NULL u svakom eksperimentalnom slotu, što je tačno stanje koje header propisuje za host koji ne implementira XFA. Nema skraćenog zapisa iz kojeg bi biblioteka čitala dalje, jer zapis nikad nije bio kraći od verzije 2. Stara logika branila je neslaganje rasporeda koje je Pascal deklaracija već otklonila
Granica koju vredi iskreno navesti je ona koju zapis ne može da pokrije. Verzija 2 na običnom dokumentu ne uključuje JavaScript, XFA scripting, ni bilo koji od host događaja iza tih callback-ova. m_pJsPlatform se priključuje samo kada je V8FeaturesAvailable true, XFA ostaje isključen osim ako je RuntimeReady bio true, a TPdf.XFA nastavlja da prijavljuje tip formulara iz FPDF_GetFormType bez obzira na to šta je okruženje pregovaralo. Host koji želi da zna da li će se dinamički XFA zaista renderovati treba da nastavi da čita XfaRuntimeAvailable pošto Active postane true, kako naša beleška o detekciji XFA formulara i izvlačenju XFA paketa preporučuje, a ne da bilo šta zaključuje iz polja verzije
procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
// Pali se iz InitializeFormFill kada je dokument XFA ali učitani
// pdfium.dll ne može da pokrene engine. Okruženje forme se i dalje otvara,
// jer je verzija 2 prosleđena u oba slučaja; isključen je samo 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; // više ne baca izuzetak na običnom PDF-u pod pdfium.v8.dll
if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
ShowStaticXfaWarning;
end;
Verzija protokola i dostupnost funkcionalnosti su dve različite ose
Opšte pravilo koje ispada iz ove ispravke je da polje verzije u strukturi callback-ova odgovara na pitanje „koliki je ovaj zapis i šta iz njega smete da čitate", dok detekcija funkcionalnosti odgovara na pitanje „koji od tih slotova će raditi nešto korisno". Prvo je fiksirano nativnim binary-jem i Pascal deklaracijom protiv koje ste kompajlirali. Drugo varira po dokumentu, po tabeli exporta DLL-a i po konfiguraciji hosta. Sažimanje ta dva u jedan boolean je primamljivo jer XFA slučaj slučajno traži oba, ali u trenutku kada build zahteva minimalnu verziju, to sažimanje se lomi za svaki dokument kojem ta funkcionalnost ne treba. XFA formulari, opisani u ISO 32000-1 §12.7.8 kao XML payload koji živi uz AcroForm rečnik, ovde su funkcionalnost; raspored zapisa je protokol, i PDFium ima pravo da insistira na rasporedu pre nego što uopšte pogleda fajl. Isti oblik se pojavljuje svuda gde C biblioteka verzionira svoje strukture: blok viewer-info, zapis render-options, tabela platform callback-ova. Bezbedan obrazac je onaj koji ispravljeni InitializeFormFill sledi. Deklarišite najnoviji raspored koji razumete, očistite ga u potpunosti, postavite verziju tako da odgovara tom rasporedu bezuslovno, a zatim pustite provere mogućnosti da odluče koje slotove ćete popuniti. Ako budući PDFium header doda verziju 3, izmena je u deklaraciji i u tom jednom dodeljivanju, a ne u grani zavisnoj od dokumenta koja će biti pogrešna za onu kombinaciju koju niko nije testirao
Ispravljena inicijalizacija form-fill-a isporučuje se u PDFium Component za Delphi, Lazarus i C++Builder, i primenjuje se isto na Win32 i Win64 jer oba builda dele istu deklaraciju zapisa. Ako vaša aplikacija već bira pdfium.v8.dll zbog AcroForm-ova vođenih JavaScript-om, ovo je izmena koja joj omogućava da otvori i ostatak vaše PDF arhive kroz isti binary bez posebnog tretiranja okruženja forme