Tehnički članak

FPDF_FORMFILLINFO verzija 2 u Delphiju: slijedite ABI DLL-a

PDFium Component sada postavlja FPDF_FORMFILLINFO.version na 2 za svako okruženje za ispunjavanje obrazaca koje inicijalizira, jer je verzija koju neki native PDFium build prihvaća svojstvo tog builda, a ne dokumenta koji se otvara. pdfium.v8.dll s omogućenim XFA odbija verziju 1 odmah, pa je običan AcroForm PDF otvoren kroz njega prije padao u FPDFDOC_InitFormFillEnvironment bez ijednog XFA-a na vidiku. Popravak u v3.116.0 je malen, ali greška iza njega je općenita i vrijedi je imenovati: polje verzije protokola opisuje memorijski layout koji druga strana očekuje, i nikad se ne smije izvoditi iz toga trebaju li vam slučajno značajke koje taj layout nosi

Zašto FPDFDOC_InitFormFillEnvironment pada na običnom PDF-u s pdfium.v8.dll?

Okruženje pada zato što PDFium build s omogućenim XFA validira polje version prije nego išta drugo učini, a stara logika wrappera predala mu je 1 kad god trenutni dokument nije bio XFA obrazac. Simptom u Delphi hostu je EPdfError podignut iz TPdf.InitializeFormFill s porukom Cannot initialize form fill environment, bačen pri otvaranju običnog računa ili poreznog obrasca koji nema ništa osim AcroForm tekstualnih polja. Ista datoteka otvara se bez problema uz obični pdfium.dll. Isti DLL otvara pravi XFA dokument bez problema. Lomi se samo kombinacija V8 builda i ne-XFA dokumenta, što je upravo kombinacija u kojoj host završi nakon što uključi EnableV8Engine da dobije AcroForm JavaScript, ili nakon što je automatski odabir u LoadDocument već opredijelio proces za pdfium.v8.dll zbog ranije XFA datoteke. Ta opredijeljenost vrijedi za cijeli proces: EnableV8Engine čita se prije prvog LoadLibrary, i jednom kada je XFA build učitan, svaki kasniji obični PDF prolazi kroz isto postavljanje okruženja nad istom binarnom datotekom. Host nije učinio ništa pogrešno; wrapper je postavio pogrešno pitanje kada je popunjavao zapis. Ako još odlučujete koju binarnu datoteku uopće isporučiti, naša bilješka o isporuci PDFium DLL-a i dijagnosticiranju padova pri učitavanju pokriva odabir obične nasuprot V8 verziji, a ovaj članak pretpostavlja da je V8 build već u procesu

PDFium Component dijagram četiriju kombinacija običnog pdfium.dll i pdfium.v8.dll s XFA podrškom nasuprot AcroForm i XFA dokumenata: zapis verzije 1 lomio je samo V8 build s običnim obrascem, EPdfError u FPDFDOC_InitFormFillEnvironment, dok ispravljeni zapis verzije 2 otvara sve četiri
Jedan uvjet vezao je ABI verziju uz dokument, pa je odabir V8 binarne datoteke na razini procesa pretvorio svaki kasniji obični PDF u neuspješnu inicijalizaciju okruženja

Što polje version u FPDF_FORMFILLINFO doista obećava?

FPDF_FORMFILLINFO.version govori PDFiumu koja polja zapisa smije čitati, a javno zaglavlje fpdf_formfill.h veže prihvatljive vrijednosti uz to kako je biblioteka kompilirana, a ne uz dokument. Prepričano, ugovor ima tri dijela. Verzija 1 pokriva stabilne callbackove od FFI_Invalidate do FFI_DoGoToAction plus pointer m_pJsPlatform. Build bez XFA modula prihvaća 1 ili 2, a s 2 će pozvati i dodatne eksperimentalne callbackove. Build s XFA modulom zahtijeva 2, bez iznimke, i zaglavlje to ponavlja dvaput kao da očekuje da će ljudi to propustiti. Nigdje ugovor ne spominje dokument. Verzija je izjava o zapisu koji ste alocirali: s 2 obećavate da memorija nakon m_pJsPlatform postoji i drži ili valjane function pointere ili NULL

Regija verzije 2 mjesto je gdje živi cijeli XFA mehanizam. Započinje s xfa_disabled, FPDF_BOOL koji zaglavlje opisuje kao ignoriran ispod verzije 2 i značajan samo kada je XFA modul kompiliran, i nastavlja sa sedamnaest function pointera, FFI_DisplayCaret do FFI_DoURIActionWithKeyboardModifier. Svaki od njih dokumentiran je kao obavezan za XFA, a inače ga treba postaviti na NULL. Ta formulacija ključ je cijelog popravka. NULL nije stanje greške za te slotove; to je dokumentirano stanje za host koji ne pokreće XFA. Zapis koji je očišćen s FillChar i zatim označen kao verzija 2 zadovoljava ugovor na ne-XFA buildu jednako dobro kao zapis verzije 1, i to je jedini zapis koji će XFA build prihvatiti

