Tehnični članak

FPDF_FORMFILLINFO različica 2: po ABI knjižnice DLL

PDFium Component zdaj za vsako okolje za izpolnjevanje obrazcev, ki ga inicializira, nastavi FPDF_FORMFILLINFO.version na 2, ker je različica, ki jo izvorna gradnja PDFium sprejme, lastnost te gradnje in ne dokumenta, ki ga odpirate. pdfium.v8.dll z omogočenim XFA različico 1 zavrne naravnost, zato je običajen PDF z AcroForm, odprt skozenj, nekoč odpovedal v FPDFDOC_InitFormFillEnvironment, čeprav XFA ni bilo videti nikjer. Popravek v različici 3.116.0 je majhen, a napaka za njim je splošna in si zasluži ime: polje različice protokola opisuje postavitev pomnilnika, ki jo pričakuje druga stran, in ga ni nikoli dovoljeno izpeljati iz tega, ali slučajno potrebujete funkcije, ki jih ta postavitev nosi

Zakaj FPDFDOC_InitFormFillEnvironment odpove pri običajnem PDF s pdfium.v8.dll?

Okolje odpove, ker gradnja PDFium z omogočenim XFA preveri polje version, preden naredi kar koli drugega, stara logika ovoja pa mu je podala 1 vedno, kadar trenutni dokument ni bil obrazec XFA. Simptom v gostitelju Delphi je EPdfError, sprožen iz TPdf.InitializeFormFill s sporočilom Cannot initialize form fill environment, vržen med odpiranjem običajnega računa ali davčnega obrazca, ki nima nič razen besedilnih polj AcroForm. Ista datoteka se brez težav odpre proti navadnemu pdfium.dll. Ista knjižnica DLL brez težav odpre resničen dokument XFA. Zlomi se le kombinacija gradnje V8 in dokumenta brez XFA, kar je natanko kombinacija, v katero gostitelj zaide potem, ko vklopi EnableV8Engine zaradi JavaScripta AcroForm, ali ko je samodejna izbira v LoadDocument proces že zavezala na pdfium.v8.dll zaradi prejšnje datoteke XFA. Ta zaveza velja za celoten proces: EnableV8Engine se prebere pred prvim LoadLibrary, in ko je gradnja XFA enkrat naložena, gre vsak poznejši običajen PDF skozi isto postavitev okolja proti isti binarni datoteki. Gostitelj ni naredil nič narobe; ovoj je zastavil napačno vprašanje, ko je polnil zapis. Če se še odločate, katero binarno datoteko sploh dostaviti, naš zapisek o nameščanju knjižnice DLL PDFium in diagnosticiranju napak pri nalaganju pokriva izbiro med navadno in gradnjo V8, ta članek pa predpostavlja, da je gradnja V8 že v procesu

Diagram PDFium Component s štirimi kombinacijami navadne pdfium.dll in pdfium.v8.dll z XFA proti dokumentom AcroForm in XFA: zapis z različico 1 je zlomil le gradnjo V8 pri običajnem obrazcu, EPdfError v FPDFDOC_InitFormFillEnvironment, popravljeni zapis z različico 2 pa odpre vse štiri
En pogoj je različico ABI-ja privezal na dokument, zato je izbira binarne datoteke V8 na ravni celotnega procesa vsak poznejši običajen PDF spremenila v neuspešno inicializacijo okolja

Kaj polje version v FPDF_FORMFILLINFO pravzaprav obljublja?

FPDF_FORMFILLINFO.version pove PDFiumu, katera polja zapisa sme brati, javna glava fpdf_formfill.h pa sprejemljive vrednosti veže na to, kako je bila knjižnica prevedena, in ne na dokument. Prevedeno v besede ima pogodba tri dele. Različica 1 pokriva stabilne povratne klice od FFI_Invalidate do FFI_DoGoToAction plus kazalec m_pJsPlatform. Gradnja brez modula XFA sprejme 1 ali 2, pri 2 pa pokliče tudi dodatne poskusne povratne klice. Gradnja z modulom XFA zahteva 2, pika, in glava to zahtevo ponovi dvakrat, kot da pričakuje, da jo bodo ljudje spregledali. Pogodba dokumenta ne omeni nikjer. Različica je izjava o zapisu, ki ste ga dodelili: z 2 obljubljate, da pomnilnik za m_pJsPlatform obstaja in drži veljavne kazalce funkcij ali NULL

