Teknisk artikel

FPDF_FORMFILLINFO version 2 i Delphi: Følg DLL-ABI'en

PDFium Component sætter nu FPDF_FORMFILLINFO.version til 2 for hvert form-fill-miljø, den initialiserer, for den version, en native PDFium-build accepterer, er en egenskab ved den build, ikke ved det dokument, der åbnes. En XFA-aktiveret pdfium.v8.dll afviser version 1 blankt, så en almindelig AcroForm-PDF åbnet gennem den plejede at fejle i FPDFDOC_InitFormFillEnvironment uden nogen XFA i sigte. V3.116.0-fixet er lille, men fejlen bag det er en generel en og det værd at navngive: Et protokolversionsfelt beskriver det hukommelseslayout, modparten forventer, og det må aldrig udledes af, om du tilfældigvis behøver de features, layoutet bærer

Hvorfor fejler FPDFDOC_InitFormFillEnvironment på en almindelig PDF med pdfium.v8.dll?

Miljøet fejler, fordi en XFA-aktiveret PDFium-build validerer version-feltet, før den gør noget som helst andet, og den gamle wrapper-logik gav den en 1, når det aktuelle dokument ikke var en XFA-form. Symptomet i en Delphi-vært er en EPdfError raiset fra TPdf.InitializeFormFill med beskeden Cannot initialize form fill environment, kastet under åbning af en almindelig faktura- eller skatteformular, der ikke har andet end AcroForm-tekstfelter. Samme fil åbner fint mod den almindelige pdfium.dll. Samme DLL åbner et rigtigt XFA-dokument fint. Kun kombinationen af V8-builden og et ikke-XFA-dokument knækker, hvilket er præcis den kombination, en vært lander i, efter den har slået EnableV8Engine til for at få AcroForm-JavaScript, eller efter autovalget i LoadDocument allerede har forpligtet processen til pdfium.v8.dll for en tidligere XFA-fil. Den forpligtelse er proces-dækkende: EnableV8Engine læses før det første LoadLibrary, og når XFA-builden først er indlæst, går hver senere almindelig PDF gennem samme miljøopsætning mod samme binære fil. Værten gjorde intet forkert; wrapperen stillede det forkerte spørgsmål, da den udfyldte recorden. Hvis du stadig er ved at beslutte, hvilken binær fil du overhovedet skal shippe, dækker vores note om at deployere PDFium-DLL'en og diagnosticere load-fejl valget mellem plain og V8, og denne artikel antager, at V8-builden allerede er i processen

PDFium Component-diagram over de fire kombinationer af almindelig pdfium.dll og XFA-aktiveret pdfium.v8.dll mod AcroForm- og XFA-dokumenter: En version 1-record knækkede kun V8-builden med en almindelig form, EPdfError i FPDFDOC_InitFormFillEnvironment, mens den korrigerede version 2-record åbner alle fire
Én betingelse bandt ABI-versionen til dokumentet, så det proces-dækkende valg af V8-binærfilen gjorde hver senere almindelig PDF til en mislykket miljøinitialisering

Hvad lover version-feltet i FPDF_FORMFILLINFO egentlig?

FPDF_FORMFILLINFO.version fortæller PDFium, hvilke felter af recorden den må læse, og den offentlige header fpdf_formfill.h binder de acceptable værdier til, hvordan biblioteket er kompileret, frem for til dokumentet. I parafrase har kontrakten tre dele. Version 1 dækker de stabile callbacks fra FFI_Invalidate til FFI_DoGoToAction plus m_pJsPlatform-pointeren. En build uden XFA-modulet accepterer enten 1 eller 2, og med 2 kalder den også de yderligere eksperimentelle callbacks. En build med XFA-modulet kræver 2, punktum, og headeren gentager det krav to gange, som om den ventede, at folk ville overse det. Intet sted nævner kontrakten dokumentet. Versionen er en udtalelse om recorden, du allokerede: Med en 2 lover du, at hukommelsen efter m_pJsPlatform findes og holder enten gyldige funktionspointere eller NULL

Version 2-regionen er der, hele XFA-maskineriet bor. Den starter med xfa_disabled, en FPDF_BOOL, headeren beskriver som ignoreret under version 2 og kun meningsfuld, når XFA-modulet er kompileret ind, og fortsætter med sytten funktionspointere, FFI_DisplayCaret til FFI_DoURIActionWithKeyboardModifier. Hver af dem er dokumenteret som påkrævet for XFA og ellers skal sættes til NULL. Den formulering er nøglen til hele fixet. NULL er ikke en fejltilstand for de slots; det er den dokumenterede tilstand for en vært, der ikke driver XFA. En record, der er ryddet med FillChar og derefter markeret som version 2, opfylder kontrakten på en ikke-XFA-build præcis lige så godt som en version-1-record gør, og den er den eneste record, en XFA-build tager imod

