Teknisk artikel

FPDF_FORMFILLINFO version 2 i Delphi: följ DLL-ABI:n

PDFium Component sätter nu FPDF_FORMFILLINFO.version till 2 för varje formfill-miljö den initierar, eftersom den version en nativ PDFium-build accepterar är en egenskap hos den builden, inte hos dokumentet som öppnas. En XFA-aktiverad pdfium.v8.dll avvisar version 1 rakt av, så en vanlig AcroForm-PDF som öppnades genom den misslyckades tidigare i FPDFDOC_InitFormFillEnvironment utan att någon XFA fanns i sikte. Fixen i v3.116.0 är liten, men misstaget bakom den är allmänt och värt att namnge: ett protokollversionsfält beskriver den minneslayout som motparten förväntar sig, och det får aldrig härledas ur huruvida du råkar behöva de funktioner som layouten bär

Varför misslyckas FPDFDOC_InitFormFillEnvironment på en vanlig PDF med pdfium.v8.dll?

Miljön misslyckas för att en XFA-aktiverad PDFium-build validerar version-fältet innan den gör något annat, och den gamla wrapper-logiken gav den en 1 så fort det aktuella dokumentet inte var ett XFA-formulär. Symtomet i en Delphi-värd är ett EPdfError som kastas från TPdf.InitializeFormFill med meddelandet Cannot initialize form fill environment, utlöst när man öppnar en vanlig faktura eller ett skatteformulär som inte har annat än AcroForm-textfält. Samma fil öppnas fint mot den vanliga pdfium.dll. Samma DLL öppnar ett riktigt XFA-dokument fint. Bara kombinationen V8-bygget och ett icke-XFA-dokument går sönder, vilket är precis den kombination en värd hamnar i efter att ha slagit på EnableV8Engine för att få AcroForm-JavaScript, eller efter att autovalet i LoadDocument redan bundit processen till pdfium.v8.dll för en tidigare XFA-fil. Den bindningen gäller hela processen: EnableV8Engine läses före den första LoadLibrary, och när XFA-bygget väl är laddat går varje senare vanlig PDF genom samma miljöuppsättning mot samma binär. Värden gjorde inget fel; wrappern ställde fel fråga när den fyllde i posten. Om du fortfarande funderar på vilken binär du ska leverera över huvud taget täcker vår not om att distribuera PDFium-DLL:en och diagnostisera laddningsfel valet mellan vanlig och V8, och den här artikeln förutsätter att V8-bygget redan finns i processen

Diagram över de fyra kombinationerna av vanlig pdfium.dll och XFA-aktiverad pdfium.v8.dll mot AcroForm- och XFA-dokument i PDFium Component: en version 1-post bröt bara V8-bygget med ett vanligt formulär, EPdfError i FPDFDOC_InitFormFillEnvironment, medan den korrigerade version 2-posten öppnar alla fyra
En enda villkorssats band ABI-versionen till dokumentet, så det processvida valet av V8-binären gjorde varje senare vanlig PDF till en misslyckad miljöinitiering

Vad lovar egentligen version-fältet i FPDF_FORMFILLINFO?

FPDF_FORMFILLINFO.version talar om för PDFium vilka fält i posten den får läsa, och den publika headern fpdf_formfill.h knyter de godtagbara värdena till hur biblioteket kompilerades snarare än till dokumentet. Omskrivet har kontraktet tre delar. Version 1 täcker de stabila återanropningarna från FFI_Invalidate till FFI_DoGoToAction plus pekaren m_pJsPlatform. En build utan XFA-modulen accepterar antingen 1 eller 2, och med 2 anropar den även de ytterligare experimentella återanropningarna. En build med XFA-modulen kräver 2, punkt slut, och headern upprepar det kravet två gånger som om den räknade med att folk skulle missa det. Ingenstans nämner kontraktet dokumentet. Versionen är ett påstående om posten du allokerade: med en 2 lovar du att minnet efter m_pJsPlatform finns och innehåller antingen giltiga funktionspekare eller NULL

Version 2-regionen är där hela XFA-maskineriet bor. Den börjar med xfa_disabled, en FPDF_BOOL som headern beskriver som ignorerad under version 2 och meningsfull bara när XFA-modulen är inkompilerad, och fortsätter med sjutton funktionspekare, FFI_DisplayCaret till FFI_DoURIActionWithKeyboardModifier. Var och en av dem dokumenteras som krävd för XFA och annars att sätta till NULL. Den formuleringen är nyckeln till hela fixen. NULL är inte ett feltillstånd för de platserna; det är det dokumenterade tillståndet för en värd som inte driver XFA. En post som rensats med FillChar och sedan märkts som version 2 uppfyller kontraktet på ett icke-XFA-bygge precis lika bra som en version 1-post, och det är den enda post ett XFA-bygge tar emot