Območje različice 2 je tisto, kjer živi vsa mašinerija XFA. Začne se z xfa_disabled, FPDF_BOOL, ki ga glava opisuje kot prezrtega pod različico 2 in smiselnega le, kadar je modul XFA preveden v knjižnico, nadaljuje pa se s sedemnajstimi kazalci funkcij, od FFI_DisplayCaret do FFI_DoURIActionWithKeyboardModifier. Za vsakega od njih je dokumentirano, da je za XFA obvezen, sicer pa naj bi bil nastavljen na NULL. Ta formulacija je ključ do celotnega popravka. NULL za te reže ni stanje napake; je dokumentirano stanje za gostitelja, ki ne poganja XFA. Zapis, ki je bil počiščen s FillChar in nato označen kot različica 2, pogodbo pri gradnji brez XFA izpolni prav tako dobro kot zapis z različico 1, in je edini zapis, ki ga bo gradnja XFA sprejela

Diagram PDFium Component zapisa FPDF_FORMFILLINFO v Delphiju: različica 1 pokriva povratne klice od FFI_Invalidate do FFI_DoGoToAction plus m_pJsPlatform, različica 2 doda xfa_disabled in sedemnajst kazalcev iz obdobja FFI_DisplayCaret, FillChar počisti vsak bajt, reže z NULL pa so dokumentirano stanje za gostitelja, ki ne poganja XFA
Zapis Pascal je vedno popolna postavitev različice 2, zato ga gradnja z omogočenim XFA sprejme, navadna gradnja pa preprosto nikoli ne pokliče poskusnih rež, ki ostanejo NULL

Stara izbira je ABI privezala na dokument

Defekt je bil en sam pogoj, ki je izoliran izgledal razumno. TPdf.InitializeFormFill izračuna zastavico RuntimeReady iz treh dejstev: dokument prek TPdf.XFA prijavi tip obrazca XFA, pomožni elementi za nize XFA so se razrešili prek XfaFeaturesAvailable in izvozi V8 so se razrešili prek V8FeaturesAvailable. Pred različico 3.116.0 je ta ista zastavica izbrala tudi različico

// v3.115.0 in starejše: različica ABI-ja je sledila dokumentu
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;

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

// ... in veja za manjkajoče okolje jo je pripela znova
else if XFA then
begin
  FFormFillInfo.Info.version := 1;
  FFormFillInfo.Info.xfa_disabled := 1;
  if Assigned(FOnXfaRuntimeMissing) then
    FOnXfaRuntimeMissing(Self);
end;

Preberite to z glavo v roki in odpoved je očitna. RuntimeReady je neresničen za vsak običajen dokument AcroForm, zato je vsak običajen dokument naznanil različico 1. Na pdfium.dll je to v redu. Na pdfium.v8.dll, kar je gradnja z omogočenim XFA, PDFium polje preveri, ugotovi, da je pod zahtevano 2, in vrne ničeln FPDF_FORMHANDLE, kar CheckPdf spremeni v zgornjo izjemo. Namen stare kode je bil obrambni: obdržati različico 1, da gradnja XFA nikoli ne prebere nedodeljenih rež različice 2. Branila se je pred težavo, ki jo glava že izključuje, in ustvarila tisto, pred katero glava izrecno opozarja. Popravljena koda o različici odloči enkrat, vnaprej, iz tega, kaj zapis fizično je

procedure TPdf.InitializeFormFill;
var
  RuntimeReady: Boolean;
