TPdf.SetFocusedFormFieldText nel componente PDFium scrive nel buffer di modifica vivo del campo modulo attualmente a fuoco, e per un modulo XFA quel buffer non raggiunge mai il pacchetto datasets che viene serializzato su disco — quindi un valore che un utente digita, e che il tuo codice conferma essere stato accettato, sparisce silenziosamente la volta successiva che il file viene aperto. I campi AcroForm non hanno questo problema: la stessa chiamata si impegna sulla voce /V del campo nel momento in cui il fuoco si sposta altrove. Un utente che compila un modulo di ingresso XFA, salva, e riapre per trovare il campo importo di nuovo vuoto non sta incontrando un glitch di rendering — sta incontrando il confine di ciò che il motore PDFium stesso espone per scrivere dati di modulo
Questa è una domanda più ristretta del rilevare un modulo XFA in primo luogo, o far eseguire il suo JavaScript: non "PDFium supporta XFA" e non "come eseguo gli script AcroForm" ma specificamente cosa succede a un valore dopo che SetFocusedFormFieldText segnala successo. La versione breve è che AcroForm e XFA non sono due dialetti dello stesso modello di modulo per quanto riguarda il percorso di scrittura di PDFium — sono due modelli di modulo con due relazioni completamente diverse tra ciò che un utente digita e ciò che un salvataggio effettivamente cattura, e confondere i due è ciò che trasforma una chiamata API di una riga in un ticket di supporto tre settimane dopo che il pilota di un cliente va in produzione. L'articolo sul JavaScript AcroForm mostra la chiamata di una riga e dichiara l'esito AcroForm-contro-XFA in un commento di codice; questo resta sulla stessa API e percorre il percorso di scrittura interno, la prova tramite il pacchetto datasets che la scrittura XFA non atterra mai, perché la lacuna risieda in PDFium stesso piuttosto che nel binding Delphi, e una soluzione alternativa di patch al proprio XML per documenti che necessitano che la modifica sopravviva a un salvataggio
Come scrive un valore di campo SetFocusedFormFieldText?
TPdf.SetFocusedFormFieldText funziona simulando una modifica a livello di tasto premuto, non inserendo un valore direttamente nel modello documento. Internamente chiama FORM_SelectAllText per selezionare il contenuto corrente del campo a fuoco, poi FORM_ReplaceSelection per sovrascrivere la selezione con la nuova stringa — le stesse due operazioni che scatenerebbe un seleziona-tutto-e-digita guidato da tastiera. Poiché la scrittura passa attraverso il percorso di modifica testo interattiva di PDFium invece di aggirarlo, qualsiasi script di tasto premuto, formato, o calcolo collegato al campo si genera esattamente come farebbe per un essere umano che digita, il che è ciò che rende l'API utile per la compilazione programmatica di moduli in un visualizzatore che mantiene JavaScript attivo. La controparte in lettura è FocusedFormFieldText, sostenuta da FORM_GetFocusedText, e riflette lo stesso buffer vivo che SetFocusedFormFieldText ha appena scritto
if Pdf.FocusedFormFieldIndex >= 0 then
begin
if Pdf.SetFocusedFormFieldText('1284.50') then
Log('Buffer now reads: ' + Pdf.FocusedFormFieldText)
else
Log('No field is focused, or it does not accept text');
end
else
Log('Focus a field first - FocusFormField or a real click');
Perché AcroForm mantiene il valore e XFA lo perde?
I campi di testo e combo AcroForm persistono perché l'ambiente di riempimento moduli proprio di PDFium impegna per te il buffer di modifica: nell'istante in cui il campo perde il fuoco, il buffer viene scritto nella voce /V del campo, la stessa chiave che ogni lettore PDF conforme guarda per conoscere il valore memorizzato di un campo. TPdf.ClearFormFieldFocus — che chiama FORM_ForceToKillFocus sotto il cofano — forza quel commit a richiesta, cosicché il codice che imposta un valore programmaticamente non debba aspettare un vero clic del mouse altrove nell'interfaccia. Salva subito dopo, e il nuovo testo fa parte del grafo di oggetti documento prima ancora che TPdf.SaveAs giri, perché /V è una vera voce in un vero dizionario di campo, non qualcosa avvitato dopo
Pdf.FocusFormField(FieldIndex);
Pdf.SetFocusedFormFieldText('1284.50');
Pdf.ClearFormFieldFocus; // forces the /V commit now
Pdf.SaveAs('invoice-acroform.pdf');
// Reopen and confirm - this is an AcroForm document, so it holds
Pdf.Active := False;
Pdf.FileName := 'invoice-acroform.pdf';
Pdf.Active := True;
Pdf.FocusFormField(FieldIndex);
Assert(Pdf.FocusedFormFieldValue = '1284.50'); // passes
Dove vive realmente una modifica di campo XFA?
I campi XFA non hanno tale cablaggio. Il testo che un utente digita atterra in un buffer CPWL_Edit che appartiene al livello di rendering e interazione XFA di PDFium, e quel livello non ha alcun percorso di codice che ricopi il buffer nel pacchetto datasets memorizzato nel PDF. TPdf.GetXfaDatasets rende visibile la lacuna: chiamalo prima e dopo una modifica su un campo XFA e i byte che restituisce sono identici, perché il metodo legge il pacchetto originale con cui il documento è stato aperto, mai lo stato vivo del widget che hai appena modificato. Nulla di ciò è un bug di caching o un problema di tempistica di refresh — il pacchetto datasets su disco e il buffer di modifica in memoria sono semplicemente due pezzi di stato diversi che l'API pubblica di PDFium non collega mai
var
Before, After: TBytes;
begin
Before := Pdf.GetXfaDatasets;
Pdf.FocusFormField(FieldIndex);
Pdf.SetFocusedFormFieldText('1284.50');
After := Pdf.GetXfaDatasets;
// Before and After are byte-for-byte identical on an XFA document -
// the edit never touched the packet GetXfaDatasets reads from
end;
È un bug del componente PDFium o una limitazione di PDFium?
Il pezzo mancante risiede in PDFium stesso, non nel binding Delphi sopra di esso. L'API pubblica di PDFium non ha alcun FPDF_SetXFAPacket per iniettare un pacchetto aggiornato e nessun FPDF_SaveAsXFA per chiedere al motore XFA di serializzare il proprio DOM corrente nell'XML datasets prima di un salvataggio. FPDF_SaveAsCopy — l'esportazione che sostiene TPdf.SaveAs — scrive il grafo di oggetti documento che PDFium già possiede; non ha alcun aggancio per chiedere al motore XFA di scaricare prima il proprio stato vivo, perché quell'aggancio non esiste a monte. Il componente PDFium non può aggiungere una riconciliazione che PDFium stesso non ha mai implementato, e distribuire un serializzatore DOM-a-XML fatto in casa che indovina lo stato XFA interno di PDFium sarebbe peggio della lacuna onesta: sembrerebbe funzionare finché la versione successiva di PDFium non cambia qualcosa che nessuno al di fuori del progetto può vedere
Questo confine è emerso durante lo stesso audit v2.13.2 che ha costruito SetFocusedFormFieldText in primo luogo. FORM_ReplaceSelection era stato collegato nella tabella di import della DLL per versioni senza mai essere chiamato dal codice Pascal, e aggiungere il percorso di scrittura che finalmente lo usava è ciò che ha reso la lacuna di persistenza abbastanza concreta da documentare invece che teorica. Lo stesso giro di audit ha fatto emergere una lacuna non correlata ma affine nello spirito: il JavaScript AcroForm era stato silenziosamente disabilitato dalla v2.13.0 perché la piattaforma JS era collegata solo dentro il ramo di inizializzazione XFA, quindi i normali documenti AcroForm con app.alert o campi calcolati non ottenevano mai affatto un motore di script. Quella era risolvibile — estendendo la piattaforma JS a ogni documento indipendentemente da XFA — ed è stata distribuita nella stessa versione; la lacuna di persistenza trattata qui non era risolvibile, per le ragioni sopra. La correzione JavaScript e gli eventi host-veto attorno ad essa sono trattati in eseguire JavaScript AcroForm con il componente PDFium
Cosa dovresti fare al riguardo in Delphi?
Per i documenti AcroForm, la correzione non è altro che una buona abitudine: chiama ClearFormFieldFocus (o altrimenti sposta il fuoco altrove) prima di SaveAs ogni volta che un valore è stato impostato programmaticamente, invece di presupporre che una successiva interazione UI scatenerà il commit per te. Per un documento che potrebbe essere sia AcroForm sia XFA — il caso comune in un visualizzatore general-purpose — controlla FormType o il booleano XFA prima di promettere a un chiamante che un salvataggio reggerà, e leggi rilevare moduli XFA ed estrarre pacchetti XFA per l'insieme completo di sonde, incluso il caso XFAF dove il contenuto XFA è stratificato sopra widget AcroForm altrimenti ordinari che rispettano /V
Per un vero modulo XFA dinamico dove i valori modificati devono sopravvivere a un salvataggio, il buffer di modifica interattivo non è affatto lo strumento giusto. Il percorso durevole è trattare GetXfaDatasets come la tua baseline, non il tuo risultato: leggilo una volta quando il documento si apre, mantieni il tuo proprio registro di cosa l'utente ha cambiato campo per campo — esattamente i valori che la tua UI già possiede, poiché PDFium non te li restituirà a posteriori — applica quelle patch alla baseline XML da te, e guida il tuo proprio output. Una scrittura che passa attraverso XML controllato dal tuo stesso codice sopravvive a un salvataggio che un buffer CPWL_Edit non potrebbe mai
function ExportEditedXfaValue(Pdf: TPdf; const FieldPath,
NewValue: string): TBytes;
var
DatasetsXml: string;
begin
// GetXfaDatasets ships with PDFium Component; PatchXmlNode below is
// your own helper over your own XML library, nothing PDFium provides
DatasetsXml := TEncoding.UTF8.GetString(Pdf.GetXfaDatasets);
DatasetsXml := PatchXmlNode(DatasetsXml, FieldPath, NewValue);
Result := TEncoding.UTF8.GetBytes(DatasetsXml);
end;
Catturare la lacuna prima che lo faccia un cliente
TPdf.SaveAs restituisce True indipendentemente dal fatto che un valore di campo XFA sia sopravvissuto, perché dal punto di vista di PDFium il salvataggio è genuinamente riuscito — ha scritto ogni byte che gli è stato chiesto di scrivere. Questo lo rende esattamente il tipo di difetto che sfugge a uno smoke test e raggiunge un cliente: nulla solleva eccezioni, nulla registra log, il file si apre bene, solo il valore specifico è sbagliato. Un test di andata e ritorno che effettivamente riapre il file salvato e confronta il valore del campo — o confronta GetXfaDatasets prima e dopo, secondo l'esempio precedente — appartiene alla suite di regressione per qualsiasi visualizzatore che permetta agli utenti di modificare contenuto XFA, non solo ai percorsi AcroForm che capitano di funzionare per default
Nulla di tutto ciò è un difetto da segnalare contro il componente PDFium quanto un confine attorno al quale progettare: SetFocusedFormFieldText fa precisamente ciò che il suo nome dice per entrambi i modelli di modulo, e la differenza nell'esito si ricollega chiaramente a cosa AcroForm e XFA collegano ciascuno quel buffer sul lato PDFium. L'API, le primitive di fuoco e salvataggio, e i lettori di pacchetto citati qui fanno parte del componente PDFium per Delphi e C++Builder