PDFium Component salvează valorile de formular XFA editate exact, prin salvare și redeschidere, când rulează runtime-ul Windows V8 pdfium.v8.dll livrat în v3.125.2 sau mai nou. Runtime-urile mai vechi adăugau line feed-uri valorilor de field, tăiau emoji-ul până la un caracter BMP fără legătură, săreau tăcut salvările XFA cu stream unic și puteau înghiți o ultimă scriere eșuată. Un simptom de redeschidere nu e deloc un defect de bibliotecă: un formular dinamic al cărui subform rădăcină nu are restoreState="auto" își reconstruiește layout-ul din template
Rapoartele de bug pentru astea arătau toate la fel. Un client completează un formular XFA de decont într-un viewer Delphi, salvează, redeschide, și ceva e ușor strâmb. O casetă de comentarii goală conține acum o linie goală, iar după a doua salvare conține două. Un nume tastat cu un emoji se întoarce cu un glif din zona private-use. Nimeni nu primește o eroare, iar asta face bug-urile astea scumpe: alunecarea iese la suprafață săptămâni mai târziu în exportul cuiva
Ce iese prost când un formular XFA e salvat și redeschis?
Patru defecte separate în calea nativă de salvare XFA au cauzat alunecarea valorilor, iar fiecare s-a ascuns după o salvare cu aspect de succes. Două veneau din serializare, una din aranjamentul de stocare cu stream unic, iar una din writer-ul PDF însuși. Tabelul mapează fiecare simptom la cauza lui și la versiunea în care PDFium Component l-a reparat
| Simptom după redeschidere | Cauză | Reparat în |
|---|---|---|
| Field gol conține un line feed; valorile cresc cu un newline per salvare | Ambii writer-i XFA inserau newline-uri de layout după tag-urile de deschidere | v3.125.2, pdfium.v8.dll |
| U+1F642 se întoarce ca U+F642, sau emoji-ul dispare din pachetul de formular | Trunchiere wchar_t pe 16 biți la decodare; filtrare de surrogate în serializer-ul de formular | v3.125.2, pdfium.v8.dll |
| Editările dintr-un document XFA cu stream unic pur și simplu au dispărut | Salvarea nativă respingea aranjamentul de stream, dar valoarea întoarsă era ignorată | v3.125.2; comentarii și instrucțiuni de procesare păstrate din v3.126.0 |
| Fișier trunchiat deși salvarea raporta succes | Ultima scriere bufferizată a picat după ce writer-ul raportase deja succes | v3.125.2 runtime V8; v3.125.3 pdfium.dll obișnuit |
| Formular dinamic cu trei pagini se redeschide cu două | Subform-ul rădăcină nu cere restoreState="auto" | Autorare de formular, nu un defect de bibliotecă |
Scrierile anterioare concludeau că editările de field XFA nu pot fi persistate deloc cu PDFium, ceea ce era corect pentru runtime-urile de atunci. Runtime-ul V8 mai nou salvează valorile XFA nativ, deci o editare făcută în formularul viu ajunge în pachetul de datasets salvat fără operație de chirurgie de pachete de partea voastră
Ce runtime PDFium salvează valorile XFA?
Fidelitatea salvării XFA depinde de DLL-ul nativ, nu de wrapper-ul Delphi, deci prima verificare e ce runtime a încărcat efectiv procesul vostru. PDFium Component livrează două build-uri Windows per arhitectură: pdfium.dll obișnuit, construit fără V8 și XFA, și pdfium.v8.dll, care poartă motorul JavaScript și runtime-ul de formulare XFA. Doar pdfium.v8.dll poate rula un formular XFA, deci fiecare reparare XFA descrisă aici locuiește acolo, începând cu bibliotecile V8 Win32 și Win64 reconstruite în v3.125.2
Repararea scrierii finale e cod generic de writer PDF, deci contează și pentru documentele obișnuite. v3.125.3 a reconstruit bibliotecile pdfium.dll obișnuite ca să poarte aceeași reparare. Sursă partajată nu e dovadă de comportament partajat: până când binarul e reconstruit, DLL-ul vechi păstrează bug-ul vechi
O a doua capcană stătea în loader. Înainte de v3.125.2, setarea EnableV8Engine pe True făcea binding-ul să aleagă numele implicit pdfium.v8.dll și să ignore o cale completă în LibraryName. O aplicație care pointa spre un runtime proaspăt livrat putea continua să încarce o copie mai veche din alt folder. Din v3.125.2, un LibraryName care conține un director selectează exact acel fișier în oricare mod de motor, iar o cale lipsă pice în loc să cadă pe altă bibliotecă inclusă
uses
System.SysUtils, PDFium;
procedure SelectXfaRuntime;
begin
// Un director în LibraryName fixează exact acest fișier (v3.125.2 și mai nou);
// dacă fișierul lipsește, încărcarea ridică excepție în loc de fallback
{$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; // pice la pornire, nu la prima salvare
end;
După deschiderea unui document, TPdf.XFA vă spune că fișierul conține XFA, iar TPdf.XfaRuntimeAvailable vă spune că DLL-ul încărcat poate efectiv să-l execute. Dacă aveți nevoie și să deosebiți formularele statice de cele dinamice, TPdf.FormType întoarce ftXfaFull sau ftXfaForeground; articolul despre detectarea formularelor XFA și extragerea pachetelor XFA în Delphi acoperă în detaliu acea sondare
De ce câștigă field-urile XFA salvate line feed-uri de prisos?
Field-urile XFA salvate câștigau line feed-uri pentru că ambii writer-i nativi XFA, writer-ul generic de elemente XML și serializer-ul de pachet de formular, formatau frumos output-ul cu un newline după tag-urile de deschidere. În majoritatea XML-ului spațiul acela e cosmetic. În datele XFA nu e: când pachetul de datasets e parsat din nou, textul dintre <Comments> și </Comments> e valoarea field-ului, newline inclus. Un field gol se redeschidea deci ținând un singur LF, iar fiecare ciclu de salvare-și-redeschidere putea adăuga încă unul
Repararea evidentă, tăierea spațiilor la încărcare, ar fi greșită. Utilizatorii tastează spații de început, spații de final și text pe mai multe linii deliberat în field-urile XFA, iar un bloc de adresă sau un cod cu lățime fixă trebuie să supraviețuiască byte cu byte. Repararea din v3.125.2 elimină deci doar spațiul pe care serializer-ul însuși l-a sintetizat în jurul tag-urilor. Valorile utilizatorului, nodurile de text existente și secțiunile CDATA trec neatinse, deci " indented" rămâne indentat, iar un field intenționat gol rămâne gol
De ce un emoji se întoarce ca un alt caracter?
Un emoji se întorcea greșit pentru că wchar_t-ul Windows are 16 biți, iar două căi de decodare stocau o valoare scalară Unicode completă într-un singur wchar_t. Decodorul de stream UTF-8 și parser-ul pentru referințe de caractere numerice precum 🙂 făceau amândouă asta. U+1F642, fața ușor zâmbitoare, nu încape în 16 biți, deci biții de sus cădeau, iar în loc apărea U+F642: un code point din Private Use Area pe care majoritatea fonturilor îl randează drept o casetă sau nimic
Serializer-ul de formular avea problema opusă. Filtra caracterele câte un wchar_t pe rând, vedea două unități de cod surrogate invalide în izolare și le arunca pe amândouă, deci emoji-ul dispărea complet din pachetul de formular. În v3.125.2 decodorul consumă fiecare valoare scalară complet și emite o pereche surrogate corectă. Când mai rămâne doar un singur slot de output, el ține surrogate-ul de jos în așteptare și nu raportează sfârșit de stream cât timp unitatea aceea e încă bufferizată. O secvență UTF-8 despărțită între blocuri de citire e dusă mai departe la următoarea citire în loc să fie aruncată. Exportatorul de formulare păstrează acum perechile surrogate valide împreună, iar referințele de caractere numerice produc perechi corecte și ele
Datele de test Latin-1 nu arată nimic din toate astea, deci fiecare test de round-trip XFA are nevoie de cel puțin un caracter din planul suplimentar
XFA cu stream unic și eșecuri de salvare pe care nimeni nu le vedea
Un document XFA cu stream unic își pierdea editările pentru că helper-ul nativ de salvare respingea acel aranjament de stocare, iar apelantul ignora eșecul. ISO 32000-1 §12.7.8 permite ca intrarea /XFA a dicționarului de formular interactiv să fie fie un array de nume de pachete și stream-uri, fie un stream unic care ține întregul document XDP. Array-urile de pachete sunt cazul comun, dar stream-urile unice sunt perfect legale, iar salvarea PDF se termina ca și cum nimic nu s-ar fi întâmplat, în timp ce datele formularului rămâneau la valorile vechi
Din v3.125.2, runtime-ul V8 gestionează submulțimea suportată cu stream unic. El exportă întâi ambele pachete vii, datasets și form, într-o zonă de staging și le validează, și abia apoi înlocuiește pachetele corespondente în XDP-ul original. Celelalte pachete și declarațiile de namespace ale rădăcinii sunt păstrate. Dacă staging-ul pice, stream-ul XFA persistent nu e niciodată atins, iar documentul își păstrează marca de modificare
Comentariile XML și instrucțiunile de procesare au cerut grijă suplimentară fiindcă DOM-ul XML intern le aruncă. În v3.125.2 prezența lor făcea salvarea să pice categoric în loc să piardă conținut tăcut. v3.126.0 le păstrează: înainte de parsare, fiecare comentariu sau instrucțiune de procesare e schimbat cu un marker construit dintr-un prefix care nu apare nicăieri în textul original. După ce pachetele vii sunt înlocuite, fiecare marker trebuie să apară exact o dată înainte ca token-ul original să fie restaurat și stream-ul să fie scris. Token-urile din afara pachetelor înlocuite își păstrează deci textul și ordinea, inclusiv token-urile din prolog, din template și din celelalte pachete
Unele intrări sunt totuși respinse în mod intenționat, iar fiecare respingere e un eșec de salvare explicit:
- Comentarii sau instrucțiuni de procesare în interiorul pachetelor vii
datasetssauform, fiindcă pozițiile lor originale nu pot fi mapate în conținutul exportat proaspăt - Declarații DTD și semnături XMLDSig, fiindcă rescrierea XDP nu poate ține o semnătură XML validă
- Encodare invalidă UTF-8 sau UTF-16, tag-uri incomplete, referințe de caractere invalide, entități necunoscute și instrucțiuni de procesare malformate, care sunt respinse în loc să fie reparate tăcut
Output-ul cu stream unic e UTF-8 și păstrează modelul de conținut XML, nu aranjamentul de bytes original sau declarația de encodare
Ultimul defect stătea sub XFA. Writer-ul nativ de fișiere bufferizează output-ul în blocuri de 32 KB și golea blocul final parțial doar în destructorul lui, după ce writer-ul documentului raportase deja succes. O eroare de disc plin sau de I/O pe blocul acela din urmă era invizibilă apelantului. Din v3.125.2 în runtime-ul V8 și v3.125.3 în runtime-ul obișnuit, flush-ul final face parte din rezultatul salvării, iar marca de modificare XFA e ștearsă doar după un succes real. Pe partea Delphi, TPdf.SaveAs(const FileName: string; Option: TSaveOption = saNone; PdfVersion: TPdfVersion = pvUnknown): Boolean scrie într-un fișier temporar lângă țintă și îl mută la loc doar când salvarea întoarce True, deci o salvare eșuată lasă fișierul anterior intact
De ce un formular dinamic XFA se redeschide cu mai puține pagini?
Un formular dinamic XFA se redeschide cu mai puține pagini când subform-ul lui rădăcină nu declară restoreState="auto", iar asta e o decizie de autorare de formular, nu un defect PDFium Component. În XFA 3.3, restoreState pe subform-ul rădăcină are implicit manual. Sub manual, procesorul XFA restaurează doar stare limitată din pachetul de formular salvat și lasă restul scripturilor autorului. Valorile de field salvate și contoarele de instanțe de subform repetabil se întorc totuși, dar proprietățile geometrice setate la runtime nu
Cazul care a expus asta a fost un formular cu trei pagini al cărui script creștea un subform la h="450pt". Pachetul de formular salvat ținea noua înălțime, valorile și contoarele de instanțe. La redeschidere, însă, layout-ul era reconstruit din înălțimile template-ului, iar formularul se re-fluxa pe două pagini. Runtime-ul avea dreptate: template-ul nu ceruse niciodată restaurare automată. Declararea lui pe subform-ul rădăcină repară redeschiderea:
<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">
<!-- field-uri; scripturile pot schimba h sau adăuga instanțe la runtime -->
</subform>
</subform>
</template>
Dacă nu dețineți template-ul, nu-l plasturați în viewer: un formular care se bazează pe modul manual se așteaptă ca scripturile lui proprii să reconstruiască starea. Repaginarea vie în timp ce utilizatorul tastează e un subiect separat, acoperit în cum urmărește PDFium Component numărul de pagini dynamic XFA și field-urile mutate
Cum verificați o salvare XFA în Delphi?
Singura verificare de salvare XFA de încredere e să redeschideți fișierul salvat într-o instanță TPdf proaspătă și să citiți datele stocate înapoi. TPdf.GetXfaDatasets întoarce pachetul de datasets așa cum e stocat în document, nu modelul de date XFA viu, deci apelarea lui înainte de salvare arată valorile vechi. După redeschidere, arată exact ce a fost scris. Un document cu stream unic nu are pachete numite separat: PDFium raportează tot XDP-ul drept un pachet cu nume gol, deci GetXfaPacketByName('datasets') și GetXfaDatasets nu întorc nimic, iar fallback-ul citește stream-ul complet prin 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; // aranjament cu array de pachete
if Length(Bytes) = 0 then
begin
Packets := Pdf.GetXfaFormPackets; // stream unic: un pachet fără nume
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); // output-ul XDP salvat e UTF-8
finally
Pdf.Free;
end;
end;
Rutina de salvare comite apoi editarea în așteptare, verifică rezultatul lui SaveAs și compară valoarea redeschisă. TPdf.ClearFormFieldFocus omoară focus-ul de formular, care e momentul în care PDFium comite buffer-ul de editare al field-ului cu focus. TPdf.SetFocusedFormFieldText(const Value: WString): Boolean umple field-ul cu focus programatic, dar se bazează pe un focus pe care wrapper-ul îl urmărește prin FocusFormField, care parcurge adnotările widget. O pagină dinamică XFA nu are în mod normal niciuna, deci acolo textul sosește de obicei prin input de tastatură în TPdfView, iar funcția întoarce False când niciun field urmărit nu are 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
// Completare scriptată opțională; False înseamnă că niciun field urmărit nu are focus
if (Pdf.FocusedFormFieldIndex >= 0) and
not Pdf.SetFocusedFormFieldText(Expected) then
raise EPdfError.Create('Could not write the focused field');
Pdf.ClearFormFieldFocus; // comite buffer-ul de editare
if not Pdf.SaveAs(FileName) then // include flush-ul final (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;
Tratați testul de sub-șir drept un smoke test. Un element gol poate fi serializat ca <Tag/>, atributele pot apărea pe elementele de date, iar escaparea dincolo de & și < e o alegere de serializer. Pentru verificări de producție, încărcați XML-ul redeschis cu un parser XML real și comparați nodul de text al elementului de date legat. Rulați verificarea de două ori la rând și pe acolo, fiindcă defectul de newline și-a arătat forma completă abia la a doua generație
Referință rapidă: lista de verificare a fidelității salvării XFA
- Livrați
pdfium.v8.dlldin v3.125.2 sau mai nou pentru formularele XFA, și v3.125.3 sau mai nou pentrupdfium.dllobișnuit, ca repararea scrierii finale să fie în ambele - Pointați
LibraryNamespre o cale completă și setațiEnableV8Enginepe True; o cale lipsă pice în loc să încarce altă copie - Confirmați
TPdf.XFAșiTPdf.XfaRuntimeAvailabledupă deschiderea documentului - Apelați
ClearFormFieldFocusînainte deSaveAsca field-ul cu focus să fie comis - Nu ignorați niciodată rezultatul Boolean al lui
SaveAs; un rezultat False lasă fișierul anterior la locul lui - Verificați prin redeschidere într-un
TPdfnou și citirea luiGetXfaDatasets, cu fallback laGetXfaFormPacketspentru XFA cu stream unic - Testați cu valori goale, spații de început, text pe mai multe linii,
&și un caracter din planul suplimentar, de-a lungul a două generații de salvare - Așteptați-vă la eșecuri de salvare explicite pentru DTD-uri, XMLDSig și comentarii în interiorul pachetelor vii ale XFA cu stream unic
- Dacă un formular dinamic pierde geometria de runtime la redeschidere, verificați subform-ul rădăcină pentru
restoreState="auto"înainte să bănuiți biblioteca
Pentru structura de callback pe care runtime-ul XFA o așteaptă de la o aplicație host, vezi FPDF_FORMFILLINFO versiunea 2 și ABI-ul XFA în Delphi. Runtime-ul V8, wrapper-ul Delphi și C++Builder și controlul de viewer fac toate parte din PDFium Component pentru Delphi și C++Builder, care include ambele runtime-uri Windows pentru Win32 și Win64