Teknisk artikkel

FPDF_FORMFILLINFO versjon 2 i Delphi: følg DLL-ABI-en

PDFium Component setter nå FPDF_FORMFILLINFO.version til 2 for hvert skjemautfyllingsmiljø det initialiserer, fordi versjonen en innebygd PDFium-bygg godtar, er en egenskap ved det bygget og ikke ved dokumentet som åpnes. En XFA-aktivert pdfium.v8.dll avviser versjon 1 tvert, så en vanlig AcroForm-PDF åpnet gjennom den feilet tidligere i FPDFDOC_InitFormFillEnvironment uten at XFA var å se noe sted. Rettelsen i v3.116.0 er liten, men feilen bak den er generell og verdt å navngi: et protokollversjonsfelt beskriver minnelayouten den andre siden forventer, og det må aldri utledes fra om du tilfeldigvis trenger funksjonene den layouten bærer

Hvorfor feiler FPDFDOC_InitFormFillEnvironment på en vanlig PDF med pdfium.v8.dll?

Miljøet feiler fordi en XFA-aktivert PDFium-bygg validerer version-feltet før den gjør noe annet, og den gamle innpakningslogikken ga den en 1 hver gang gjeldende dokument ikke var et XFA-skjema. Symptomet i en Delphi-vert er en EPdfError kastet fra TPdf.InitializeFormFill med meldingen Cannot initialize form fill environment, kastet mens en helt vanlig faktura eller skatteblankett med bare AcroForm-tekstfelt åpnes. Den samme filen åpner fint mot den vanlige pdfium.dll. Den samme DLL-en åpner et ekte XFA-dokument fint. Bare kombinasjonen av V8-bygget og et ikke-XFA-dokument feiler, og det er nøyaktig kombinasjonen en vert havner i etter å ha slått på EnableV8Engine for å få AcroForm-JavaScript, eller etter at autovalget i LoadDocument allerede har bundet prosessen til pdfium.v8.dll for en tidligere XFA-fil. Den bindingen gjelder hele prosessen: EnableV8Engine leses før den første LoadLibrary, og når XFA-bygget først er lastet, går hver senere vanlig PDF gjennom det samme miljøoppsettet mot det samme binæret. Verten gjorde ingenting galt; innpakningen stilte feil spørsmål da den fylte ut recorden. Hvis du fortsatt vurderer hvilket binær du i det hele tatt skal levere, dekker notatet vårt om å distribuere PDFium-DLL-en og diagnostisere lastingsfeil valget mellom vanlig og V8, og denne artikkelen forutsetter at V8-bygget allerede er i prosessen

Diagram fra PDFium Component over de fire kombinasjonene av vanlig pdfium.dll og XFA-aktivert pdfium.v8.dll mot AcroForm- og XFA-dokumenter: en versjon 1-record ødela bare V8-bygget med et vanlig skjema, med EPdfError i FPDFDOC_InitFormFillEnvironment, mens den rettede versjon 2-recorden åpner alle fire
Én betingelse bandt ABI-versjonen til dokumentet, så det prosessomfattende valget av V8-binæret gjorde hver senere vanlig PDF til en mislykket miljøinitialisering

Hva lover egentlig versjonsfeltet i FPDF_FORMFILLINFO?

FPDF_FORMFILLINFO.version forteller PDFium hvilke felt i recorden den har lov til å lese, og den offentlige headeren fpdf_formfill.h knytter de akseptable verdiene til hvordan biblioteket ble kompilert og ikke til dokumentet. Omskrevet har kontrakten tre deler. Versjon 1 dekker de stabile tilbakekallene fra FFI_Invalidate til og med FFI_DoGoToAction, pluss pekeren m_pJsPlatform. Et bygg uten XFA-modulen godtar enten 1 eller 2, og med 2 vil den også kalle de ekstra eksperimentelle tilbakekallene. Et bygg med XFA-modulen krever 2, punktum, og headeren gjentar det kravet to ganger som om den regnet med at folk ville overse det. Ingen steder nevner kontrakten dokumentet. Versjonen er en uttalelse om recorden du allokerte: med en 2 lover du at minnet etter m_pJsPlatform finnes og holder enten gyldige funksjonspekere eller NULL

Versjon 2-området er der hele XFA-maskineriet bor. Det begynner med xfa_disabled, en FPDF_BOOL som headeren beskriver som ignorert under versjon 2 og bare meningsfull når XFA-modulen er kompilert inn, og fortsetter med sytten funksjonspekere, fra FFI_DisplayCaret til FFI_DoURIActionWithKeyboardModifier. Hver av dem er dokumentert som påkrevd for XFA og ellers å skulle settes til NULL. Den ordlyden er nøkkelen til hele rettelsen. NULL er ikke en feiltilstand for de lukene; det er den dokumenterte tilstanden for en vert som ikke driver XFA. En record som er tømt med FillChar og deretter merket som versjon 2, oppfyller kontrakten på et ikke-XFA-bygg nøyaktig like godt som en versjon 1-record, og det er den eneste recorden et XFA-bygg vil ta imot