PDFium Component dijagram zapisa FPDF_FORMFILLINFO u Delphiju: verzija 1 pokriva callbackove od FFI_Invalidate do FFI_DoGoToAction plus m_pJsPlatform, verzija 2 dodaje xfa_disabled i sedamnaest pointera iz ere FFI_DisplayCaret, FillChar čisti svaki bajt, a NULL slotovi dokumentirano su stanje za host koji ne pokreće XFA
Pascal zapis uvijek je potpuni layout verzije 2, pa ga XFA build prihvaća, a obični build jednostavno nikad ne pozove eksperimentalne slotove koji ostaju NULL

Stari odabir vezao je ABI uz dokument

Defekt je bio jedan uvjet koji je izolirano izgledao razumno. TPdf.InitializeFormFill računa zastavicu RuntimeReady iz triju činjenica: dokument prijavljuje XFA tip obrasca kroz TPdf.XFA, XFA string pomoćnici razriješeni su kroz XfaFeaturesAvailable, a V8 izvozi razriješeni kroz V8FeaturesAvailable. Prije v3.116.0 ta ista zastavica birala je i verziju

// v3.115.0 i starije: ABI verzija slijedila je 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 prikivala ju je ponovno
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 zaglavljem u ruci i pad je očit. 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 build, PDFium provjerava polje, nalazi ga ispod obaveznog 2, i vraća null FPDF_FORMHANDLE, što CheckPdf pretvara u gornju iznimku. Namjera starog koda bila je obrambena: zadržati verziju 1 da XFA build nikad ne čita nedodijeljene slotove verzije 2. Branio se od problema koji zaglavlje već isključuje i stvorio onaj na koji zaglavlje izričito upozorava. Ispravljeni kod odlučuje o verziji jednom, unaprijed, iz onoga što zapis fizički jest

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;

  // Puni zapis verzije 2 alociran je i očišćen iznad. PDFium
  // prihvaća verziju 2 bez XFA i zahtijeva je u svakom buildu s XFA,
  // uključujući i kada ovaj dokument ne sadrži XFA obrazac.
  FFormFillInfo.Info.version := 2;
  FFormFillInfo.Info.xfa_disabled := 1;

  // RuntimeReady određuje XFA callbackove i xfa_disabled, nikad verziju.
  RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
  ...

Gdje RuntimeReady još pripada: callbackovi i xfa_disabled

RuntimeReady zadržava svoju ulogu kapije za XFA ponašanje; samo više ne dira layout zapisa. Callbackovi verzije 1, FFI_Invalidate, FFI_SetTimer, FFI_GetPage, FFI_DoURIAction, FFI_DoGoToAction i ostatak tog bloka, povezani su bezuvjetno jer i AcroForm i XFA ovise o njima. Sedamnaest pointera verzije 2 dodjeljuje se samo unutar grane RuntimeReady, zajedno s xfa_disabled := 0. Kada je dokument XFA, ali runtime nije tu, zapis ostaje na verziji 2 s xfa_disabled na 1 i slotovima verzije 2 ostavljenima na NULL, a wrapper diže OnXfaRuntimeMissing da host može predložiti ponovno pokretanje na pdfium.v8.dll. Nakon što okruženje postoji, FPDF_LoadXFA poziva se samo kada je RuntimeReady bio true, i samo istinit povratak postavlja FXfaRuntimeUsable, što TPdf.XfaRuntimeAvailable i prijavljuje

  if RuntimeReady then
  begin
    FFormFillInfo.Info.xfa_disabled := 0;   // 0 = XFA omoguć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, obavijesti 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 vlastiti binding. FXfaPageCountOverride vraća se na -1 kao sentinel prije nego se išta drugo dogodi, pa PageCount pada natrag na statično stablo stranica dok FFI_PageEvent ne prijavi ponovno paginiranje; nula bi ondje tiho tvrdila da je dokument prazan. A svaki od callbackova verzije 2 statična je cdecl rutina koja vraća vlasnički TPdf iz zapisa i guta svaku Pascal iznimku prije povratka u PDFium, što je disciplina koju naša bilješka o očvršćivanju PDFium ABI-ja u Delphiju razlaže za FFI_OpenFile. Ništa u promjeni verzije ne popušta nijedno od tih dvaju pravila

Je li verzija 2 sigurna kada DLL nema XFA modul?

