HotPDF popunjava dinamičke XFA formulare u Delphi-ju kroz TXFAWidgetRuntime, host-neutralni sloj vidžeta koji svaku izmenu polja tretira kao jednu transakciju: snapshot, validate, calculate, reflow, pa objavljivanje ili rollback celog. Radi u jednoj niti unutar vašeg sopstvenog VCL ili FMX domaćina, ne zahteva instaliran Acrobat i sprovodi svaki budžet pre nego što bilo šta alocira
Scenario je poznat svakome ko je isporučio dokumentacioni softver državnim ili osiguravajućim kućama. Prijava štete ili poreska prijava stiže kao PDF čiji je sadržaj stranice jedno obaveštenje „Sačekajte... ako se ova poruka na kraju ne zameni", a svako pravo polje živi u XFA paketu koji renderuje samo Adobe Acrobat. Vaši korisnici žele da ga popune unutar vaše aplikacije. Ne možete se izvući ni rasterizovanjem, jer formular raste redovima kako se podaci unose, a raspored posle trećeg reda nije raspored koji je stigao u fajlu
Zašto je dinamički XFA i dalje problem vredan rešavanja
Dinamički XFA opstaje jer raspoređeni formulari nadžive format koji ih je nosio. ISO 32000-1 §12.7.8 opisuje XFA kao /XFA unos u AcroForm rečniku koji drži XDP paket tok, a ISO 32000-2 ukida ceo mehanizam; ukidanje ga je skinulo sa puteva, ne sa terena, i formulari autirani prema XFA 3.3 specifikaciji i dalje se izdaju i pravno obavezuju. Statički XFA može se svesti na obične widget anotacije, i HotPDF to radi kada pozovete ApplyXFAAsAcroForm, sa kompromisima pokrivenim u prevođenju XFA formulara u AcroForm polja. Dinamički XFA je druga životinja: njegovi occur opsezi, tekst koji raste i calculate skripte čine skup polja funkcijom podataka, pa ne postoji fiksna lista anotacija na koju se može svesti dok korisnik ne završi kucanje. To je praznina koju TXFAWidgetRuntime popunjava, držeći XFA DOM živim, ponovo računajući raspored posle svake prihvaćene izmene i predajući vašem domaćinu ravan niz pozicioniranih vidžeta za crtanje i hit-test
Šta runtime predaje aplikaciji domaćinu?
Predaje vam geometriju i stanje, i ništa što pretpostavlja UI alat. TXFAWidgetRuntime izlaže WidgetCount i Widgets[I] kao TXFAWidgetState zapise koji nose ID, Name, Kind, PageIndex, Bounds u PDF tačkama, Value, EditValue i zastavice Focused, Editing, ReadOnly, Valid, dok crtanje, caret i rutiranje tastature ostaju u vašem kodu. Identitet vidžeta stabilan je i redni: svaki vidžet dobija ID oblika name[n], gde n broji prethodne pojave tog imena polja u redosledu rasporeda, pa drugi red ponavljajućeg subforma je amount[1]. Taj identitet preživi ponovnu izgradnju, i njime govore FocusWidget, BeginEdit, DispatchEvent i HitTest. Za dokument već otvoren u THotPDF instanci, CreateLoadedXFAWidgetRuntime izvlači XDP pakete, uzima prvi page box kao veličinu stranice rasporeda i vraća nil kada fajl uopšte ne nosi 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 kada nema /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 u prostoru stranice, najviši vidžet pobeđuje
if Runtime.HitTest(0, 120.0, 96.0, WidgetID) then
Runtime.BeginEdit(WidgetID);
finally
Runtime.Free;
end;
finally
Pdf.Free;
end;
end;
Šta mora biti atomično kada se polje potvrđuje?
Sve čega se izmena može dohvatis, što je znatno više od vrednosti polja. CommitEdit poziva CaptureSnapshot pre nego što išta upiše, i taj snapshot pokriva četiri stvari: serializovani XFA DOM iz TXFADocument.SaveToBytes, ceo niz TXFAWidgetState zapisa o interakciji, brojače LastCalculationPasses i LastReflowPasses, i trenutni Warnings.Count. Čuvanje samih vrednosti čvorova je primamljiva prečica i pogrešna je, jer calculate skripta ili nerešen binding mogu pozvati EnsureValueNode i materijalizovati čvorove podataka koji nisu postojali kad je izmena počela; obnova samo vrednosti nema načina da ih ukloni, pa bi odbijena izmena ostavila trajan strukturni ostatak u datasets paketu. Sekvenca potvrde sama je strog — upiši kandidat vrednost, izvrši validate za uređivano polje, izvrši calculate do fiksne tačke, pa reflow dok se raspored ne stabilizuje — i svaki neuspeh u bilo kom koraku ide kroz FailAndRestore, koji ponovo učitava snapshot bajtove u svež TXFADocument, obnavlja listu vidžeta, ponovo primenjuje zabeležena interakciona stanja, resetuje brojače i skraćuje Warnings na snapshot dužinu. LastDiagnostic drži razlog pri neuspehu, a u patološkom slučaju kada sama obnova baci izuzetak drži literal XFA transaction rollback failed
function EditAmount(Runtime: TXFAWidgetRuntime;
const AWidgetID: AnsiString; const AText: UnicodeString): Boolean;
var
Current: UnicodeString;
begin
Result := False;
if not Runtime.BeginEdit(AWidgetID) then
Exit; // read-only, ili nema takvog vidžeta
Current := Runtime.Widgets[Runtime.FocusedIndex].EditValue;
if not Runtime.ReplaceSelection(0, Length(Current), AText) then
begin
Runtime.CancelEdit; // loš opseg, ili podeljen surrogate
Exit;
end;
Result := Runtime.CommitEdit; // sve-ili-ništa
if not Result then
// dokument, vidžeti, brojači i upozorenja već su vraćeni u stanje
// pre izmene; fokusirani vidžet samo je označen kao nevalidan
ShowMessage(Runtime.LastDiagnostic);
end;
ReplaceSelection zaslužuje svoju belešku, jer je to mesto gde je deformisan ulaz najjeftinije odbaciti. Odbija selekciju koja preseca UTF-16 surrogate par, odbija zamenski tekst koji sadrži neuparen visoki ili niski surrogate, i odbija svaki rezultat duži od MaxValueChars. Hvatanje toga na sloju tastera znači da mašinerija transakcije nikad ne mora da razmotava upola napisan karakter sa astralnog plana
Gradi u privatnu listu, objavi jednim zamenom
Ponovna izgradnja vidžeta nikad ne sme biti vidljiva napola gotova, pa RebuildWidgets gradi potpuno odvojenu vlasničku TObjectList i zamenjuje je na mestu jednom dodelom na kraju. Razlog nije estetika: TXFALayoutEngine.ComputeLayout radi dok je izgradnja u toku i zove host kod kroz MeasureText funkciju koju ste dali, i može baciti EXFAWidgetRuntimeError kada se pogodi limit vidžeta. Da runtime menja živu listu na mestu, bilo koji od ta dva puta ostavio bi domaćina sa listom koja je delom stari, delom novi raspored, sa DataNode pokazivačima u dokument koji će biti vraćen. Konvergenciju reflow-a zatim odlučuje LayoutSignature, string građen iz broja vidžeta plus svakog ID, indeksa stranice i okvira zaokruženog na četiri decimale: CommitEdit gradi ponovo, poredi potpise i ponavlja dok se dva uzastopna potpisa ne poklope ili dok se budžet prolaza ne iscrpi. Kada se potpis nikad nije promenio, LastReflowPasses ostaje 0, čime razlikujete izmenu samo vrednosti od one koja je zaista porasla formular, a interakciono stanje prenosi se kroz svaku izgradnju po vidžet ID-u, pa fokus i izmena u toku prežive umetanje reda
Zašto bi vezano polje pročitalo pogrešan zapis?
Jer je skripta radila bez konteksta podataka. Polje koje nosi eksplicitno <bind match="dataRef" ref="$record.actual"/> i polje imenovano po tom istom podatkovnom čvoru dva su različita vidžeta usmerena na jednu vrednost, a ponavljajući subform sa <occur max="2"/> proizvodi više vidžeta koji dele ime i razlikuju se samo u tome kom redu podataka pripadaju; evaluacija validacije i računanja od korena dokumenta svakom od njih razrešava this u prvi odgovarajući čvor u celom datasets paketu, pa drugi red tiho validira prvi. HotPDF to izbegava čuvajući razrešeni DataNode na svakom unosu vidžeta kada ga raspored proizvede, pa taj čvor provlači kroz oba HPDFXFAEvaluateFieldScript poziva, za xfskValidate i xfskCalculate jednako. Isti kontekst odlučuje protiv kog čvora EnsureValueNode stvara kada računanje cilja binding koji još ne postoji, a kad se nijedan binding ne može razrešiti potvrda čisto pada sa XFA calculation target is not bound umesto da upiše u pogrešan red. FormCalc semantika iza tih skripti odjekuje ono što AcroForm dokumenti dobijaju od akcija opisanih u AcroForm format i calculate skriptama, ali pravila razrešavanja ovde su XFA opsega, a ne imena polja
Budžeti se proveravaju pre sporednih efekata, ne posle
Svaki limit u runtime-u je preduslov, jer budžet sproveden posle alociranja već učinjenog nije budžet. TXFAWidgetRuntimeOptions.Default isporučuje MaxWidgets na 10000, MaxValueChars na 1048576, MaxCalculationPasses na 16 i MaxReflowPasses na 4, a podrazumevane TXFAFormScriptOptions nose MaxOperations na 100000 sa MaxElapsedMilliseconds na 500. Ispod, XFA DOM primenjuje sopstvene TXFADOMLimits: 128 MB ograničenja na dekompresovan ulaz i izlaz, najviše 1024 paketa spojena, 1000000 čvorova i dubina ugnježđavanja 256. Dva detalja su važnija od samih brojeva. Prvo, skript budžeti važe za celu transakciju, a ne po skripti: CommitEdit seje jedan brojač preostalih operacija i jedan monotonski rok, i svaki validate i calculate poziv crtaju iz istog brojača i dobijaju samo milisekunde koje su još ostale, pa formular sa dvesta računajućih polja ne može potrošiti punih 500 ms dvesta puta. Drugo, rok dolazi iz injektovane MonotonicMilliseconds funkcije, što ponašanje proteklog vremena čini reprodukovanim u testnom paketu umesto bacanja novčića na zauzetom build agentu
var
Options: TXFAWidgetRuntimeOptions;
Runtime: TXFAWidgetRuntime;
begin
Options := TXFAWidgetRuntimeOptions.Default;
Options.MaxWidgets := 2000; // podrazumevano 10000
Options.MaxCalculationPasses := 8; // podrazumevano 16
Options.MaxReflowPasses := 2; // podrazumevano 4
Options.ScriptOptions.Limits.MaxOperations := 20000; // cela transakcija
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; // okida se samo kada reflow zaista pomeri vidžete
end;
// ... pokreći formular ...
finally
Runtime.Free;
end;
end;
Gde runtime staje, i zašto to kaže naglas
Runtime namerno nije opšti XFA skripting engine. DispatchEvent rodom obrađuje enter i exit aktivnosti pomeranjem fokusa, a za svaku drugu aktivnost koja nosi skriptu odbija sa specifičnom, stabilnom dijagnostikom umesto da se pretvara: skripte koje pominju addInstance, removeInstance ili instanceManager vraćaju XFA runtime does not support event-driven instance mutation, skripte koje dodiruju .presence vraćaju presence ekvivalent, a sve ostalo vraća XFA runtime does not support this event script. Predvidivo odbijanje na kojem možete granati bolje je od delimične emulacije koja radi na vašem oglednom fajlu i račva se na fajlu kupca
Model nitenja jednako je direktna: jedna runtime instanca pripada jednoj niti, bez unutrašnjeg zaključavanja, jer layout engine poseže nazad u host callback-ove merenja, a brava oko toga je deadlock koji čeka repaint. Bogat sadržaj unutar polja sledi istu konzervativnu liniju kao i drugde u biblioteci, gde se exData sadržaji obrađuju kao što je opisano u XFA exData rich text i hiperlinkovima, a signature i button vidžeti vraćaju se kao ReadOnly dok nepodržane UI vrste iskaču kao xwkUnsupported umesto kao editable text box koji tiho gubi podatke
Zajedno, to je upotrebljiv odgovor na dinamički XFA u Delphi-ju: držite DOM živim, činite svaku izmenu transakcijom koja ili dospeva celog sebe ili ne ostavlja ništa, ograničite svaki prolaz, i budite eksplicitni oko toga šta je van opsega. Ako ovo procenjujete za prijave šteta, poreske ili socijalne tokove rada, XFA runtime stiže kao deo HotPDF Delphi PDF component, uz AcroForm, flattening i renderovanje puteve koje ti projekti obično na kraju trebaju zajedno