Diagram fra PDFium Component over FPDF_FORMFILLINFO-recorden i Delphi: versjon 1 dekker tilbakekallene fra FFI_Invalidate til FFI_DoGoToAction pluss m_pJsPlatform, versjon 2 legger til xfa_disabled og sytten pekere fra FFI_DisplayCaret-æraen, FillChar tømmer hver byte, og NULL-luker er den dokumenterte tilstanden for en vert som ikke driver XFA
Pascal-recorden er alltid den komplette versjon 2-layouten, så et XFA-aktivert bygg godtar den, og et vanlig bygg kaller rett og slett aldri de eksperimentelle lukene som forblir NULL

Det gamle valget bandt ABI-en til dokumentet

Feilen var én enkelt betingelse som så rimelig ut isolert. TPdf.InitializeFormFill regner ut flagget RuntimeReady fra tre fakta: dokumentet rapporterer en XFA-skjematype gjennom TPdf.XFA, XFA-streng hjelperne løses opp gjennom XfaFeaturesAvailable, og V8-eksportene løses opp gjennom V8FeaturesAvailable. Før v3.116.0 valgte det samme flagget også versjonen

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

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

// ... og grenen for manglende runtime låste den igjen
else if XFA then
begin
  FFormFillInfo.Info.version := 1;
  FFormFillInfo.Info.xfa_disabled := 1;
  if Assigned(FOnXfaRuntimeMissing) then
    FOnXfaRuntimeMissing(Self);
end;

Les det med headeren i hånden, og feilen er åpenbar. RuntimeReady er false for hvert vanlig AcroForm-dokument, så hvert vanlig dokument meldte versjon 1. På pdfium.dll er det greit. På pdfium.v8.dll, som er det XFA-aktiverte bygget, sjekker PDFium feltet, finner det under de påkrevde 2, og returnerer et null FPDF_FORMHANDLE, som CheckPdf gjør om til unntaket over. Intensjonen med den gamle koden var defensiv: behold versjon 1 så et XFA-bygg aldri leser de utildelte versjon 2-lukene. Den forsvarte seg mot et problem headeren allerede utelukker, og skapte ett headeren eksplisitt advarer mot. Den rettede koden bestemmer versjonen én gang, på forhånd, ut fra hva recorden fysisk er

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

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

  // Den fulle versjon 2-recorden er allokert og tømt over. PDFium
  // godtar versjon 2 uten XFA og krever det i hvert XFA-aktiverte
  // bygg, også når dette dokumentet ikke inneholder noe XFA-skjema.
  FFormFillInfo.Info.version := 2;
  FFormFillInfo.Info.xfa_disabled := 1;

  // RuntimeReady gater XFA-tilbakekallene og xfa_disabled, aldri versjonen.
  RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
  ...

Hvor RuntimeReady fortsatt hører hjemme: tilbakekallene og xfa_disabled

RuntimeReady beholder jobben sin som portvakt for XFA-oppførsel; den rører bare ikke lenger recordlayouten. Versjon 1-tilbakekallene, FFI_Invalidate, FFI_SetTimer, FFI_GetPage, FFI_DoURIAction, FFI_DoGoToAction og resten av den blokken, kobles ubetinget fordi både AcroForm og XFA er avhengige av dem. De sytten versjon 2-pekerne tilordnes bare inne i RuntimeReady-grenen, sammen med xfa_disabled := 0. Når dokumentet er XFA men runtime-en ikke finnes, forblir recorden på versjon 2 med xfa_disabled satt til 1 og versjon 2-lukene stående som NULL, og innpakningen reiser OnXfaRuntimeMissing slik at verten kan foreslå å starte på nytt med pdfium.v8.dll. Etter at miljøet finnes, kalles FPDF_LoadXFA bare når RuntimeReady var true, og bare en sann returverdi setter FXfaRuntimeUsable, som er det TPdf.XfaRuntimeAvailable rapporterer

  if RuntimeReady then
  begin
    FFormFillInfo.Info.xfa_disabled := 0;   // 0 = XFA aktivert
    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 utilgjengelig: behold versjon 2, la XFA være avslått, si fra til verten.
    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 blokken er lette å få galt når du skriver din egen binding. FXfaPageCountOverride nullstilles til -1 som en sentinel før noe annet skjer, så PageCount faller tilbake på det statiske sidetreet helt til FFI_PageEvent rapporterer en ny paginering; en null der ville stille påstå et tomt dokument. Og hvert av versjon 2-tilbakekallene er en statisk cdecl-rutine som gjenfinner den eiede TPdf fra recorden og svelger ethvert Pascal-unntak før den returnerer til PDFium, og det er disiplinen notatet vårt om å herde PDFium-ABI-en i Delphi skriver ut for FFI_OpenFile. Ingenting ved versjonsendringen lemper på noen av de to reglene

Er versjon 2 trygt når DLL-en ikke har noen XFA-modul?

