PDFium Component salva i valori modificati dei form XFA esattamente, attraverso salvataggio e riapertura, quando gira con il runtime Windows V8 pdfium.v8.dll spedito dalla v3.125.2 in poi. I runtime più vecchi aggiungevano line feed ai valori dei campi, riducevano le emoji a un carattere BMP imparentato, saltavano in silenzio i salvataggi XFA a stream singolo e potevano ingoiare una scrittura finale fallita. Un sintomo di riapertura non è per niente un difetto della libreria: un form dinamico il cui subform radice manca di restoreState="auto" ricostruisce il proprio layout dal template
Le segnalazioni di bug su tutto questo sembravano tutte uguali. Un cliente compila un form XFA di richiesta in un viewer Delphi, salva, riapre, e qualcosa è leggermente storto. Una casella commenti vuota ora contiene una riga vuota, e dopo un secondo salvataggio ne contiene due. Un nome digitato con una emoji torna con un glifo a uso privato. Nessuno riceve un errore, e questo è ciò che rende questi bug costosi: la divergenza si mostra settimane dopo nell'esportazione di qualcun altro
Che cosa va storto quando un form XFA viene salvato e riaperto?
Quattro difetti separati nel percorso nativo di salvataggio XFA causavano divergenze di valore, e ognuno si nascondeva dietro a un salvataggio dall'aria riuscita. Due venivano dalla serializzazione, uno dal layout di memorizzazione a stream singolo, e uno dal writer PDF stesso. La tabella mappa ogni sintomo sulla sua causa e sulla release in cui PDFium Component l'ha sistemato
| Sintomo dopo la riapertura | Causa | Sistemato in |
|---|---|---|
| Il campo vuoto contiene un line feed; i valori guadagnano una newline per salvataggio | Entrambi i writer XFA inserivano newline di layout dopo i tag di apertura | v3.125.2, pdfium.v8.dll |
| U+1F642 torna come U+F642, oppure l'emoji sparisce dal pacchetto form | Troncamento wchar_t a 16 bit in decodifica; filtraggio dei surrogate nel serializzatore del form | v3.125.2, pdfium.v8.dll |
| Le modifiche in un documento XFA a stream singolo sono semplicemente sparite | Il salvataggio nativo rifiutava il layout a stream, ma il valore di ritorno veniva ignorato | v3.125.2; commenti e istruzioni di elaborazione conservati dalla v3.126.0 |
| File troncato benché il salvataggio avesse riportato successo | La scrittura finale bufferizzata è fallita dopo che il writer aveva già riportato successo | v3.125.2 runtime V8; v3.125.3 pdfium.dll ordinario |
| Un form dinamico di tre pagine si riapre come due pagine | Il subform radice non richiede restoreState="auto" | Scrittura del form, non un difetto della libreria |
Scritti precedenti concludevano che le modifiche ai campi XFA non si potessero affatto conservare con PDFium, il che era accurato per i runtime di allora. Il runtime V8 più recente salva i valori XFA nativamente, quindi una modifica fatta nel form vivo arriva al pacchetto datasets salvato senza chirurgia sui pacchetti da parte tua
Quale runtime PDFium salva i valori XFA?
La fedeltà del salvataggio XFA dipende dalla DLL nativa, non dal wrapper Delphi, quindi il primo controllo è quale runtime il tuo processo ha realmente caricato. PDFium Component spedisce due build Windows per architettura: la pdfium.dll ordinaria, compilata senza V8 e senza XFA, e la pdfium.v8.dll, che porta il motore JavaScript e il runtime dei form XFA. Solo la pdfium.v8.dll può far girare un form XFA, quindi ogni correzione XFA descritta qui vive lì, a cominciare dalle librerie V8 Win32 e Win64 ricompilate nella v3.125.2
La correzione della scrittura finale è codice generico del writer PDF, quindi conta anche per i documenti ordinari. La v3.125.3 ha ricompilato le librerie pdfium.dll ordinarie per portare quella stessa riparazione. Sorgente condiviso non è prova di comportamento condiviso: finché il binario non viene ricompilato, la vecchia DLL conserva il vecchio bug
Una seconda trappola stava nel loader. Prima della v3.125.2, impostare EnableV8Engine a True faceva sì che il binding prendesse il nome predefinito pdfium.v8.dll e ignorasse un percorso completo in LibraryName. Un'applicazione che puntava a un runtime appena distribuito poteva continuare a caricare una copia più vecchia da un'altra cartella. Dalla v3.125.2, un LibraryName che contiene una directory seleziona esattamente quel file in entrambe le modalità motore, e un percorso mancante fallisce invece di ricadere su un'altra libreria inclusa
uses
System.SysUtils, PDFium;
procedure SelectXfaRuntime;
begin
// Una directory in LibraryName blocca questo file esatto (v3.125.2 e successive);
// se il file manca, il caricamento solleva invece di ripiegare altrove
{$IFDEF WIN64}
PDFium.LibraryName := ExtractFilePath(ParamStr(0)) + 'DLLs\Win64\pdfium.v8.dll';
{$ELSE}
PDFium.LibraryName := ExtractFilePath(ParamStr(0)) + 'DLLs\Win32\pdfium.v8.dll';
{$ENDIF}
PDFium.EnableV8Engine := True;
PDFium.LoadLibrary; // fallisci all'avvio, non al primo salvataggio
end;
Dopo l'apertura di un documento, TPdf.XFA ti dice che il file contiene XFA e TPdf.XfaRuntimeAvailable ti dice che la DLL caricata può davvero eseguirlo. Se ti serve anche distinguere i form statici dai dinamici, TPdf.FormType restituisce ftXfaFull o ftXfaForeground; l'articolo su rilevare i form XFA ed estrarre i pacchetti XFA in Delphi copre quella esplorazione nel dettaglio
Perché i campi XFA salvati guadagnano line feed extra?
I campi XFA salvati guadagnavano line feed perché entrambi i writer XFA nativi, il writer XML generico di elementi e il serializzatore del pacchetto form, facevano il pretty-print del proprio output con una newline dopo i tag di apertura. Nella maggior parte dell'XML quello spazio è cosmetico. Nei dati XFA no: quando il pacchetto datasets viene parseato di nuovo, il testo tra <Comments> e </Comments> è il valore del campo, newline compresa. Un campo vuoto quindi si riapriva contenente un singolo LF, e ogni ulteriore ciclo salva-e-riapri poteva aggiungerne un altro
La riparazione ovvia, tagliare i valori al caricamento, sarebbe sbagliata. Gli utenti digitano spazi iniziali, spazi finali e testo multi-riga deliberato nei campi XFA, e un blocco indirizzo o un codice a larghezza fissa deve sopravvivere byte per byte. La correzione v3.125.2 quindi rimuove solo lo spazio che il serializzatore stesso ha sintetizzato attorno ai tag. I valori utente, i nodi di testo esistenti e le sezioni CDATA passano intoccati, così " indented" resta indentato e un campo volutamente vuoto resta vuoto
Perché un'emoji torna come un carattere diverso?
Un'emoji tornava sbagliata perché su Windows wchar_t è largo 16 bit, e due percorsi di decodifica memorizzavano un intero valore scalare Unicode in un singolo wchar_t. Lo facevano il decoder di stream UTF-8 e il parser dei riferimenti a caratteri numerici come 🙂. U+1F642, la faccina sorridente, non sta in 16 bit, così i bit alti cadevano e al loro posto compariva U+F642: un code point nella Private Use Area che la maggior parte dei font renderizza come un riquadro o niente
Il serializzatore del form aveva il problema opposto. Filtrava i caratteri un wchar_t alla volta, vedeva due code unit surrogate non valide in isolamento, e le buttava entrambe, così l'emoji spariva dal pacchetto form del tutto. Nella v3.125.2 il decoder consuma ogni valore scalare completamente ed emette una coppia surrogate appropriata. Quando resta un solo slot di uscita, tiene la surrogate bassa in sospeso e non riporta fine-stream finché quell'unità è ancora bufferizzata. Una sequenza UTF-8 spezzata tra blocchi di lettura viene riportata alla lettura successiva anziché scartata. L'esportatore del form ora tiene insieme le coppie surrogate valide, e i riferimenti a caratteri numerici producono coppie corrette a loro volta
Dati di test Latin-1 non mostrano mai nulla di tutto ciò, quindi ogni test di round trip XFA ha bisogno di almeno un carattere del piano supplementare
XFA a stream singolo e fallimenti di salvataggio che nessuno vedeva
Un documento XFA a stream singolo perdeva le proprie modifiche perché l'helper di salvataggio nativo rifiutava quel layout di memorizzazione e il suo chiamante ignorava il fallimento. ISO 32000-1 §12.7.8 consente che l'entry /XFA del dizionario del form interattivo sia o un array di nomi di pacchetti e stream, o un singolo stream che contiene l'intero documento XDP. Gli array di pacchetti sono il caso comune, ma gli stream singoli sono perfettamente legali, e il salvataggio PDF si completava come se niente fosse mentre i dati del form restavano ai loro vecchi valori
Dalla v3.125.2, il runtime V8 gestisce il sottoinsieme supportato degli stream singoli. Per prima cosa esporta entrambi i pacchetti vivi, datasets e form, in un'area di staging e li valida, e solo allora sostituisce i pacchetti corrispondenti nell'XDP originale. Gli altri pacchetti e le dichiarazioni di namespace della radice vengono conservati. Se lo staging fallisce, lo stream XFA persistente non viene mai toccato e il documento conserva il suo marchio di modifica
Commenti XML e istruzioni di elaborazione richiedevano un'attenzione extra perché il DOM XML interno li scarta. Nella v3.125.2 la loro presenza faceva fallire il salvataggio sul posto anziché perdere contenuto in silenzio. La v3.126.0 li conserva: prima del parsing, ogni commento o istruzione di elaborazione viene scambiato con un marker costruito da un prefisso che non compare in nessun punto del testo originale. Dopo che i pacchetti vivi sono stati sostituiti, ogni marker deve comparire esattamente una volta prima che il token originale venga restituito e lo stream scritto. I token fuori dai pacchetti sostituiti quindi conservano testo e ordine, compresi i token nel prologo, nel template e negli altri pacchetti
Alcuni input vengono comunque rifiutati di proposito, e ogni rifiuto è un fallimento di salvataggio esplicito:
- Commenti o istruzioni di elaborazione dentro i pacchetti vivi
datasetsoform, dato che le loro posizioni originali non si possono mappare su contenuto appena esportato - Dichiarazioni DTD e firme XMLDSig, dato che riscrivere l'XDP non può mantenere valida una firma XML
- Codifica UTF-8 o UTF-16 non valida, tag incompleti, riferimenti a caratteri non validi, entità sconosciute e istruzioni di elaborazione malformate, che vengono rifiutate anziché riparate in silenzio
L'output a stream singolo è UTF-8 e conserva il modello di contenuto XML, non il layout di byte originale né la dichiarazione di codifica
L'ultimo difetto stava sotto l'XFA. Il writer di file nativo bufferizza l'output in blocchi da 32 KB e scaricava l'ultimo blocco parziale solo nel suo distruttore, dopo che il writer del documento aveva già riportato successo. Un disco pieno o un errore I/O su quell'ultimo blocco era invisibile al chiamante. Dalla v3.125.2 nel runtime V8 e dalla v3.125.3 nel runtime ordinario, quel flush finale fa parte del risultato del salvataggio, e il marchio di modifica XFA viene pulito solo dopo un successo vero. Sul lato Delphi, TPdf.SaveAs(const FileName: string; Option: TSaveOption = saNone; PdfVersion: TPdfVersion = pvUnknown): Boolean scrive su un file temporaneo accanto al bersaglio e lo sposta al suo posto solo quando il salvataggio restituisce True, così un salvataggio fallito lascia intatto il file precedente
Perché un form XFA dinamico si riapre con meno pagine?
Un form XFA dinamico si riapre con meno pagine quando il suo subform radice non dichiara restoreState="auto", e quella è una decisione di scrittura del form anziché un difetto di PDFium Component. In XFA 3.3, restoreState sul subform radice ha default manual. Sotto manual, il processore XFA ripristina solo uno stato limitato dal pacchetto form salvato e lascia il resto agli script dell'autore. I valori dei campi salvati e i conteggi di istanze dei subform ripetuti tornano comunque, ma le proprietà geometriche impostate a runtime no
Il caso che ha smascherato questo era un form di tre pagine il cui script cresceva un subform a h="450pt". Il pacchetto form salvato conteneva la nuova altezza, i valori e i conteggi di istanze. Alla riapertura, però, il layout veniva ricostruito dalle altezze del template e il form riscorreva su due pagine. Il runtime aveva ragione: il template non aveva mai chiesto il ripristino automatico. Dichiararlo sul subform radice sistema la riapertura:
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb" restoreState="auto">
<pageSet>
<pageArea name="Page1">
<contentArea x="0.25in" y="0.25in" w="8in" h="10.5in"/>
<medium stock="letter"/>
</pageArea>
</pageSet>
<subform name="Details" layout="tb" w="7.5in">
<!-- campi; gli script possono cambiare h o aggiungere istanze a runtime -->
</subform>
</subform>
</template>
Se il template non è tuo, non fare patch intorno ad esso nel viewer: un form che conta sulla modalità manual si aspetta che siano i suoi script a ricostruire lo stato. La ripaginazione in vivo mentre l'utente digita è un argomento a parte, coperto in come PDFium Component traccia i conteggi di pagina XFA dinamici e i campi spostati
Come si verifica un salvataggio XFA in Delphi?
L'unico controllo affidabile di un salvataggio XFA è riaprire il file salvato in un'istanza TPdf fresca e rileggere i dati memorizzati. TPdf.GetXfaDatasets restituisce il pacchetto datasets così com'è memorizzato nel documento, non il modello dati XFA vivo, quindi chiamarlo prima del salvataggio mostra i valori vecchi. Dopo la riapertura mostra esattamente ciò che è stato scritto. Un documento a stream singolo non ha pacchetti con nome separato: PDFium riporta l'intero XDP come un pacchetto con nome vuoto, così GetXfaPacketByName('datasets') e GetXfaDatasets non restituiscono niente, e il fallback legge lo stream completo attraverso GetXfaFormPackets
uses
System.SysUtils, PDFium, FPdfXfa;
function ReadSavedXfaData(const FileName: string): string;
var
Pdf: TPdf;
Packets: TXfaPacketList;
Bytes: TBytes;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True;
Bytes := Pdf.GetXfaDatasets; // layout ad array di pacchetti
if Length(Bytes) = 0 then
begin
Packets := Pdf.GetXfaFormPackets; // stream singolo: un pacchetto senza nome
if Length(Packets) = 1 then
begin
SetLength(Bytes, Length(Packets[0].Content));
if Length(Bytes) > 0 then
Move(Packets[0].Content[0], Bytes[0], Length(Bytes));
end;
end;
Result := TEncoding.UTF8.GetString(Bytes); // l'output XDP salvato è UTF-8
finally
Pdf.Free;
end;
end;
La routine di salvataggio poi commette la modifica in sospeso, controlla il risultato di SaveAs e confronta il valore riaperto. TPdf.ClearFormFieldFocus toglie il focus dal form, che è il momento in cui PDFium commette il buffer di modifica del campo con focus. TPdf.SetFocusedFormFieldText(const Value: WString): Boolean riempie programmaticamente il campo con focus, ma conta su un focus che il wrapper traccia attraverso FocusFormField, che percorre le annotazioni widget. Una pagina XFA dinamica normalmente non ne ha, quindi lì il testo di solito arriva tramite input da tastiera in TPdfView, e la funzione restituisce False quando nessun campo tracciato ha il focus
function XmlText(const S: string): string;
begin
Result := StringReplace(S, '&', '&', [rfReplaceAll]);
Result := StringReplace(Result, '<', '<', [rfReplaceAll]);
end;
procedure SaveXfaAndVerify(Pdf: TPdf; const FileName, FieldTag,
Expected: string);
var
Saved: string;
begin
// Riempimento scriptato opzionale; False significa nessun campo tracciato con focus
if (Pdf.FocusedFormFieldIndex >= 0) and
not Pdf.SetFocusedFormFieldText(Expected) then
raise EPdfError.Create('Could not write the focused field');
Pdf.ClearFormFieldFocus; // commette il buffer di modifica
if not Pdf.SaveAs(FileName) then // include il flush finale (v3.125.2+)
raise EPdfError.CreateFmt('Saving %s failed', [FileName]);
Saved := ReadSavedXfaData(FileName);
if Pos('<' + FieldTag + '>' + XmlText(Expected) + '</' + FieldTag + '>',
Saved) = 0 then
raise EPdfError.CreateFmt('%s did not survive the round trip', [FieldTag]);
end;
Tratta il test della sottostringa come uno smoke test. Un elemento vuoto può essere serializzato come <Tag/>, gli attributi possono comparire sugli elementi dati, e l'escaping oltre & e < è una scelta del serializzatore. Per controlli di produzione, carica l'XML riaperto con un vero parser XML e confronta il nodo di testo dell'elemento dati legato. Esegui il controllo anche due volte di fila, perché il difetto della newline mostrava la sua forma completa solo alla seconda generazione
Riferimento rapido: checklist di fedeltà del salvataggio XFA
- Distribuisci
pdfium.v8.dlldalla v3.125.2 o successiva per i form XFA, e dalla v3.125.3 o successiva per lapdfium.dllordinaria, così la correzione della scrittura finale è in entrambe - Punta
LibraryNamea un percorso completo e impostaEnableV8Enginea True; un percorso mancante fallisce invece di caricare un'altra copia - Conferma
TPdf.XFAeTPdf.XfaRuntimeAvailabledopo aver aperto il documento - Chiama
ClearFormFieldFocusprima diSaveAscosì il campo con focus viene commesso - Ignora mai il risultato booleano di
SaveAs; un risultato False lascia al suo posto il file precedente - Verifica riaprendo in un nuovo
TPdfe leggendoGetXfaDatasets, ricadendo suGetXfaFormPacketsper l'XFA a stream singolo - Prova con valori vuoti, spazi iniziali, testo multi-riga,
&e un carattere del piano supplementare, su due generazioni di salvataggio - Aspettati fallimenti di salvataggio espliciti per DTD, XMLDSig e commenti dentro i pacchetti vivi dell'XFA a stream singolo
- Se un form dinamico perde geometria a runtime alla riapertura, controlla il subform radice per
restoreState="auto"prima di sospettare la libreria
Per la struttura di callback che il runtime XFA si aspetta da un'applicazione host, vedi FPDF_FORMFILLINFO versione 2 e l'ABI XFA in Delphi. Il runtime V8, il wrapper Delphi e C++Builder e il controllo viewer sono tutti parte di PDFium Component for Delphi and C++Builder, che include entrambi i runtime Windows per Win32 e Win64