begin
  FXfaRuntimeUsable := False;
  FXfaPageCountOverride := -1;   // stražar: uporabi statično drevo strani
  if not FormFill then
    Exit;

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

  // Zapis različice 2 je zgoraj dodeljen in počiščen v celoti. PDFium
  // sprejme različico 2 brez XFA in jo zahteva v vsaki gradnji z
  // omogočenim XFA, tudi kadar ta dokument ne vsebuje obrazca XFA.
  FFormFillInfo.Info.version := 2;
  FFormFillInfo.Info.xfa_disabled := 1;

  // RuntimeReady krmili povratne klice XFA in xfa_disabled, nikoli različice.
  RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
  ...

Kam RuntimeReady še spada: povratni klici in xfa_disabled

RuntimeReady obdrži svojo nalogo kot vrata za vedenje XFA; le postavitve zapisa se ne dotika več. Povratni klici različice 1, FFI_Invalidate, FFI_SetTimer, FFI_GetPage, FFI_DoURIAction, FFI_DoGoToAction in preostanek tega bloka, so povezani brezpogojno, ker sta od njih odvisna tako AcroForm kot XFA. Sedemnajst kazalcev različice 2 se dodeli le znotraj veje RuntimeReady, skupaj z xfa_disabled := 0. Ko je dokument XFA, okolja pa ni, zapis ostane pri različici 2 z xfa_disabled pri 1 in režami različice 2, ki ostanejo NULL, ovoj pa sproži OnXfaRuntimeMissing, da lahko gostitelj predlaga ponoven zagon na pdfium.v8.dll. Ko okolje obstaja, se FPDF_LoadXFA pokliče le, če je bil RuntimeReady resničen, in le resničen rezultat nastavi FXfaRuntimeUsable, kar poroča TPdf.XfaRuntimeAvailable

  if RuntimeReady then
  begin
    FFormFillInfo.Info.xfa_disabled := 0;   // 0 = XFA omogoč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
    // Okolje ni na voljo: obdrži različico 2, pusti XFA izklopljen, obvesti gostitelja.
    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;

Dve podrobnosti v tem bloku zlahka zamočite, ko pišete svojo povezavo. FXfaPageCountOverride se ponastavi na -1 kot stražar, preden se zgodi kar koli drugega, zato PageCount pade nazaj na statično drevo strani, dokler FFI_PageEvent ne prijavi ponovnega paginiranja; ničla na tem mestu bi tiho naznanila prazen dokument. In vsak od povratnih klicev različice 2 je statična rutina cdecl, ki iz zapisa izlušči lastnika TPdf in pogoltne vsako izjemo Pascal, preden se vrne v PDFium, kar je disciplina, ki jo naš zapisek o utrjevanju ABI-ja PDFium v Delphiju predpiše za FFI_OpenFile. Sprememba različice nobenega od obeh pravil ne omili

Je različica 2 varna, kadar knjižnica DLL nima modula XFA?

Da, in razlog je v zapisu, ne v obljubi knjižnice. Pri gradnji brez XFA glava pravi, da različica 2 povzroči, da se pokličejo tudi poskusni povratni klici, zato je vprašanje, kaj PDFium najde, ko pogleda. TPdfFormFillInfo je pakiran zapis, katerega član Info je celoten FPDF_FORMFILLINFO, vključno z vsakim poljem različice 2, in InitializeFormFill po celotnem zapisu požene FillChar, preden se dotakne enega bajta. Na navadnem pdfium.dll z običajnim dokumentom torej knjižnica vidi različico 2, nastavljen xfa_disabled in NULL v vsaki poskusni reži, kar je natanko stanje, ki ga glava predpisuje za gostitelja, ki XFA ne izvaja. Odrezanega zapisa, čez katerega bi knjižnica brala, ni, ker zapis nikoli ni bil krajši od različice 2. Stara logika je branila pred neujemanjem postavitve, ki ga je deklaracija Pascal že odpravila