Diagram över posten FPDF_FORMFILLINFO i PDFium Component i Delphi: version 1 täcker återanropningarna FFI_Invalidate till FFI_DoGoToAction plus m_pJsPlatform, version 2 lägger till xfa_disabled och sjutton pekare från FFI_DisplayCaret-eran, FillChar rensar varje byte, och NULL-platser är det dokumenterade tillståndet för en värd som inte driver XFA
Pascal-posten är alltid den kompletta version 2-layouten, så ett XFA-aktiverat bygge accepterar den och ett vanligt bygge anropar helt enkelt aldrig de experimentella platser som förblir NULL

Det gamla valet band ABI:n till dokumentet

Felet var en enda villkorssats som såg rimlig ut i sin egen kontext. TPdf.InitializeFormFill beräknar en flagga RuntimeReady ur tre fakta: dokumentet rapporterar en XFA-formulärtyp via TPdf.XFA, XFA-stränghjälparna löses upp via XfaFeaturesAvailable, och V8-exporterna löses upp via V8FeaturesAvailable. Före v3.116.0 valde samma flagga också versionen

// v3.115.0 och tidigare: ABI-versionen följde dokumentet
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;

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

// ... och grenen för saknad runtime spikade den igen
else if XFA then
begin
  FFormFillInfo.Info.version := 1;
  FFormFillInfo.Info.xfa_disabled := 1;
  if Assigned(FOnXfaRuntimeMissing) then
    FOnXfaRuntimeMissing(Self);
end;

Läs den med headern i handen och felet är uppenbart. RuntimeReady är falsk för varje vanligt AcroForm-dokument, så varje vanligt dokument annonserade version 1. På pdfium.dll är det bra. På pdfium.v8.dll, som är det XFA-aktiverade bygget, kontrollerar PDFium fältet, finner det under det krävda 2:an och returnerar ett null-FPDF_FORMHANDLE, som CheckPdf förvandlar till undantaget ovan. Avsikten med den gamla koden var defensiv: behåll version 1 så att ett XFA-bygge aldrig läser de otilldelade version 2-platserna. Den försvarade sig mot ett problem som headern redan utesluter och skapade ett som headern uttryckligen varnar för. Den korrigerade koden bestämmer versionen en gång, på förhand, utifrån vad posten fysiskt är

procedure TPdf.InitializeFormFill;
var
  RuntimeReady: Boolean;
begin
  FXfaRuntimeUsable := False;
  FXfaPageCountOverride := -1;   // sentinel: använd det statiska sidträdet
  if not FormFill then
    Exit;

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

  // Den fullständiga version 2-posten allokeras och rensas ovan. PDFium
  // accepterar version 2 utan XFA och kräver den i varje XFA-aktiverat
  // bygge, även när dokumentet inte innehåller något XFA-formulär.
  FFormFillInfo.Info.version := 2;
  FFormFillInfo.Info.xfa_disabled := 1;

  // RuntimeReady grindar XFA-återanropningarna och xfa_disabled, aldrig version.
  RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
  ...

Var RuntimeReady fortfarande hör hemma: återanropningarna och xfa_disabled

RuntimeReady behåller sin uppgift som grind för XFA-beteendet; den rör bara inte längre postens layout. Version 1-återanropningarna, FFI_Invalidate, FFI_SetTimer, FFI_GetPage, FFI_DoURIAction, FFI_DoGoToAction och resten av det blocket, kopplas in ovillkorligt eftersom både AcroForm och XFA är beroende av dem. De sjutton version 2-pekarna tilldelas bara inuti RuntimeReady-grenen, tillsammans med xfa_disabled := 0. När dokumentet är XFA men runtimemiljön inte finns stannar posten på version 2 med xfa_disabled på 1 och version 2-platserna kvar som NULL, och wrappern utlöser OnXfaRuntimeMissing så att värden kan föreslå en omstart på pdfium.v8.dll. När miljön finns anropas FPDF_LoadXFA bara om RuntimeReady var sann, och bara en sann retur sätter FXfaRuntimeUsable, vilket är vad TPdf.XfaRuntimeAvailable rapporterar

  if RuntimeReady then
  begin
    FFormFillInfo.Info.xfa_disabled := 0;   // 0 = XFA aktiverat
    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 till FFI_DoURIActionWithKeyboardModifier
  end
  else if XFA then
  begin
    // Runtime saknas: behåll version 2, lämna XFA avstängt, säg till värden.
    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;

Två detaljer i det blocket är lätta att få fel när du skriver din egen bindning. FXfaPageCountOverride återställs till -1 som sentinel innan något annat händer, så PageCount faller tillbaka på det statiska sidträdet tills FFI_PageEvent rapporterar en ompaginering; en nolla där skulle tyst påstå ett tomt dokument. Och var och en av version 2-återanropningarna är en statisk cdecl-rutin som återvinner den ägande TPdf:n ur posten och sväljer varje Pascal-undantag innan den återvänder till PDFium, vilket är den disciplin som vår not om att härda PDFium-ABI:n i Delphi skriver ut för FFI_OpenFile. Inget i versionsändringen lägger på någon av reglerna

