Ora PDFium Component imposta FPDF_FORMFILLINFO.version a 2 per ogni ambiente form-fill che inizializza, perché la versione che una build nativa di PDFium accetta è una proprietà di quella build, non del documento che si apre. Una pdfium.v8.dll con XFA abilitato rifiuta la versione 1 di netto, quindi un semplice PDF AcroForm aperto attraverso di essa falliva in FPDFDOC_InitFormFillEnvironment senza alcun XFA in vista. La correzione della v3.116.0 è piccola, ma l'errore che ci sta dietro è generale e vale la pena nominarlo: un campo di versione di protocollo descrive il layout di memoria che l'altra parte si aspetta, e non va mai derivato dal fatto che ti servano o meno le funzionalità che quel layout porta con sé
Perché FPDFDOC_InitFormFillEnvironment fallisce su un PDF normale con pdfium.v8.dll?
L'ambiente fallisce perché una build PDFium con XFA abilitato valida il campo version prima di fare qualsiasi altra cosa, e la vecchia logica del wrapper gli passava un 1 ogni volta che il documento corrente non era un form XFA. Il sintomo in un host Delphi è un EPdfError sollevato da TPdf.InitializeFormFill con il messaggio Cannot initialize form fill environment, lanciato mentre si apre una normale fattura o un modulo fiscale che non ha altro che campi di testo AcroForm. Lo stesso file si apre senza problemi con la pdfium.dll normale. La stessa DLL apre senza problemi un vero documento XFA. Solo la combinazione della build V8 con un documento non XFA si rompe, che è esattamente la combinazione in cui finisce un host dopo aver attivato EnableV8Engine per avere il JavaScript di AcroForm, o dopo che la selezione automatica in LoadDocument ha già impegnato il processo su pdfium.v8.dll per un file XFA precedente. Quell'impegno vale per l'intero processo: EnableV8Engine viene letto prima della prima LoadLibrary, e una volta caricata la build XFA ogni PDF normale successivo passa dallo stesso setup dell'ambiente contro lo stesso binario. L'host non ha fatto nulla di male; è il wrapper che ha posto la domanda sbagliata quando ha riempito il record. Se stai ancora decidendo quale binario distribuire, la nota sul deployment della DLL di PDFium e la diagnosi dei fallimenti di caricamento copre la scelta tra normale e V8, e questo articolo dà per scontato che la build V8 sia già nel processo
Che cosa promette davvero il campo version in FPDF_FORMFILLINFO?
FPDF_FORMFILLINFO.version dice a PDFium quali campi del record può leggere, e l'header pubblico fpdf_formfill.h lega i valori accettabili a come è stata compilata la libreria, non al documento. In parafrasi, il contratto ha tre parti. La versione 1 copre le callback stabili da FFI_Invalidate fino a FFI_DoGoToAction più il puntatore m_pJsPlatform. Una build senza il modulo XFA accetta sia 1 sia 2, e con 2 chiamerà anche le callback sperimentali aggiuntive. Una build con il modulo XFA richiede 2, punto, e l'header ripete quel requisito due volte come se si aspettasse che la gente lo saltasse. In nessun punto il contratto menziona il documento. La versione è un'affermazione sul record che hai allocato: con un 2 stai promettendo che la memoria dopo m_pJsPlatform esiste e contiene puntatori a funzione validi oppure NULL
La regione della versione 2 è dove vive tutta la macchina XFA. Inizia con xfa_disabled, un FPDF_BOOL che l'header descrive come ignorato sotto la versione 2 e significativo solo quando il modulo XFA è compilato dentro, e prosegue con diciassette puntatori a funzione, da FFI_DisplayCaret fino a FFI_DoURIActionWithKeyboardModifier. Ognuno di essi è documentato come obbligatorio per XFA e altrimenti da impostare a NULL. È quella formulazione la chiave dell'intera correzione. NULL non è uno stato di errore per quegli slot; è lo stato documentato per un host che non sta pilotando XFA. Un record azzerato con FillChar e poi marcato come versione 2 soddisfa il contratto su una build non XFA esattamente come lo soddisfa un record versione 1, ed è l'unico record che una build XFA accetterà
La vecchia selezione legava l'ABI al documento
Il difetto era un unico condizionale che, preso da solo, sembrava ragionevole. TPdf.InitializeFormFill calcola un flag RuntimeReady da tre fatti: il documento dichiara un tipo di form XFA tramite TPdf.XFA, gli helper di stringa XFA si sono risolti tramite XfaFeaturesAvailable, e gli export V8 si sono risolti tramite V8FeaturesAvailable. Prima della v3.116.0 lo stesso flag sceglieva anche la versione
// v3.115.0 e precedenti: la versione dell'ABI seguiva il documento
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
if RuntimeReady then
FFormFillInfo.Info.version := 2
else
FFormFillInfo.Info.version := 1;
// ... e il ramo con runtime mancante la fissava di nuovo
else if XFA then
begin
FFormFillInfo.Info.version := 1;
FFormFillInfo.Info.xfa_disabled := 1;
if Assigned(FOnXfaRuntimeMissing) then
FOnXfaRuntimeMissing(Self);
end;
Leggilo con l'header in mano e il fallimento è ovvio. RuntimeReady è false per ogni documento AcroForm normale, quindi ogni documento normale annunciava la versione 1. Su pdfium.dll va bene. Su pdfium.v8.dll, che è la build con XFA abilitato, PDFium controlla il campo, lo trova sotto il 2 richiesto e restituisce un FPDF_FORMHANDLE nullo, che CheckPdf trasforma nell'eccezione di cui sopra. L'intento del vecchio codice era difensivo: tenere la versione 1 perché una build XFA non leggesse mai gli slot della versione 2 non assegnati. Difendeva da un problema che l'header esclude già e ne creava uno che l'header avverte esplicitamente. Il codice corretto decide la versione una volta sola, in testa, da ciò che il record è fisicamente
procedure TPdf.InitializeFormFill;
var
RuntimeReady: Boolean;
begin
FXfaRuntimeUsable := False;
FXfaPageCountOverride := -1; // sentinella: usa l'albero delle pagine statico
if not FormFill then
Exit;
FillChar(FFormFillInfo, SizeOf(FFormFillInfo), 0);
FFormFillInfo.Pdf := Self;
// Il record completo di versione 2 è allocato e azzerato sopra. PDFium
// accetta la versione 2 senza XFA e la richiede in ogni build con XFA
// abilitato, anche quando questo documento non contiene alcun form XFA.
FFormFillInfo.Info.version := 2;
FFormFillInfo.Info.xfa_disabled := 1;
// RuntimeReady fa da gate per le callback XFA e xfa_disabled, mai per la versione.
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
...
Dove RuntimeReady serve ancora: le callback e xfa_disabled
RuntimeReady mantiene il suo ruolo di gate per il comportamento XFA; semplicemente non tocca più il layout del record. Le callback di versione 1, FFI_Invalidate, FFI_SetTimer, FFI_GetPage, FFI_DoURIAction, FFI_DoGoToAction e il resto di quel blocco, vengono cablate incondizionatamente perché sia AcroForm sia XFA dipendono da loro. I diciassette puntatori di versione 2 vengono assegnati solo dentro il ramo RuntimeReady, insieme a xfa_disabled := 0. Quando il documento è XFA ma il runtime non c'è, il record resta alla versione 2 con xfa_disabled a 1 e gli slot di versione 2 lasciati NULL, e il wrapper solleva OnXfaRuntimeMissing così l'host può suggerire di riavviare su pdfium.v8.dll. Dopo che l'ambiente esiste, FPDF_LoadXFA viene chiamata solo se RuntimeReady era true, e solo un ritorno true imposta FXfaRuntimeUsable, che è ciò che riporta TPdf.XfaRuntimeAvailable
if RuntimeReady then
begin
FFormFillInfo.Info.xfa_disabled := 0; // 0 = XFA abilitato
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;
// ... da FFI_UploadTo fino a FFI_DoURIActionWithKeyboardModifier
end
else if XFA then
begin
// Runtime non disponibile: tieni la versione 2, lascia XFA disabilitato, avvisa l'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;
Due dettagli in quel blocco sono facili da sbagliare quando scrivi un binding tuo. FXfaPageCountOverride viene riportato a -1 come sentinella prima di qualsiasi altra cosa, così PageCount ricade sull'albero delle pagine statico finché FFI_PageEvent non segnala una ripaginazione; uno zero lì dentro dichiarerebbe in silenzio un documento vuoto. E ognuna delle callback di versione 2 è una routine statica cdecl che recupera il TPdf proprietario dal record e ingoia qualsiasi eccezione Pascal prima di tornare a PDFium, che è la disciplina che la nota sull'irrobustimento dell'ABI di PDFium in Delphi espone per FFI_OpenFile. Nulla nella modifica della versione allenta l'una o l'altra regola
La versione 2 è sicura quando la DLL non ha il modulo XFA?
Sì, e il motivo sta nel record, non in una promessa della libreria. Su una build non XFA l'header dice che la versione 2 fa chiamare anche le callback sperimentali, quindi la domanda è che cosa trovi PDFium quando guarda. TPdfFormFillInfo è un record packed il cui membro Info è il completo FPDF_FORMFILLINFO, inclusi tutti i campi di versione 2, e InitializeFormFill azzera tutto quanto con FillChar prima di toccare un byte. Quindi su una pdfium.dll normale con un documento normale la libreria vede versione 2, xfa_disabled impostato e NULL in ogni slot sperimentale, che è precisamente lo stato che l'header prescrive per un host che non implementa XFA. Non c'è alcun record troncato oltre cui la libreria possa leggere, perché il record non è mai stato più corto della versione 2. La vecchia logica difendeva da un disallineamento di layout che la dichiarazione Pascal aveva già eliminato
Il confine che vale la pena dichiarare onestamente è quello che il record non può coprire. La versione 2 su un documento normale non accende JavaScript, scripting XFA né alcuno degli eventi host dietro quelle callback. m_pJsPlatform viene agganciato solo quando V8FeaturesAvailable è true, XFA resta disabilitato a meno che RuntimeReady non fosse true, e TPdf.XFA continua a riportare il tipo di form da FPDF_GetFormType indipendentemente da ciò che l'ambiente ha negoziato. Un host che vuole sapere se l'XFA dinamico verrà davvero renderizzato dovrebbe continuare a leggere XfaRuntimeAvailable dopo che Active diventa true, come raccomanda la nota sul rilevamento dei form XFA e l'estrazione dei pacchetti XFA, invece di dedurre qualcosa dal campo version
procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
// Scatta da InitializeFormFill quando il documento è XFA ma la pdfium.dll
// caricata non può eseguire il motore. L'ambiente del form si apre comunque,
// perché la versione 2 è stata passata in entrambi i casi; solo il runtime XFA è spento.
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; // non solleva più eccezioni su un PDF normale sotto pdfium.v8.dll
if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
ShowStaticXfaWarning;
end;
Versione del protocollo e disponibilità delle funzionalità sono due assi diversi
La regola generale che esce da questa correzione è che un campo di versione in una struttura di callback risponde alla domanda "quanto è grande questo record e cosa puoi leggerne", mentre il rilevamento delle funzionalità risponde a "quali di quegli slot faranno qualcosa di utile". Il primo è fissato dal binario nativo e dalla dichiarazione Pascal con cui hai compilato. Il secondo varia per documento, per tabella degli export della DLL e per configurazione dell'host. Ridurre i due a un unico booleano è tentatore perché il caso XFA ha bisogno di entrambi, ma nel momento in cui una build impone una versione minima il collasso si rompe per ogni documento che quella funzionalità non la richiede. I form XFA, descritti in ISO 32000-1 §12.7.8 come un payload XML che vive accanto al dizionario AcroForm, sono qui la funzionalità; il layout del record è il protocollo, e PDFium ha il diritto di insistere sul layout prima ancora di guardare il file. La stessa forma compare ovunque una libreria C metta una versione nelle sue strutture: un blocco viewer-info, un record di opzioni di rendering, una tabella di callback di piattaforma. Lo schema sicuro è quello che segue InitializeFormFill corretto. Dichiara il layout più recente che capisci, azzeralo completamente, imposta la versione in modo che corrisponda a quel layout incondizionatamente, e poi lascia che i controlli di capacità decidano quali slot popolare. Se un futuro header di PDFium aggiungesse una versione 3, la modifica riguarderebbe la dichiarazione e quell'unica assegnazione, non un ramo dipendente dal documento che sarà sbagliato per la combinazione che nessuno ha testato
L'inizializzazione form-fill corretta è inclusa in PDFium Component per Delphi, Lazarus e C++Builder, e vale allo stesso modo su Win32 e Win64, dato che entrambe le build condividono la stessa dichiarazione del record. Se la tua applicazione seleziona già pdfium.v8.dll per gli AcroForm guidati da JavaScript, questa è la modifica che le permette di aprire il resto del tuo archivio PDF attraverso lo stesso binario senza trattare l'ambiente del form come caso speciale