Meja, ki jo velja pošteno povedati, je tista, ki je zapis ne more pokriti. Različica 2 pri običajnem dokumentu ne vklopi JavaScripta, skriptov XFA ali katerega od gostiteljskih dogodkov za temi povratnimi klici. m_pJsPlatform se pripne le, kadar je V8FeaturesAvailable resničen, XFA ostane izklopljen, razen če je bil RuntimeReady resničen, in TPdf.XFA še naprej poroča tip obrazca iz FPDF_GetFormType, ne glede na to, kaj je okolje izpogajalo. Gostitelj, ki ga zanima, ali se bo dinamični XFA res upodobil, naj po tem, ko se Active postavi na true, še naprej bere XfaRuntimeAvailable, kot priporoča naš zapisek o zaznavanju obrazcev XFA in izluščevanju paketov XFA, namesto da bi kar koli sklepal iz polja različice

procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Sproži se iz InitializeFormFill, ko je dokument XFA, naloženi
  // pdfium.dll pa ne more pognati motorja. Okolje obrazcev se vseeno
  // odpre, ker je bila različica 2 podana v vsakem primeru; izklopljeno je le okolje XFA.
  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;   // pri običajnem PDF pod pdfium.v8.dll ne vrže več izjeme
  if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
    ShowStaticXfaWarning;
end;

Različica protokola in razpoložljivost funkcij sta dve različni osi

Splošno pravilo, ki izhaja iz tega popravka, je, da polje različice v strukturi povratnih klicev odgovarja na vprašanje "kako velik je ta zapis in kaj smete brati iz njega", zaznavanje funkcij pa na vprašanje "katera od teh rež bo naredila kaj koristnega". Prvo določata izvorna binarna datoteka in deklaracija Pascal, proti kateri ste prevedli. Drugo se spreminja po dokumentu, po izvozni tabeli knjižnice DLL in po nastavitvi gostitelja. Zliti oboje v en logični pogoj je mamljivo, ker primer XFA slučajno potrebuje oboje, a v trenutku, ko gradnja uveljavi najnižjo različico, se to zlitje zlomi za vsak dokument, ki te funkcije ne potrebuje. Obrazci XFA, ki jih ISO 32000-1 §12.7.8 opisuje kot tovor XML, ki živi ob slovarju AcroForm, so tu funkcija; postavitev zapisa je protokol in PDFium je upravičen vztrajati pri postavitvi, preden sploh pogleda datoteko. Ista oblika se pojavi povsod, kjer knjižnica C različiči svoje strukture: blok informacij o pregledovalniku, zapis možnosti upodabljanja, tabela povratnih klicev platforme. Varen vzorec je tisti, ki ga sledi popravljeni InitializeFormFill. Razglasite najnovejšo postavitev, ki jo razumete, jo počistite v celoti, različico nastavite tako, da ustreza tej postavitvi, brezpogojno, in nato prepustite preverjanjem zmogljivosti, da odločijo, katere reže zapolniti. Če bo prihodnja glava PDFium dodala različico 3, bo sprememba v deklaraciji in v tisti eni dodelitvi, ne pa v od dokumenta odvisni veji, ki bo napačna za tisto kombinacijo, ki je nihče ni preskusil

Diagram PDFium Component, ki loči obe osi za FPDF_FORMFILLINFO: različico protokola, ki jo določata postavitev zapisa in izvorna binarna datoteka, ter razpoložljivost funkcij, kjer RuntimeReady krmili xfa_disabled, sedemnajst rež različice 2, FPDF_LoadXFA in m_pJsPlatform po dokumentu in po gostitelju
Polje različice opisuje pomnilnik, ki ga druga stran sme brati, preverjanja zmogljivosti odločijo, katere reže naredijo kaj koristnega, zlitje obojega v en logični pogoj pa zlomi gradnjo, ki uveljavlja najnižjo različico

Popravljena inicializacija izpolnjevanja obrazcev izhaja v komponenti PDFium Component za Delphi, Lazarus in C++Builder, velja pa enako na Win32 in Win64, ker si obe gradnji delita isto deklaracijo zapisa. Če vaša aplikacija že izbere pdfium.v8.dll zaradi obrazcev AcroForm, ki jih poganja JavaScript, je to sprememba, ki ji omogoči, da preostanek svojega arhiva PDF odpre skozi isto binarno datoteko, brez posebne obravnave okolja obrazcev