HotPDF completează formulare XFA dinamice în Delphi prin TXFAWidgetRuntime, un strat de widgeturi neutru față de gazdă care tratează fiecare editare de câmp ca o singură tranzacție: snapshot, validate, calculate, reflow, apoi publică totul sau revine integral. Rulează pe un singur fir în interiorul propriei dumneavoastră gazde VCL sau FMX, nu cere Acrobat instalat și impune fiecare buget înainte să aloce ceva
Scenariul este familiar oricui a livrat software de documente către administrație sau asigurări. Un formular de despăgubiri sau o declarație fiscală sosește ca un PDF al cărui conținut de pagină este o singură notificare „Please wait... if this message is not eventually replaced”, iar fiecare câmp real trăiește într-un pachet XFA pe care doar Adobe Acrobat îl randează. Utilizatorii dumneavoastră vor să îl completeze în interiorul aplicației dumneavoastră. Nu vă puteți salva nici prin rasterizare, pentru că formularul crește rânduri pe măsură ce se introduc date, iar aranjamentul de după al treilea rând nu este aranjamentul livrat în fișier
De ce XFA dinamic rămâne o problemă care merită rezolvată
XFA dinamic persistă pentru că formularele implementate supraviețuiesc formatului care le-a purtat. ISO 32000-1 §12.7.8 descrie XFA ca o intrare /XFA pe dicționarul AcroForm care ține un flux de pachet XDP, iar ISO 32000-2 marchează întregul mecanism ca depășit; statutul de depășit l-a scos din foaia de parcurs, nu din teren, iar formularele redactate conform specificației XFA 3.3 sunt încă emise și încă valabile legal. XFA statică poate fi redusă la adnotări obișnuite de widget, iar HotPDF face asta când apelați ApplyXFAAsAcroForm, cu compromisurile tratate în aplatizarea formularelor XFA în câmpuri AcroForm. XFA dinamică este un cu totul alt animal: intervalele occur, textul care crește și scripturile calculate fac din mulțimea de câmpuri o funcție a datelor, deci nu există o listă fixă de adnotări în care să aplatizați până când utilizatorul nu a terminat de tastat. Exact golul acesta îl umple TXFAWidgetRuntime, ținând DOM-ul XFA viu, recalculând aranjamentul după fiecare editare acceptată și predând gazdei dumneavoastră un tablou plat de widgeturi poziționate de desenat și de hit-test
Ce predă runtime-ul unei aplicații gazdă?
Vă predă geometrie și stare, și nimic care să presupună un toolkit de interfață. TXFAWidgetRuntime expune WidgetCount și Widgets[I] ca înregistrări TXFAWidgetState care poartă ID, Name, Kind, PageIndex, Bounds în puncte PDF, Value, EditValue și indicatoarele Focused, Editing, ReadOnly, Valid, în timp ce desenarea, cursorul de text și rutarea tastaturii rămân în codul dumneavoastră. Identitatea widgetului este stabilă și ordinală: fiecare widget primește un ID de forma name[n], unde n numără aparițiile anterioare ale numelui de câmp în ordinea aranjamentului, astfel încât al doilea rând al unui subformular repetabil este amount[1]. Acea identitate este cea care supraviețuiește unei reconstrucții, și este limba comună a lui FocusWidget, BeginEdit, DispatchEvent și HitTest. Pentru un document deja deschis într-o instanță THotPDF, CreateLoadedXFAWidgetRuntime extrage pachetele XDP, ia prima casetă de pagină ca dimensiune a paginii de aranjament și returnează nil când fișierul nu poartă deloc XFA
var
Pdf: THotPDF;
Runtime: TXFAWidgetRuntime;
WidgetID: AnsiString;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('claim-dynamic.pdf');
Runtime := Pdf.CreateLoadedXFAWidgetRuntime; // nil când nu există /XFA
if Runtime = nil then
Exit;
try
for I := 0 to Runtime.WidgetCount - 1 do
Memo1.Lines.Add(Format('%s p%d [%.1f %.1f %.1f %.1f] = %s',
[string(Runtime.Widgets[I].ID), Runtime.Widgets[I].PageIndex,
Runtime.Widgets[I].Bounds.Left, Runtime.Widgets[I].Bounds.Top,
Runtime.Widgets[I].Bounds.Right, Runtime.Widgets[I].Bounds.Bottom,
string(Runtime.Widgets[I].Value)]));
// hit test în spațiul paginii, câștigă widgetul cel mai de sus
if Runtime.HitTest(0, 120.0, 96.0, WidgetID) then
Runtime.BeginEdit(WidgetID);
finally
Runtime.Free;
end;
finally
Pdf.Free;
end;
end;
Ce trebuie să fie atomic la confirmarea unui câmp?
Tot ce poate atinge editarea, ceea ce este considerabil mai mult decât valoarea câmpului. CommitEdit apelează CaptureSnapshot înainte să scrie ceva, iar acel snapshot acoperă patru lucruri: DOM-ul XFA serializat de la TXFADocument.SaveToBytes, întregul tablou de înregistrări de interacțiune TXFAWidgetState, contoarele LastCalculationPasses și LastReflowPasses, și Warnings.Count curent. Salvarea doar a valorilor nodurilor este scurtătura tentantă și este greșită, pentru că un script calculate sau o legare nerezolvată poate apela EnsureValueNode și materializa noduri de date care nu existau când a început editarea; o restaurare doar a valorilor nu are cum să le elimine, deci o editare respinsă ar lăsa reziduuri structurale permanente în pachetul datasets. Secvența de confirmare în sine este strictă — scrie valoarea candidat, rulează validate pentru câmpul editat, rulează calculate până la punct fix, apoi reflow până când aranjamentul este stabil — și orice eșec în orice etapă trece prin FailAndRestore, care reîncarcă octeții snapshot într-un TXFADocument proaspăt, reconstruiește lista de widgeturi, reaplică stările de interacțiune consemnate, resetează contoarele și taie Warnings înapoi la lungimea din snapshot. LastDiagnostic păstrează motivul la eșec, și păstrează literalul XFA transaction rollback failed în cazul patologic în care restaurarea însăși ridică o excepție
function EditAmount(Runtime: TXFAWidgetRuntime;
const AWidgetID: AnsiString; const AText: UnicodeString): Boolean;
var
Current: UnicodeString;
begin
Result := False;
if not Runtime.BeginEdit(AWidgetID) then
Exit; // doar în citire, sau widget inexistent
Current := Runtime.Widgets[Runtime.FocusedIndex].EditValue;
if not Runtime.ReplaceSelection(0, Length(Current), AText) then
begin
Runtime.CancelEdit; // interval greșit, sau surrogate scindat
Exit;
end;
Result := Runtime.CommitEdit; // totul sau nimic
if not Result then
// documentul, widgeturile, contoarele și avertismentele sunt deja
// înapoi la starea de dinainte de editare; widgetul cu focus este
// pur și simplu marcat invalid
ShowMessage(Runtime.LastDiagnostic);
end;
ReplaceSelection merită o mențiune separată, pentru că acolo respingerea datelor deformate este cea mai ieftină. Refuză o selecție care scindează o pereche surrogate UTF-16, refuză text de înlocuire care conține un surrogate înalt sau jos nepereche, și refuză orice rezultat mai lung de MaxValueChars. Prinderea asta la stratul de tastare înseamnă că mecanismul de tranzacție nu are niciodată de desfăcut un caracter de pe planul astral scris pe jumătate
Reconstruiți într-o listă privată, publicați printr-un singur swap
O reconstrucție de widgeturi nu trebuie să fie observabilă niciodată pe jumătate terminată, astfel încât RebuildWidgets construiește un TObjectList deținător complet separat și îl schimbă la loc printr-o singură atribuire la final. Motivul nu este estetic: TXFALayoutEngine.ComputeLayout rulează în timp ce reconstrucția este în zbor și apelează înapoi în codul gazdei prin funcția MeasureText pe care ați furnizat-o, și poate ridica EXFAWidgetRuntimeError când se atinge limita de widgeturi. Dacă runtime-ul și-ar muta lista vie în loc, oricare dintre căi ar lăsa gazda ținând o listă care este parțial aranjamentul vechi și parțial cel nou, cu pointeri DataNode către un document pe cale să fie anulat. Convergența reflow-ului este apoi decisă de LayoutSignature, un șir construit din numărul de widgeturi plus fiecare ID, indice de pagină și casetă delimitatoare rotunjită la patru zecimale: CommitEdit reconstruiește, compară semnăturile și repetă până când două semnături consecutive se potrivesc sau bugetul de treceri se epuizează. Când semnătura nu s-a schimbat deloc, LastReflowPasses rămâne 0, ceea ce vă spune o editare doar de valoare față de una care a crescut efectiv formularul, iar starea de interacțiune este purtată peste fiecare reconstrucție prin ID de widget, astfel încât focusul și editarea în curs supraviețuiesc inserării unui rând
De ce ar citi un câmp legat înregistrarea greșită?
Pentru că scriptul a rulat fără un context de date. Un câmp care poartă un <bind match="dataRef" ref="$record.actual"/> explicit și un câmp numit după același nod de date sunt două widgeturi diferite îndreptate spre o singură valoare, iar un subformular repetabil cu <occur max="2"/> produce mai multe widgeturi care împart un nume și diferă doar prin rândul de date căruia îi aparțin; evaluați validarea și calculul față de rădăcina documentului și fiecare dintre ele rezolvă this la primul nod potrivit din tot pachetul datasets, astfel încât rândul doi validează în tăcere rândul unu. HotPDF evită asta stocând DataNode rezolvat pe fiecare intrare de widget când aranjamentul îl produce, apoi trecând acel nod prin ambele apeluri HPDFXFAEvaluateFieldScript, atât pentru xfskValidate, cât și pentru xfskCalculate. Același context decide față de care nod creează EnsureValueNode când un calcul țintește o legare care nu există încă, iar când nicio legare nu poate fi rezolvată confirmarea eșuează curat cu XFA calculation target is not bound în loc să scrie în rândul greșit. Semantica FormCalc din spatele acelor scripturi face ecou ce primesc documentele AcroForm din acțiunile descrise în scripturile de format și calculate AcroForm, dar regulile de rezolvare de aici sunt cu scop XFA, nu cu scop de nume de câmp
Bugetele sunt verificate înainte de efectele secundare, nu după
Fiecare limită din runtime este o precondiție, pentru că un buget impus după ce alocarea s-a întâmplat deja nu este un buget. TXFAWidgetRuntimeOptions.Default livrează MaxWidgets la 10000, MaxValueChars la 1048576, MaxCalculationPasses la 16 și MaxReflowPasses la 4, iar TXFAFormScriptOptions implicite poartă MaxOperations la 100000 cu MaxElapsedMilliseconds la 500. Mai jos, DOM-ul XFA își aplică propriile TXFADOMLimits: plafoane de 128 MB pe intrarea și ieșirea decomprimată, cel mult 1024 de pachete cusute împreună, 1000000 de noduri și o adâncime de imbricare de 256. Două detalii contează mai mult decât numerele în sine. În primul rând, bugetele de script sunt la nivel de tranzacție, nu pe script: CommitEdit seamănă un singur contor de operații rămase și un termen limită monotonic, iar fiecare invocație validate și calculate consumă din același contor și primește doar milisecundele rămase, astfel încât un formular cu două sute de câmpuri calculatoare nu poate cheltui toți cei 500 ms de două sute de ori. În al doilea rând, termenul limită vine de la o funcție injectabilă MonotonicMilliseconds, ceea ce face comportamentul de timp scurs reproductibil într-o suită de teste în loc de o aruncare cu banul pe un agent de build ocupat
var
Options: TXFAWidgetRuntimeOptions;
Runtime: TXFAWidgetRuntime;
begin
Options := TXFAWidgetRuntimeOptions.Default;
Options.MaxWidgets := 2000; // implicit 10000
Options.MaxCalculationPasses := 8; // implicit 16
Options.MaxReflowPasses := 2; // implicit 4
Options.ScriptOptions.Limits.MaxOperations := 20000; // întreaga tranzacție
Options.ScriptOptions.Limits.MaxElapsedMilliseconds := 200;
Options.MeasureText :=
function(const AText: UnicodeString; const AFont: TXFAFontSpec;
AMaxWidth: Double): TXFATextExtent
begin
Result := MeasureWithHostCanvas(AText, AFont, AMaxWidth);
end;
Runtime := TXFAWidgetRuntime.Create(XDPBytes, 612, 792, Options);
try
Runtime.OnLayoutChanged :=
procedure
begin
RepaintAllPages; // declanșat doar când reflow a mutat efectiv widgeturi
end;
// ... conduc formularul ...
finally
Runtime.Free;
end;
end;
Unde se oprește runtime-ul și de ce spune asta cu voce tare
Runtime-ul nu este, în mod deliberat, un motor de scripting XFA general. DispatchEvent tratează nativ activitățile enter și exit mutând focusul, iar pentru orice altă activitate care poartă un script refuză cu un diagnostic specific și stabil în loc să se prefacă: scripturile care menționează addInstance, removeInstance sau instanceManager returnează XFA runtime does not support event-driven instance mutation, scripturile care ating .presence returnează echivalentul pentru presence, iar orice altceva returnează XFA runtime does not support this event script. Un refuz previzibil pe care puteți ramifica bate o emulare parțială care merge pe fișierul dumneavoastră exemplu și diverge pe al clientului
Modelul de fire este la fel de franc: o instanță de runtime aparține unui singur fir, fără blocare internă, pentru că motorul de aranjament ajunge înapoi în callback-urile de măsurare ale gazdei, iar o blocare în jurul acestora este un deadlock care așteaptă o repictare. Conținutul rich din interiorul câmpurilor urmează aceeași linie conservatoare ca în restul bibliotecii, unde încărcăturile exData sunt tratate cum este descris în textul rich XFA exData și hyperlinkuri, iar widgeturile de semnătură și buton revin ca ReadOnly, în timp ce tipurile de UI nesuportate apar ca xwkUnsupported în loc de o casetă de text editabilă care pierde date în tăcere
Puse cap la cap, acestea sunt un răspuns funcțional pentru XFA dinamic în Delphi: țineți DOM-ul viu, faceți din fiecare editare o tranzacție care fie aterizează complet, fie nu lasă nimic în urmă, plafonați fiecare trecere și fiți explicit în privința a ceea ce este în afara scopului. Dacă evaluați asta pentru un flux de despăgubiri, impozite sau prestații, runtime-ul XFA vine ca parte a componentei HotPDF Delphi PDF, alături de căile AcroForm, aplatizare și randare de care acele proiecte ajung de obicei să aibă nevoie împreună