PDFium Component setează acum FPDF_FORMFILLINFO.version la 2 pentru fiecare mediu de completare de formulare pe care îl inițializează, pentru că versiunea pe care o acceptă un build nativ de PDFium este o proprietate a acelui build, nu a documentului care se deschide. Un pdfium.v8.dll cu XFA respinge direct versiunea 1, așa că un PDF AcroForm simplu deschis prin el eșua în FPDFDOC_InitFormFillEnvironment fără să existe vreun XFA în peisaj. Reparația din v3.116.0 este mică, dar greșeala din spatele ei este una generală și merită numită: un câmp de versiune de protocol descrie aspectul memoriei pe care îl așteaptă cealaltă parte și nu trebuie niciodată derivat din faptul că aveți sau nu nevoie de funcționalitățile pe care le cară acel aspect
De ce eșuează FPDFDOC_InitFormFillEnvironment pe un PDF simplu cu pdfium.v8.dll?
Mediul eșuează pentru că un build PDFium cu XFA validează câmpul version înainte de a face orice altceva, iar vechea logică a wrapper-ului îi dădea un 1 ori de câte ori documentul curent nu era un formular XFA. Simptomul într-un host Delphi este un EPdfError ridicat din TPdf.InitializeFormFill cu mesajul Cannot initialize form fill environment, aruncat la deschiderea unei facturi sau a unui formular fiscal obișnuit care nu are decât câmpuri de text AcroForm. Același fișier se deschide fără probleme pe pdfium.dll simplu. Același DLL deschide fără probleme un document XFA adevărat. Doar combinația dintre build-ul V8 și un document non-XFA se rupe, exact combinația în care ajunge un host după ce pornește EnableV8Engine ca să obțină JavaScript pentru AcroForm, sau după ce auto-selectarea din LoadDocument a angajat deja procesul pe pdfium.v8.dll pentru un fișier XFA anterior. Acel angajament este la nivel de proces: EnableV8Engine este citit înainte de primul LoadLibrary, iar odată ce build-ul XFA este încărcat, fiecare PDF simplu de mai târziu trece prin aceeași inițializare de mediu împotriva aceluiași binar. Host-ul nu a greșit cu nimic; wrapper-ul a pus întrebarea greșită când a completat înregistrarea. Dacă încă decideți ce binar să livrați, nota noastră despre livrarea DLL-ului PDFium și diagnosticarea erorilor de încărcare acoperă alegerea între simplu și V8, iar acest articol presupune că build-ul V8 este deja în proces
Ce promite de fapt câmpul version din FPDF_FORMFILLINFO?
FPDF_FORMFILLINFO.version îi spune lui PDFium ce câmpuri ale înregistrării are voie să citească, iar antetul public fpdf_formfill.h leagă valorile acceptabile de felul în care a fost compilată librăria, nu de document. În parafrază, contractul are trei părți. Versiunea 1 acoperă callback-urile stabile de la FFI_Invalidate până la FFI_DoGoToAction, plus pointerul m_pJsPlatform. Un build fără modulul XFA acceptă fie 1, fie 2, iar cu 2 va apela în plus callback-urile experimentale suplimentare. Un build cu modulul XFA cere 2, punct, iar antetul repetă acea cerință de două ori, ca și cum s-ar aștepta ca lumea să o rateze. Nicăieri contractul nu menționează documentul. Versiunea este o afirmație despre înregistrarea pe care ați alocat-o: cu un 2 promiteți că memoria de după m_pJsPlatform există și conține fie pointeri de funcție valizi, fie NULL
Regiunea versiunii 2 este locul unde trăiește toată mecanica XFA. Începe cu xfa_disabled, un FPDF_BOOL pe care antetul îl descrie ca ignorat sub versiunea 2 și semnificativ doar când modulul XFA este compilat, și continuă cu șaptesprezece pointeri de funcție, de la FFI_DisplayCaret până la FFI_DoURIActionWithKeyboardModifier. Fiecare dintre ei este documentat ca obligatoriu pentru XFA și, în rest, ca trebuind setat la NULL. Acea formulare este cheia întregii reparații. NULL nu este o stare de eroare pentru acele poziții; este starea documentată pentru un host care nu conduce XFA. O înregistrare care a fost curățată cu FillChar și apoi marcată drept versiunea 2 satisface contractul pe un build non-XFA exact la fel de bine ca o înregistrare de versiunea 1 și este singura înregistrare pe care o acceptă un build XFA
Vechea selecție lega ABI-ul de document
Defectul era un singur condițional care părea rezonabil în izolare. TPdf.InitializeFormFill calculează un flag RuntimeReady din trei fapte: documentul raportează un tip de formular XFA prin TPdf.XFA, helper-ele de șiruri XFA s-au rezolvat prin XfaFeaturesAvailable, iar exporturile V8 s-au rezolvat prin V8FeaturesAvailable. Înainte de v3.116.0 același flag alegea și versiunea
// v3.115.0 și mai vechi: versiunea ABI urma documentul
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
if RuntimeReady then
FFormFillInfo.Info.version := 2
else
FFormFillInfo.Info.version := 1;
// ... iar ramura de runtime lipsă o fixa din nou
else if XFA then
begin
FFormFillInfo.Info.version := 1;
FFormFillInfo.Info.xfa_disabled := 1;
if Assigned(FOnXfaRuntimeMissing) then
FOnXfaRuntimeMissing(Self);
end;
Citiți-l cu antetul în mână și eșecul este evident. RuntimeReady este fals pentru fiecare document AcroForm simplu, așa că fiecare document simplu anunța versiunea 1. Pe pdfium.dll asta este în regulă. Pe pdfium.v8.dll, care este build-ul cu XFA, PDFium verifică câmpul, îl găsește sub 2-ul cerut și întoarce un FPDF_FORMHANDLE null, pe care CheckPdf îl transformă în excepția de mai sus. Intenția codului vechi era defensivă: păstrează versiunea 1 ca un build XFA să nu citească niciodată pozițiile nealocate ale versiunii 2. Se apăra împotriva unei probleme pe care antetul o exclude deja și a creat una despre care antetul avertizează explicit. Codul corectat decide versiunea o singură dată, din start, după ce este fizic înregistrarea
procedure TPdf.InitializeFormFill;
var
RuntimeReady: Boolean;
begin
FXfaRuntimeUsable := False;
FXfaPageCountOverride := -1; // sentinelă: folosește arborele static de pagini
if not FormFill then
Exit;
FillChar(FFormFillInfo, SizeOf(FFormFillInfo), 0);
FFormFillInfo.Pdf := Self;
// Înregistrarea completă de versiunea 2 este alocată și curățată mai sus. PDFium
// acceptă versiunea 2 fără XFA și o cere în fiecare build cu XFA
// activat, inclusiv când acest document nu conține niciun formular XFA.
FFormFillInfo.Info.version := 2;
FFormFillInfo.Info.xfa_disabled := 1;
// RuntimeReady condiționează callback-urile XFA și xfa_disabled, niciodată versiunea.
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
...
Unde îi este locul lui RuntimeReady: callback-urile și xfa_disabled
RuntimeReady își păstrează rolul de poartă pentru comportamentul XFA; pur și simplu nu mai atinge aspectul înregistrării. Callback-urile versiunii 1, FFI_Invalidate, FFI_SetTimer, FFI_GetPage, FFI_DoURIAction, FFI_DoGoToAction și restul blocului, sunt conectate necondiționat pentru că atât AcroForm, cât și XFA depind de ele. Cei șaptesprezece pointeri ai versiunii 2 sunt atribuiți doar în interiorul ramurii RuntimeReady, împreună cu xfa_disabled := 0. Când documentul este XFA, dar runtime-ul nu este acolo, înregistrarea rămâne la versiunea 2 cu xfa_disabled la 1 și pozițiile versiunii 2 lăsate NULL, iar wrapper-ul ridică OnXfaRuntimeMissing, ca host-ul să poată sugera repornirea pe pdfium.v8.dll. După ce mediul există, FPDF_LoadXFA este apelat doar când RuntimeReady a fost adevărat, iar doar o întoarcere adevărată setează FXfaRuntimeUsable, ceea ce raportează TPdf.XfaRuntimeAvailable
if RuntimeReady then
begin
FFormFillInfo.Info.xfa_disabled := 0; // 0 = XFA activat
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 până la FFI_DoURIActionWithKeyboardModifier
end
else if XFA then
begin
// Runtime indisponibil: păstrează versiunea 2, lasă XFA dezactivat, anunță host-ul.
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;
Două detalii din acel bloc sunt ușor de greșit când vă scrieți propria legătură. FXfaPageCountOverride este resetat la -1 ca sentinelă înainte de orice altceva, așa că PageCount cade înapoi pe arborele static de pagini până când FFI_PageEvent raportează o repaginare; un zero acolo ar pretinde în tăcere un document gol. Iar fiecare dintre callback-urile versiunii 2 este o rutină statică cdecl care recuperează TPdf-ul proprietar din înregistrare și înghite orice excepție Pascal înainte de a se întoarce la PDFium, disciplina pe care nota noastră despre securizarea ABI-ului PDFium în Delphi o detaliază pentru FFI_OpenFile. Nimic din schimbarea de versiune nu relaxează vreuna dintre cele două reguli
Este versiunea 2 sigură când DLL-ul nu are modul XFA?
Da, iar motivul este în înregistrare, nu într-o promisiune a librăriei. Pe un build non-XFA, antetul spune că versiunea 2 face ca și callback-urile experimentale să fie apelate, așa că întrebarea este ce găsește PDFium când se uită. TPdfFormFillInfo este o înregistrare packed al cărei membru Info este FPDF_FORMFILLINFO complet, inclusiv fiecare câmp al versiunii 2, iar InitializeFormFill curăță tot cu FillChar înainte de a atinge vreun octet. Așa că pe un pdfium.dll simplu cu un document simplu librăria vede versiunea 2, xfa_disabled setat și NULL în fiecare poziție experimentală, exact starea pe care antetul o prescrie pentru un host care nu implementează XFA. Nu există nicio înregistrare trunchiată peste care librăria să citească, pentru că înregistrarea nu a fost niciodată mai scurtă decât versiunea 2. Vechea logică apăra o nepotrivire de aspect pe care declarația Pascal o eliminase deja
Limita care merită spusă onest este cea pe care înregistrarea nu o poate acoperi. Versiunea 2 pe un document simplu nu pornește JavaScript, scriptarea XFA sau vreunul dintre evenimentele de host din spatele acelor callback-uri. m_pJsPlatform este atașat doar când V8FeaturesAvailable este adevărat, XFA rămâne dezactivat dacă RuntimeReady nu a fost adevărat, iar TPdf.XFA raportează în continuare tipul de formular din FPDF_GetFormType, indiferent ce a negociat mediul. Un host care vrea să știe dacă XFA dinamic va randa efectiv ar trebui să citească în continuare XfaRuntimeAvailable după ce Active devine adevărat, cum recomandă nota noastră despre detectarea formularelor XFA și extragerea pachetelor XFA, în loc să deducă ceva din câmpul de versiune
procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
// Se declanșează din InitializeFormFill când documentul este XFA, dar
// pdfium.dll încărcat nu poate rula motorul. Mediul de formular se deschide
// în continuare, pentru că versiunea 2 a fost transmisă oricum; doar runtime-ul XFA este oprit.
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; // nu mai ridică excepție pe un PDF simplu sub pdfium.v8.dll
if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
ShowStaticXfaWarning;
end;
Versiunea de protocol și disponibilitatea funcționalităților sunt două axe diferite
Regula generală care decurge din această reparație este că un câmp de versiune dintr-o structură de callback răspunde la întrebarea „cât de mare este înregistrarea asta și ce ai voie să citești din ea”, în timp ce detectarea funcționalităților răspunde la „care dintre pozițiile acelea vor face ceva util”. Prima este fixată de binarul nativ și de declarația Pascal față de care ați compilat. A doua variază per document, per tabelă de exporturi a DLL-ului și per configurație de host. Strângerea celor două într-un singur boolean este tentantă pentru că întâmplător cazul XFA are nevoie de amândouă, dar în momentul în care un build impune o versiune minimă, strângerea se rupe pentru fiecare document care nu are nevoie de funcționalitate. Formularele XFA, descrise în ISO 32000-1 §12.7.8 ca un payload XML care trăiește lângă dicționarul AcroForm, sunt funcționalitatea de aici; aspectul înregistrării este protocolul, iar PDFium are dreptul să insiste pe aspect înainte de a se uita vreodată la fișier. Aceeași formă apare oriunde o librărie C își versionează structurile: un bloc viewer-info, o înregistrare de opțiuni de randare, un tabel de callback-uri de platformă. Tiparul sigur este cel pe care îl urmează InitializeFormFill corectat. Declarați cel mai nou aspect pe care îl înțelegeți, curățați-l complet, setați versiunea să corespundă acelui aspect necondiționat și apoi lăsați verificările de capabilități să decidă ce poziții se populează. Dacă un antet PDFium viitor adaugă o versiune 3, schimbarea este în declarație și în acea singură atribuire, nu într-o ramură dependentă de document care va fi greșită pentru orice combinație pe care nimeni nu a testat-o
Inițializarea de completare de formulare corectată este livrată în PDFium Component pentru Delphi, Lazarus și C++Builder și se aplică la fel pe Win32 și Win64, pentru că ambele build-uri împart aceeași declarație de înregistrare. Dacă aplicația voastră selectează deja pdfium.v8.dll pentru AcroForm-uri conduse de JavaScript, aceasta este schimbarea care îi permite să deschidă restul arhivei de PDF-uri prin același binar, fără să trateze special mediul de formular