Jest, i razlog je u zapisu, a ne u obećanju biblioteke. Na ne-XFA buildu zaglavlje kaže da verzija 2 uzrokuje da se pozovu i eksperimentalni callbackovi, pa je pitanje što PDFium nađe kada pogleda. TPdfFormFillInfo je packed record čiji je član Info potpuni FPDF_FORMFILLINFO uključujući svako polje verzije 2, a InitializeFormFill čisti cijelu stvar s FillChar prije nego dotakne ijedan bajt. Pa na običnom pdfium.dll s običnim dokumentom biblioteka vidi verziju 2, postavljen xfa_disabled i NULL u svakom eksperimentalnom slotu, što je upravo stanje koje zaglavlje propisuje za host koji ne implementira XFA. Nema skraćenog zapisa koji bi biblioteka mogla čitati preko ruba, jer zapis nikad nije bio kraći od verzije 2. Stara logika branila je nesklad layouta koji je Pascal deklaracija već uklonila

Granica koju valja iskreno reći je ona koju zapis ne može pokriti. Verzija 2 na običnom dokumentu ne uključuje JavaScript, XFA skriptiranje niti bilo koji od host događaja iza tih callbackova. m_pJsPlatform prikvačen je samo kada je V8FeaturesAvailable true, XFA ostaje isključen osim ako RuntimeReady nije bio true, a TPdf.XFA i dalje prijavljuje tip obrasca iz FPDF_GetFormType bez obzira na to što je okruženje pregovaralo. Host koji želi znati hoće li se dinamički XFA doista renderirati treba i dalje čitati XfaRuntimeAvailable nakon što Active postane true, kako preporučuje naša bilješka o detekciji XFA obrazaca i izvlačenju XFA paketa, umjesto da bilo što zaključuje iz polja verzije

procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Aktivira se iz InitializeFormFill kada je dokument XFA, ali učitani
  // pdfium.dll ne može pokrenuti engine. Okruženje obrasca i dalje se otvara,
  // jer je verzija 2 predana 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 diže iznimku na običnom PDF-u pod pdfium.v8.dll
  if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
    ShowStaticXfaWarning;
end;

Verzija protokola i dostupnost značajki dvije su različite osi

Opće pravilo koje iz ovog popravka slijedi jest da polje verzije u strukturi callbackova odgovara na pitanje "koliko je taj zapis velik i što smijete čitati iz njega", dok detekcija značajki odgovara na "koji će od tih slotova učiniti nešto korisno". Prvo je fiksirano native binarnom datotekom i Pascal deklaracijom s kojom ste kompajlirali. Drugo varira po dokumentu, po tablici izvoza DLL-a i po konfiguraciji hosta. Spajanje toga dvoga u jedan boolean primamljivo je jer XFA slučaj slučajno treba oboje, ali u trenutku kada neki build provodi minimalnu verziju, to spajanje puca za svaki dokument kojem ta značajka ne treba. XFA obrasci, opisani u ISO 32000-1 §12.7.8 kao XML payload koji živi uz AcroForm rječnik, ovdje su značajka; layout zapisa je protokol, i PDFium ima pravo inzistirati na layoutu prije nego uopće pogleda datoteku. Isti oblik pojavljuje se gdje god C biblioteka verzionira svoje strukture: viewer-info blok, zapis opcija renderiranja, tablica platform callbackova. Siguran uzorak je onaj koji slijedi ispravljeni InitializeFormFill. Deklarirajte najnoviji layout koji razumijete, očistite ga u cijelosti, postavite verziju tako da odgovara tom layoutu bezuvjetno, i zatim pustite provjere sposobnosti da odluče koje slotove popuniti. Ako buduće PDFium zaglavlje doda verziju 3, promjena je u deklaraciji i u toj jednoj dodjeli, a ne u grani ovisnoj o dokumentu koja će biti pogrešna za onu kombinaciju koju nitko nije testirao

PDFium Component dijagram koji razdvaja dvije osi iza FPDF_FORMFILLINFO: verziju protokola fiksiranu layoutom zapisa i native binarnom datotekom, te dostupnost značajki gdje RuntimeReady određuje xfa_disabled, sedamnaest slotova verzije 2, FPDF_LoadXFA i m_pJsPlatform po dokumentu i po hostu
Polje verzije opisuje memoriju koju druga strana smije čitati, provjere sposobnosti odlučuju koji slotovi čine nešto korisno, a spajanje toga dvoga u jedan boolean lomi build koji provodi minimum

Ispravljena inicijalizacija ispunjavanja obrazaca isporučuje se u PDFium Component za Delphi, Lazarus i C++Builder, i primjenjuje se i na Win32 i na Win64 jer oba builda dijele istu deklaraciju zapisa. Ako vaša aplikacija već odabire pdfium.v8.dll za AcroForm obrasce vođene JavaScriptom, ovo je promjena koja joj omogućuje da kroz istu binarnu datoteku otvori i ostatak vaše PDF arhive bez posebnog tretiranja okruženja obrasca