TPdf.SetFocusedFormFieldText din componenta PDFium scrie în bufferul de editare viu al câmpului de formular aflat curent în focus, iar pentru un formular XFA acel buffer nu ajunge niciodată la pachetul datasets care este serializat pe disc — așa că o valoare pe care un utilizator o tastează, iar codul dvs. confirmă că a fost acceptată, dispare silențios data viitoare când fișierul se deschide. Câmpurile AcroForm nu au această problemă: același apel confirmă intrarea /V a câmpului în momentul în care focus-ul se mută în altă parte. Un utilizator care completează un formular de intake XFA, salvează, și redeschide pentru a găsi câmpul de sumă gol din nou nu lovește un defect de randare — lovește marginea a ceea ce expune motorul PDFium însuși pentru scrierea datelor de formular
Aceasta este o întrebare mai restrânsă decât detectarea unui formular XFA în primul rând, sau punerea în funcțiune a JavaScript-ului său: nu „susține PDFium XFA" și nu „cum execut scripturi AcroForm", ci specific ce se întâmplă cu o valoare după ce SetFocusedFormFieldText raportează succes. Versiunea scurtă este că AcroForm și XFA nu sunt două dialecte ale aceluiași model de formular, atâta timp cât privește calea de scriere a PDFium — sunt două modele de formular cu două relații complet diferite între ce tastează un utilizator și ce chiar captează o salvare, iar confundarea celor două este ceea ce transformă un apel API de o linie într-un tichet de suport trei săptămâni după ce implementarea pilot a unui client intră în producție. Articolul despre JavaScript AcroForm arată apelul de o linie și declară rezultatul AcroForm-versus-XFA într-un comentariu de cod; acesta rămâne pe același API și parcurge calea de scriere internă, dovada cu pachetul datasets că scrierea XFA nu aterizează niciodată, de ce golul stă în PDFium însuși, nu în legarea Delphi, și o soluție de corectare-a-propriului-XML pentru documente care au nevoie ca editarea să supraviețuiască unei salvări
Cum scrie SetFocusedFormFieldText o valoare de câmp?
TPdf.SetFocusedFormFieldText funcționează simulând o editare la nivel de apăsare de tastă, nu introducând o valoare direct în modelul documentului. Intern apelează FORM_SelectAllText pentru a selecta conținutul curent al câmpului în focus, apoi FORM_ReplaceSelection pentru a suprascrie selecția cu noul șir — aceleași două operații pe care le-ar declanșa un selectează-tot-și-tastează condus de tastatură. Pentru că scrierea trece prin calea de editare interactivă de text a PDFium, nu în jurul ei, orice script de apăsare-tastă, format, sau calcul legat de câmp se declanșează exact așa cum ar face-o pentru un om care tastează, ceea ce face API-ul util pentru completarea programatică de formulare într-un vizualizator care păstrează JavaScript-ul viu. Corespondentul de citire este FocusedFormFieldText, susținut de FORM_GetFocusedText, și reflectă același buffer viu pe care tocmai l-a scris SetFocusedFormFieldText
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');
De ce păstrează AcroForm valoarea și XFA o pierde?
Câmpurile de text și combo AcroForm persistă pentru că propriul mediu de completare de formular al PDFium confirmă bufferul de editare pentru dvs.: în clipa în care câmpul pierde focus-ul, bufferul este scris în intrarea /V a câmpului, aceeași cheie pe care fiecare cititor PDF conform o privește pentru a ști valoarea stocată a unui câmp. TPdf.ClearFormFieldFocus — care apelează FORM_ForceToKillFocus pe sub — forțează acea confirmare la cerere, așa că un cod care setează o valoare programatic nu trebuie să aștepte un clic real de mouse în altă parte a UI-ului. Salvați imediat după, iar noul text face parte din graful de obiecte al documentului înainte ca TPdf.SaveAs să ruleze vreodată, pentru că /V este o intrare reală într-un dicționar de câmp real, nu ceva atașat ulterior
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
Unde trăiește de fapt o editare de câmp XFA?
Câmpurile XFA nu au o astfel de conectare. Textul pe care îl tastează un utilizator aterizează într-un buffer CPWL_Edit care aparține stratului de randare și interacțiune XFA al PDFium, iar acel strat nu are nicio cale de cod care să copieze bufferul înapoi în pachetul datasets stocat în PDF. TPdf.GetXfaDatasets face golul vizibil: apelați-l înainte și după o editare pe un câmp XFA, iar octeții pe care îi returnează sunt identici, pentru că metoda citește pachetul original cu care a fost deschis documentul, niciodată starea vie a widget-ului pe care tocmai l-ați editat. Nimic din asta nu este un bug de cache sau o problemă de sincronizare a reîmprospătării — pachetul datasets de pe disc și bufferul de editare din memorie sunt pur și simplu două bucăți diferite de stare pe care API-ul public al PDFium nu le conectează niciodată
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;
Este acesta un bug al componentei PDFium sau o limitare a PDFium?
Piesa lipsă stă în PDFium însuși, nu în legarea Delphi de deasupra lui. API-ul public al PDFium nu are niciun FPDF_SetXFAPacket pentru a injecta un pachet actualizat și niciun FPDF_SaveAsXFA pentru a cere motorului XFA să își serializeze DOM-ul curent înapoi în XML datasets înainte de o salvare. FPDF_SaveAsCopy — exportul care susține TPdf.SaveAs — scrie graful de obiecte al documentului pe care PDFium îl are deja; nu are niciun cârlig pentru a cere motorului XFA să își descarce mai întâi starea vie, pentru că acel cârlig nu există în amonte. Componenta PDFium nu poate adăuga o reconciliere pe care PDFium însuși nu a implementat-o niciodată, iar livrarea unui serializator DOM-la-XML propriu care ghicește starea internă XFA a PDFium ar fi mai rău decât golul onest: ar arăta ca și cum funcționează până când următoarea versiune PDFium schimbă ceva ce nimeni din afara proiectului nu poate vedea
Această graniță a apărut în timpul aceluiași audit v2.13.2 care a construit SetFocusedFormFieldText în primul rând. FORM_ReplaceSelection fusese legat în tabelul de import DLL pentru versiuni fără a fi vreodată apelat din cod Pascal, iar adăugarea căii de scriere care în cele din urmă l-a folosit este ceea ce a făcut golul de persistență suficient de concret pentru a fi documentat, nu doar teoretic. Aceeași rundă de audit a scos la iveală un gol fără legătură dar înrudit în spirit: JavaScript-ul AcroForm fusese dezactivat silențios de la v2.13.0 pentru că platforma JS era conectată doar în interiorul ramurii de inițializare XFA, așa că documentele AcroForm obișnuite cu app.alert sau câmpuri calculate nu primeau deloc un motor de script. Acela era reparabil — extinderea platformei JS la fiecare document indiferent de XFA — și a fost livrat în aceeași versiune; golul de persistență acoperit aici nu era reparabil, din motivele de mai sus. Corecția de JavaScript și evenimentele de veto gazdă din jurul ei sunt acoperite în rularea JavaScript AcroForm cu componenta PDFium
Ce ar trebui să faceți în privința asta în Delphi?
Pentru documentele AcroForm, soluția nu este nimic mai mult decât un obicei bun: apelați ClearFormFieldFocus (sau altfel mutați focus-ul în altă parte) înainte de SaveAs ori de câte ori o valoare a fost setată programatic, în loc să presupuneți că o interacțiune UI ulterioară va declanșa confirmarea pentru dvs. Pentru un document care ar putea fi fie AcroForm, fie XFA — ceea ce este cazul comun într-un vizualizator cu scop general — verificați FormType sau booleanul XFA înainte de a promite unui apelant că o salvare va rămâne, și citiți detectarea formularelor XFA și extragerea pachetelor XFA pentru setul complet de sonde, incluzând cazul XFAF unde conținutul XFA este stratificat peste widget-uri altfel-obișnuite AcroForm care chiar respectă /V
Pentru un formular XFA dinamic autentic unde valorile editate trebuie să supraviețuiască unei salvări, bufferul de editare interactiv nu este deloc unealta corectă. Calea durabilă este să tratați GetXfaDatasets ca linia dvs. de bază, nu rezultatul dvs.: citiți-l o dată când documentul se deschide, păstrați propria dvs. evidență a ce a schimbat utilizatorul câmp cu câmp — exact valorile pe care le are deja UI-ul dvs., întrucât PDFium nu vi le va returna ulterior — corectați-le pe acestea în XML-ul de bază singur, și conduceți propria dvs. ieșire. O scriere care trece prin XML pe care propriul dvs. cod îl controlează supraviețuiește unei salvări pe care un buffer CPWL_Edit nu ar putea niciodată
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;
Prinderea golului înainte ca un client să o facă
TPdf.SaveAs returnează True indiferent dacă o valoare de câmp XFA a supraviețuit sau nu, pentru că din punctul de vedere al PDFium salvarea a reușit efectiv — a scris fiecare octet care i s-a cerut să îl scrie. Asta face din acesta exact genul de defect care alunecă pe lângă un test de fum și ajunge la un client: nimic nu ridică o excepție, nimic nu înregistrează, fișierul se deschide bine, doar valoarea specifică este greșită. Un test dus-întors care chiar redeschide fișierul salvat și compară valoarea câmpului — sau compară GetXfaDatasets înainte și după, conform exemplului anterior — aparține suitei de regresie pentru orice vizualizator care permite utilizatorilor să editeze conținut XFA, nu doar căile AcroForm care se întâmplă să funcționeze implicit
Nimic din toate acestea nu este un defect de raportat împotriva componentei PDFium, ci mai degrabă o graniță în jurul căreia să proiectați: SetFocusedFormFieldText face exact ce spune numele său pentru ambele modele de formular, iar diferența de rezultat urmărește curat la ce conectează AcroForm și XFA acel buffer fiecare pe partea PDFium. API-ul, primitivele de focus și salvare, și cititorii de pachete referiți aici fac parte din componenta PDFium pentru Delphi și C++Builder