PDFium Component-diagram over FPDF_FORMFILLINFO-recorden i Delphi: Version 1 dækker FFI_Invalidate til FFI_DoGoToAction-callbacks plus m_pJsPlatform, version 2 tilføjer xfa_disabled og sytten FFI_DisplayCaret-æra-pointere, FillChar rydder hver byte, og NULL-slots er den dokumenterede tilstand for en vært, der ikke driver XFA
Pascal-recorden er altid det komplette version 2-layout, så en XFA-aktiveret build accepterer den, og en almindelig build kalder simpelthen aldrig de eksperimentelle slots, der forbliver NULL

Det gamle valg bandt ABI'en til dokumentet

Defekten var én enkelt betingelse, der så fornuftig ud i isolation. TPdf.InitializeFormFill beregner et RuntimeReady-flag ud fra tre fakta: Dokumentet rapporterer en XFA-formtype gennem TPdf.XFA, XFA-strenghelperne er opløst gennem XfaFeaturesAvailable, og V8-eksporterne er opløst gennem V8FeaturesAvailable. Før v3.116.0 valgte samme flag også versionen

// v3.115.0 og tidligere: ABI-versionen fulgte dokumentet
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;

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

// ... og runtime-missing-grenen fastgjorde 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 headeren i hånden, og fejlen er åbenlys. RuntimeReady er False for hvert almindeligt AcroForm-dokument, så hvert almindeligt dokument annoncerede version 1. På pdfium.dll er det fint. På pdfium.v8.dll, som er XFA-builden, tjekker PDFium feltet, finder det under de krævede 2 og returnerer en null-FPDF_FORMHANDLE, som CheckPdf forvandler til exceptionen ovenfor. Den gamle kodes hensigt var defensiv: Behold version 1, så en XFA-build aldrig læser de ikke-tildelte version-2-slots. Den forsvarede sig mod et problem, headeren allerede udelukker, og skabte et, headeren eksplicit advarer om. Den korrigerede kode afgør versionen én gang, på forhånd, ud fra hvad recorden fysisk er

procedure TPdf.InitializeFormFill;
var
  RuntimeReady: Boolean;
begin
  FXfaRuntimeUsable := False;
  FXfaPageCountOverride := -1;   // sentinel: brug det statiske sidetræ
  if not FormFill then
    Exit;

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

  // Den komplette version 2-record allokeres og ryddes ovenfor. PDFium
  // accepterer version 2 uden XFA og kræver den i hver XFA-aktiveret
  // build, også når dette dokument ikke indeholder nogen XFA-form.
  FFormFillInfo.Info.version := 2;
  FFormFillInfo.Info.xfa_disabled := 1;

  // RuntimeReady gater XFA-callbacks og xfa_disabled, aldrig version.
  RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
  ...

Hvor RuntimeReady stadig hører hjemme: callbacks og xfa_disabled

RuntimeReady beholder sit arbejde som porten for XFA-adfærd; den rører simpelthen ikke længere record-layoutet. Version 1-callbacks — FFI_Invalidate, FFI_SetTimer, FFI_GetPage, FFI_DoURIAction, FFI_DoGoToAction og resten af den blok — kobles ubetinget, fordi AcroForm og XFA begge afhænger af dem. De sytten version 2-pointere tildeles kun inde i RuntimeReady-grenen, sammen med xfa_disabled := 0. Når dokumentet er XFA, men runtimen ikke er der, forbliver recorden på version 2 med xfa_disabled på 1 og version 2-slots efterladt NULL, og wrapperen raiser OnXfaRuntimeMissing, så værten kan foreslå genstart på pdfium.v8.dll. Når miljøet findes, kaldes FPDF_LoadXFA kun, når RuntimeReady var True, og kun en True-returværdi sætter FXfaRuntimeUsable, hvilket er det, TPdf.XfaRuntimeAvailable rapporterer

  if RuntimeReady then
  begin
    FFormFillInfo.Info.xfa_disabled := 0;   // 0 = XFA slået til
    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 til FFI_DoURIActionWithKeyboardModifier
  end
  else if XFA then
  begin
    // Runtime utilgængelig: Behold version 2, lad XFA være slået fra, sig det til værten.
    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;

To detaljer i den blok er lette at ramme forkert, når du skriver din egen binding. FXfaPageCountOverride nulstilles til -1 som sentinel, før noget som helst sker, så PageCount falder tilbage til det statiske sidetræ, indtil FFI_PageEvent rapporterer en repaginering; et nul dér ville lydløst hævde et tomt dokument. Og hver af version 2-callbacks er en statisk cdecl-rutine, der genfinder den ejende TPdf fra recorden og opsluger enhver Pascal-exception, før den returnerer til PDFium, hvilket er disciplinen, vores note om at hærde PDFium-ABI'en i Delphi staver ud for FFI_OpenFile. Intet ved versionsændringen afslapper nogen af reglerne