Är version 2 säkert när DLL:en saknar XFA-modul?

Ja, och skälet står i posten, inte i ett löfte från biblioteket. På ett icke-XFA-bygge säger headern att version 2 gör att de experimentella återanropningarna anropas också, så frågan är vad PDFium hittar när den tittar. TPdfFormFillInfo är en packad post vars Info-medlem är den fullständiga FPDF_FORMFILLINFO inklusive varje version 2-fält, och InitializeFormFill rensar hela saken med FillChar innan den rör en byte. Så på en vanlig pdfium.dll med ett vanligt dokument ser biblioteket version 2, xfa_disabled satt och NULL i varje experimentell plats, vilket är precis det tillstånd headern föreskriver för en värd som inte implementerar XFA. Det finns ingen trunkerad post för biblioteket att läsa förbi, för posten var aldrig kortare än version 2 till att börja med. Den gamla logiken försvarade sig mot en layoutmissmatchning som Pascal-deklarationen redan hade undanröjt

Gränsen som är värd att ärligt nämna är den posten inte kan täcka. Version 2 på ett vanligt dokument slår inte på JavaScript, XFA-skriptning eller någon av de värdhändelser som ligger bakom de återanropningarna. m_pJsPlatform kopplas bara in när V8FeaturesAvailable är sann, XFA förblir avstängt om inte RuntimeReady var sann, och TPdf.XFA fortsätter rapportera formulärtypen från FPDF_GetFormType oavsett vad miljön förhandlade fram. En värd som vill veta om dynamisk XFA faktiskt kommer att renderas bör fortsätta läsa XfaRuntimeAvailable efter att Active blir sann, som vår not om att upptäcka XFA-formulär och extrahera XFA-paket rekommenderar, i stället för att sluta sig till något ur versionsfältet

procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Utlöses från InitializeFormFill när dokumentet är XFA men den laddade
  // pdfium.dll inte kan köra motorn. Formulärmiljön öppnas ändå,
  // eftersom version 2 skickades oavsett; bara XFA-runtimen är avstängd.
  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;   // kastar inte längre på en vanlig PDF under pdfium.v8.dll
  if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
    ShowStaticXfaWarning;
end;

Protokollversion och funktionstillgång är två olika axlar

Den allmänna regeln som faller ut av fixen är att ett versionsfält i en återanropsstruktur svarar på frågan "hur stor är den här posten och vad får du läsa ur den", medan funktionsdetektering svarar på "vilka av de platserna kommer att göra något användbart". Den första är fixerad av den nativa binären och av den Pascal-deklaration du kompilerade mot. Den andra varierar per dokument, per DLL-exporttabell och per värdkonfiguration. Att slå ihop de två till en enda boolean är frestande eftersom XFA-fallet råkar behöva båda, men i samma stund ett bygge kräver en minimiversion går sammanslagningen sönder för varje dokument som inte behöver funktionen. XFA-formulär, beskrivna i ISO 32000-1 §12.7.8 som en XML-payload som lever vid sidan av AcroForm-ordboken, är funktionen här; postens layout är protokollet, och PDFium har rätt att kräva layouten innan den ens tittar på filen. Samma form dyker upp överallt där ett C-bibliotek versionerar sina strukturer: ett viewer-info-block, en render-options-post, en plattformsåteranropstabell. Det säkra mönstret är det korrigerade InitializeFormFill följer. Deklarera den nyaste layout du förstår, rensa den helt, sätt versionen så att den matchar layouten ovillkorligt och låt sedan kapacitetskontrollerna avgöra vilka platser som fylls i. Om en framtida PDFium-header lägger till version 3 består ändringen i deklarationen och i den enda tilldelningen, inte i en dokumentberoende gren som kommer att vara fel för den kombination ingen testade

Diagram som skiljer de två axlarna bakom FPDF_FORMFILLINFO i PDFium Component: protokollversionen som fixeras av postens layout och den nativa binären, och funktionstillgången där RuntimeReady grindar xfa_disabled, sjutton version 2-platser, FPDF_LoadXFA och m_pJsPlatform per dokument och per värd
Ett versionsfält beskriver minnet motparten får läsa, kapacitetskontroller avgör vilka platser som gör något användbart, och att slå ihop de två till en boolean bryter det bygge som kräver ett minimum

Den korrigerade formfill-initieringen levereras i PDFium Component för Delphi, Lazarus och C++Builder, och den gäller på Win32 och Win64 likadant eftersom båda byggena delar samma postdeklaration. Om ditt program redan väljer pdfium.v8.dll för JavaScript-drivna AcroFormulär är det här ändringen som låter det öppna resten av ditt PDF-arkiv genom samma binär utan att specialbehandla formulärmiljön