Ja, og grunnen ligger i recorden, ikke i et løfte fra biblioteket. På et ikke-XFA-bygg sier headeren at versjon 2 også fører til at de eksperimentelle tilbakekallene blir kalt, så spørsmålet er hva PDFium finner når den ser etter. TPdfFormFillInfo er en pakket record der Info-medlemmet er den komplette FPDF_FORMFILLINFO med hvert versjon 2-felt inkludert, og InitializeFormFill tømmer hele greia med FillChar før den rører en byte. Så på en vanlig pdfium.dll med et vanlig dokument ser biblioteket versjon 2, xfa_disabled satt og NULL i hver eksperimentell luke, som er nøyaktig den tilstanden headeren foreskriver for en vert som ikke implementerer XFA. Det finnes ingen avkuttet record biblioteket kan lese forbi, fordi recorden aldri var kortere enn versjon 2 i utgangspunktet. Den gamle logikken forsvarte seg mot et layoutavvik som Pascal-deklarasjonen allerede hadde eliminert

Grensen som er verdt å si ærlig, er den recorden ikke kan dekke. Versjon 2 på et vanlig dokument slår ikke på JavaScript, XFA-scripting eller noen av vertshendelsene bak de tilbakekallene. m_pJsPlatform festes bare når V8FeaturesAvailable er true, XFA forblir avslått med mindre RuntimeReady var true, og TPdf.XFA fortsetter å rapportere skjematypen fra FPDF_GetFormType uansett hva miljøet forhandlet fram. En vert som vil vite om dynamisk XFA faktisk vil rendres, bør fortsette å lese XfaRuntimeAvailable etter at Active blir true, slik notatet vårt om å oppdage XFA-skjemaer og hente ut XFA-pakker anbefaler, i stedet for å slutte noe fra versjonsfeltet

procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Utløses fra InitializeFormFill når dokumentet er XFA men den innlastede
  // pdfium.dll ikke kan kjøre motoren. Skjemamiljøet åpnes likevel,
  // fordi versjon 2 ble sendt uansett; bare XFA-runtime-en er av.
  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 lenger på en vanlig PDF under pdfium.v8.dll
  if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
    ShowStaticXfaWarning;
end;

Protokollversjon og funksjonstilgjengelighet er to forskjellige akser

Den generelle regelen som faller ut av denne rettelsen, er at et versjonsfelt i en tilbakekallsstruktur svarer på spørsmålet «hvor stor er denne recorden, og hva får du lese fra den», mens funksjonsdeteksjon svarer på «hvilke av de lukene vil gjøre noe nyttig». Den første er fastsatt av det innebygde binæret og av Pascal-deklarasjonen du kompilerte mot. Den andre varierer per dokument, per DLL-eksporttabell og per vertskonfigurasjon. Å slå de to sammen til én boolsk verdi er fristende fordi XFA-tilfellet tilfeldigvis trenger begge, men i det øyeblikket et bygg håndhever en minsteversjon, bryter sammenstillingen for hvert dokument som ikke trenger funksjonen. XFA-skjemaer, beskrevet i ISO 32000-1 §12.7.8 som en XML-nyttelast som lever ved siden av AcroForm-ordboken, er funksjonen her; recordlayouten er protokollen, og PDFium har rett til å insistere på layouten før den i det hele tatt ser på filen. Den samme formen dukker opp overalt der et C-bibliotek versjonerer strukturene sine: en viewer-info-blokk, en record for render-alternativer, en plattformtabell for tilbakekall. Det trygge mønsteret er det den rettede InitializeFormFill følger. Deklarer den nyeste layouten du forstår, tøm den fullstendig, sett versjonen til å matche den layouten ubetinget, og la deretter kapabilitetssjekker avgjøre hvilke luker som fylles. Hvis en framtidig PDFium-header legger til en versjon 3, er endringen i deklarasjonen og i den ene tilordningen, ikke i en dokumentavhengig gren som vil være gal for den kombinasjonen ingen testet

Diagram fra PDFium Component som skiller de to aksene bak FPDF_FORMFILLINFO: protokollversjonen, fastsatt av recordlayouten og det innebygde binæret, og funksjonstilgjengeligheten, der RuntimeReady gater xfa_disabled, sytten versjon 2-luker, FPDF_LoadXFA og m_pJsPlatform per dokument og per vert
Et versjonsfelt beskriver minnet den andre siden får lese, kapabilitetssjekker avgjør hvilke luker som gjør noe nyttig, og å slå de to sammen til én boolsk verdi bryter bygget som håndhever et minimum

Den rettede initialiseringen av skjemautfylling leveres i PDFium Component for Delphi, Lazarus og C++Builder, og den gjelder på Win32 og Win64 likt, siden begge byggene deler den samme recorddeklarasjonen. Hvis applikasjonen din allerede velger pdfium.v8.dll for JavaScript-drevne AcroForm-skjemaer, er dette endringen som lar den åpne resten av PDF-arkivet ditt gjennom det samme binæret uten å spesialbehandle skjemamiljøet