Er version 2 sikker, når DLL'en ikke har noget XFA-modul?

Ja, og grunden ligger i recorden, ikke i et løfte fra biblioteket. På en ikke-XFA-build siger headeren, at version 2 også får de eksperimentelle callbacks kaldt, så spørgsmålet er, hvad PDFium finder, når den kigger. TPdfFormFillInfo er en packed record, hvis Info-medlem er den komplette FPDF_FORMFILLINFO inklusive hvert version 2-felt, og InitializeFormFill rydder det hele med FillChar, før den rører en eneste byte. Så på en almindelig pdfium.dll med et almindeligt dokument ser biblioteket version 2, xfa_disabled sat og NULL i hver eksperimentel slot, hvilket er præcis den tilstand, headeren foreskriver for en vært, der ikke implementerer XFA. Der er ingen trunkeret record for biblioteket at læse forbi, fordi recorden aldrig var kortere end version 2. Den gamle logik forsvarede et layout-mismatch, som Pascal-deklarationen allerede havde elimineret

Grænsen, der er det værd at sige ærligt, er den, recorden ikke kan dække. Version 2 på et almindeligt dokument tænder ikke for JavaScript, XFA-scripting eller nogen af værts-events bag de callbacks. m_pJsPlatform kobles kun på, når V8FeaturesAvailable er True, XFA forbliver slået fra, medmindre RuntimeReady var True, og TPdf.XFA fortsætter med at rapportere formtypen fra FPDF_GetFormType uanset hvad miljøet forhandlede. En vært, der vil vide, om dynamisk XFA faktisk renderer, bør blive ved med at læse XfaRuntimeAvailable, efter Active bliver True, som vores note om at detektere XFA-formularer og udtrække XFA-pakker anbefaler, frem for at udlede noget af versionsfeltet

procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Udløses fra InitializeFormFill, når dokumentet er XFA, men den indlæste
  // pdfium.dll ikke kan køre enginen. Form-miljøet åbner stadig,
  // for version 2 blev sendt alligevel; kun XFA-runtimen er slået fra.
  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;   // kaster ikke længere på en almindelig PDF under pdfium.v8.dll
  if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
    ShowStaticXfaWarning;
end;

Protokolversion og feature-tilgængelighed er to forskellige akser

Den generelle regel, der falder ud af dette fix, er, at et versionsfelt i en callback-struktur besvarer spørgsmålet "hvor stor er denne record, og hvad må du læse fra den", mens feature-detektering besvarer "hvilke af de slots vil gøre noget nyttigt". Det første er fastlagt af den native binærfil og af den Pascal-deklaration, du kompilerede imod. Det andet varierer pr. dokument, pr. DLL-eksporttabel og pr. værtskonfiguration. At folde de to sammen til én boolean er fristende, fordi XFA-casen tilfældigvis behøver begge, men i det øjeblik en build håndhæver en minimumsversion knækker sammenfoldningen for hvert dokument, der ikke behøver featuren. XFA-formularer, beskrevet i ISO 32000-1 §12.7.8 som en XML-payload, der lever ved siden af AcroForm-ordbogen, er featuren her; record-layoutet er protokollen, og PDFium har ret til at insistere på layoutet, før den nogensinde ser på filen. Samme form viser sig alle steder, hvor et C-bibliotek versionerer sine strukturer: En viewer-info-blok, en render-options-record, en platform-callback-tabel. Det sikre mønster er det, den korrigerede InitializeFormFill følger. Deklarér det nyeste layout, du forstår, rydd den fuldstændigt, sæt versionen til at matche det layout ubetinget, og lad så capability-tjek afgøre, hvilke slots der skal udfyldes. Tilføjer en fremtidig PDFium-header en version 3, er ændringen i deklarationen og i den ene tildeling, ikke i en dokumentafhængig gren, der bliver forkert for den kombination, ingen testede

PDFium Component-diagram, der adskiller de to akser bag FPDF_FORMFILLINFO: Protokolversionen fastlagt af record-layoutet og den native binærfil, og feature-tilgængelighed, hvor RuntimeReady gater xfa_disabled, sytten version 2-slots, FPDF_LoadXFA og m_pJsPlatform pr. dokument og pr. vært
Et versionsfelt beskriver den hukommelse, modparten må læse, capability-tjek afgør, hvilke slots der gør noget nyttigt, og at folde de to sammen til én boolean knækker den build, der håndhæver et minimum

Den korrigerede form-fill-initialisering ships i PDFium Component til Delphi, Lazarus og C++Builder, og den gælder på Win32 og Win64 lige, da begge builds deler samme record-deklaration. Vælger din applikation allerede pdfium.v8.dll til JavaScript-drevne AcroForms, er dette den ændring, der lader den åbne resten af dit PDF-arkiv gennem samme binærfil uden special-casing af form-miljøet