HotPDF ispunjava dinamičke XFA obrasce u Delphiju kroz TXFAWidgetRuntime, sloj widgeta neovisan o domaćinu koji svaku izmjenu polja tretira kao jednu transakciju: snimka stanja, validacija, izračun, ponovni raspored, a zatim objava ili vraćanje u cijelosti. Radi u jednoj niti unutar vašeg vlastitog VCL ili FMX domaćina, ne traži instalirani Acrobat i svaki budžet nametne prije nego što išta alocira
Scenarij je poznat svakome tko je isporučivao softver za dokumente u državnu upravu ili osiguranje. Obrazac za prijavu štete ili poreznu prijavu stigne kao PDF čiji je sadržaj stranice jedna jedina obavijest „Please wait... if this message is not eventually replaced“, a sva stvarna polja žive u XFA paketu koji renderira samo Adobe Acrobat. Vaši korisnici žele ispuniti ga unutar vaše aplikacije. Izlaska nema ni rasterizacijom, jer obrazac dodaje redove kako se podaci unose, a raspored nakon trećeg retka nije raspored iz isporučene datoteke
Zašto je dinamički XFA i dalje problem vrijedan rješavanja
Dinamički XFA opstaje jer raspoređeni obrasci nadžive format koji ih je nosio. ISO 32000-1 §12.7.8 opisuje XFA kao /XFA unos u AcroForm rječniku koji drži tok XDP paketa, a ISO 32000-2 cijeli mehanizam označava kao zastario; ta oznaka uklonila ga je iz planova, ali ne iz terena, pa se obrasci pisani prema specifikaciji XFA 3.3 i dalje izdaju i i dalje pravno obvezuju. Statički XFA može se svesti na obične widget anotacije, i to HotPDF radi kada pozovete ApplyXFAAsAcroForm, uz kompromise obrađene u članku o pretvaranju XFA obrazaca u AcroForm polja. Dinamički XFA sasvim je druga priča: njegovi occur rasponi, tekst koji raste i calculate skripte čine skup polja funkcijom podataka, pa fiksni popis anotacija za pretvaranje ne postoji dok korisnik ne završi s tipkanjem. Upravo tu prazninu popunjava TXFAWidgetRuntime: drži XFA DOM živim, ponovno računa raspored nakon svake prihvaćene izmjene i vašem domaćinu predaje ravan niz pozicioniranih widgeta za crtanje i provjeru pogodaka
Što runtime predaje aplikaciji domaćinu?
Predaje vam geometriju i stanje, i ništa što pretpostavlja UI alatnik. TXFAWidgetRuntime izlaže WidgetCount i Widgets[I] kao zapise TXFAWidgetState koji nose ID, Name, Kind, PageIndex, Bounds u PDF točkama, Value, EditValue te zastavice Focused, Editing, ReadOnly i Valid, dok crtanje, ispis kursora i usmjeravanje tipkovnice ostaju u vašem kodu. Identitet widgeta stabilan je i redni: svaki widget dobiva ID oblika name[n], gdje n broji prijašnje pojave tog imena polja u redoslijedu rasporeda, pa je drugi redak ponavljajućeg subforma amount[1]. Taj identitet preživljava ponovnu izgradnju i njime govore FocusWidget, BeginEdit, DispatchEvent i HitTest. Za dokument već otvoren u instanci THotPDF, CreateLoadedXFAWidgetRuntime izvlači XDP pakete, uzima kutiju prve stranice kao veličinu stranice rasporeda i vraća nil kada datoteka uopće 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, pobjeđuje najgornji widget
if Runtime.HitTest(0, 120.0, 96.0, WidgetID) then
Runtime.BeginEdit(WidgetID);
finally
Runtime.Free;
end;
finally
Pdf.Free;
end;
end;
Što mora biti atomično kada se polje potvrđuje?
Sve što izmjena može dirnuti, a to je znatno više od vrijednosti polja. CommitEdit poziva CaptureSnapshot prije nego što išta zapiše, a ta snimka obuhvaća četiri stvari: serijalizirani XFA DOM iz TXFADocument.SaveToBytes, cijeli niz zapisa interakcije TXFAWidgetState, brojače LastCalculationPasses i LastReflowPasses te trenutni Warnings.Count. Spremiti samo vrijednosti čvorova primamljiv je prečac i pogrešan je, jer calculate skripta ili neriješeno povezivanje mogu pozvati EnsureValueNode i materijalizirati podatkovne čvorove koji nisu postojali kad je izmjena počela; vraćanje samo vrijednosti nema načina da ih ukloni, pa bi odbijena izmjena u paketu datasets ostavila trajni strukturni ostatak. Sami slijed potvrde strog je — zapiši predloženu vrijednost, provrti validate za uređivano polje, provrti calculate do fiksne točke, a zatim ponavljaj raspored dok se ne ustali — i svaki neuspjeh u bilo kojoj fazi ide kroz FailAndRestore, koji snimljene bajtove učitava u svježi TXFADocument, ponovno gradi popis widgeta, vraća zabilježena stanja interakcije, resetira brojače i skraćuje Warnings natrag na snimljenu duljinu. LastDiagnostic na neuspjehu drži razlog, a u patološkom slučaju kada podigne i samo vraćanje 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; // samo za čitanje, ili nema takvog widgeta
Current := Runtime.Widgets[Runtime.FocusedIndex].EditValue;
if not Runtime.ReplaceSelection(0, Length(Current), AText) then
begin
Runtime.CancelEdit; // neispravan raspon, ili razdvojen surrogat
Exit;
end;
Result := Runtime.CommitEdit; // sve ili ništa
if not Result then
// dokument, widgeti, brojači i upozorenja već su vraćeni u
// stanje prije izmjene; widget u fokusu samo je označen nevažećim
ShowMessage(Runtime.LastDiagnostic);
end;
ReplaceSelection zaslužuje vlastitu napomenu, jer je to mjesto gdje je neispravan ulaz najjeftinije odbiti. Odbija selekciju koja razdvaja UTF-16 surrogatni par, odbija zamjenski tekst koji sadrži neupareni visoki ili niski surrogat i odbija svaki rezultat dulji od MaxValueChars. Hvatanje toga na razini tipke znači da mehanizam transakcije nikada ne mora razmotavati napola zapisani znak iz astralne ravnine
Ponovna izgradnja u privatni popis, objava jednom zamjenom
Ponovna izgradnja widgeta nikada ne smije biti motriva u napola dovršenom stanju, pa RebuildWidgets gradi potpuno odvojeni vlasnički TObjectList i na kraju ga ugrađuje jednom jedinom dodjelom. Razlog nije estetski: TXFALayoutEngine.ComputeLayout radi dok je izgradnja u tijeku i poziva kod domaćina kroz funkciju MeasureText koju ste dostavili, a može podići EXFAWidgetRuntimeError kada se pogodi ograničenje widgeta. Da je runtime mijenjao svoj živi popis na licu mjesta, bilo koji od tih putova ostavio bi domaćina s popisom koji je djelomično stari, a djelomično novi raspored, s DataNode pokazivačima u dokument koji će uskoro biti vraćen. Konvergenciju ponovnog rasporeda zatim odlučuje LayoutSignature, niz sastavljen od broja widgeta plus svakog ID, indeksa stranice i okvirne kutije zaokružene na četiri decimale: CommitEdit gradi ponovno, uspoređuje potpise i ponavlja dok se dva uzastopna potpisa ne poklope ili dok se ne iscrpi budžet prolaza. Kada se potpis uopće nije mijenjao, LastReflowPasses ostaje 0, po čemu razlikujete izmjenu samo vrijednosti od one koja je stvarno proširila obrazac, a stanje interakcije nosi se kroz svaku izgradnju po widget ID-u, pa fokus i izmjena u tijeku prežive umetanje retka
Zašto bi povezano polje pročitalo pogrešan zapis?
Jer je skripta radila bez podatkovnog konteksta. Polje s izričitim <bind match="dataRef" ref="$record.actual"/> i polje imenovano po tom istom podatkovnom čvoru dva su različita widgeta usmjerena na jednu vrijednost, a ponavljajući subform s <occur max="2"/> proizvodi više widgeta koji dijele ime i razlikuju se samo u tome kojem retku podataka pripadaju; ako validaciju i izračun vrednujete od korijena dokumenta, svaki od njih riješit će this na prvi odgovarajući čvor u cijelom paketu datasets, pa drugi redak tiho validira prvi. HotPDF to izbjegava tako da prilikom izrade rasporeda na svaki unos widgeta pohrani riješeni DataNode, a zatim taj čvor provlači kroz oba poziva HPDFXFAEvaluateFieldScript, jednako za xfskValidate i xfskCalculate. Isti kontekst odlučuje na koji će čvor EnsureValueNode stvarati kada izračun cilja povezivanje koje još ne postoji, a kada se nijedno povezivanje ne može riješiti, potvrda čisto propada s XFA calculation target is not bound umjesto da piše u pogrešan redak. FormCalc semantika iza tih skripti odjekuje ono što AcroForm dokumenti dobivaju od akcija opisanih u članku AcroForm format i calculate skripte, ali se pravila rješavanja ovdje vežu uz XFA, a ne uz ime polja
Budžeti se provjeravaju prije nuspojava, a ne poslije
Svako ograničenje u runtimeu preduvjet je, jer budžet nametnut nakon što se alokacija već dogodila nije budžet. TXFAWidgetRuntimeOptions.Default isporučuje MaxWidgets na 10000, MaxValueChars na 1048576, MaxCalculationPasses na 16 i MaxReflowPasses na 4, a zadani TXFAFormScriptOptions nose MaxOperations na 100000 s MaxElapsedMilliseconds na 500. Ispod toga XFA DOM primjenjuje vlastite TXFADOMLimits: plafon od 128 MB na dekomprimirani ulaz i izlaz, najviše 1024 međusobno spojenih paketa, 1000000 čvorova i dubina ugnježđivanja od 256. Dvije pojedinosti važnije su od samih brojeva. Prvo, skriptni budžeti vrijede za cijelu transakciju, a ne po skripti: CommitEdit zasije jedan brojač preostalih operacija i jedan monotoni rok, a svaki poziv validate i calculate troši taj isti brojač i dobiva samo još preostale milisekunde, pa obrazac s dvjesto izračunskih polja ne može potrošiti punih 500 ms dvjesto puta. Drugo, rok dolazi iz ubacive funkcije MonotonicMilliseconds, čime je ponašanje po proteku vremena reproducibilno u testnom skupu umjesto bacanja novčića na zauzetom build agentu
var
Options: TXFAWidgetRuntimeOptions;
Runtime: TXFAWidgetRuntime;
begin
Options := TXFAWidgetRuntimeOptions.Default;
Options.MaxWidgets := 2000; // zadano 10000
Options.MaxCalculationPasses := 8; // zadano 16
Options.MaxReflowPasses := 2; // zadano 4
Options.ScriptOptions.Limits.MaxOperations := 20000; // cijela 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; // aktivira se samo kada reflow stvarno pomakne widgete
end;
// ... upravljaj obrascem ...
finally
Runtime.Free;
end;
end;
Gdje runtime staje i zašto to govori naglas
Runtime namjerno nije općeniti XFA skriptni motor. DispatchEvent izvorno rukuje aktivnostima enter i exit pomicanjem fokusa, a za svaku drugu aktivnost koja nosi skriptu odbija s konkretnom, stabilnom dijagnostikom umjesto da se pravi: skripte koje spominju addInstance, removeInstance ili instanceManager vraćaju XFA runtime does not support event-driven instance mutation, skripte koje diraju .presence vraćaju odgovarajuću poruku za presence, a sve ostalo vraća XFA runtime does not support this event script. Predvidljivo odbijanje na kojem možete granati bolje je od djelomične emulacije koja radi na vašoj oglednoj datoteci, a skreće na datoteci kupca
Model radnih niti jednako je direktan: jedna instanca runtimea pripada jednoj niti, bez internog zaključavanja, jer layout motor poseže natrag u povratne pozive za mjerenje kod domaćina, a brava oko toga deadlock je koji čeka repaint. Bogat sadržaj unutar polja slijedi istu konzervativnu liniju kao i ostatak knjižnice, gdje se exData paketi rukuju kako je opisano u članku XFA exData obogaćeni tekst i hiperpoveznice, a widgeti potpisa i gumba vraćaju se kao ReadOnly, dok nepodržane UI vrste iskaču kao xwkUnsupported umjesto kao tekstualna kutija za uređivanje koja tiho gubi podatke
Zbrojeno, to je izvodljiv odgovor na dinamički XFA u Delphiju: držite DOM živim, učinite svaku izmjenu transakcijom koja ili sasvim sjedne ili ne ostavi ništa, ograničite svaki prolaz i budite izričiti oko onoga što je izvan opsega. Ako ovo vrednujete za tokove rada oko prijava šteta, poreza ili socijalnih davanja, XFA runtime isporučuje se kao dio HotPDF Delphi PDF komponente, uz putanje za AcroForm, pretvaranje i renderiranje koje ti projekti obično na kraju trebaju zajedno