HotPDF izpolnjuje dinamične XFA obrazce v Delphiju prek TXFAWidgetRuntime, plast i gradnikov, ki je nevtralna do gostitelja in vsako urejanje polja obravnava kot eno transakcijo: posnetek stanja, preverjanje, izračun, ponovna postavitev, nato objava v celoti ali popolna povrnitev. Deluje na eni sami niti znotraj vašega gostitelja VCL ali FMX, ne potrebuje nameščenega Acrobata in vsak proračun uveljavi, preden kaj dodeli
Scenarij je znan vsakomur, ki je dokumentno programsko opremo dobavljal v državno upravo ali zavarovalništvo. Obrazec za škodni zahtevek ali davčno napoved pride kot PDF, katerega vsebina strani je eno samo obvestilo »Please wait... if this message is not eventually replaced«, vsa prava polja pa živijo v paketu XFA, ki ga izriše le Adobe Acrobat. Vaši uporabniki ga želijo izpolniti znotraj vaše aplikacije. Rešitve z rasterizacijo tudi ne pride v poštev, ker obrazec ob vnašanju podatkov dodaja vrstice, postavitev po tretji vrstici pa ni več postavitev, ki je prišla v datoteki
Zakaj se dinamični XFA še vedno splača reševati
Dinamični XFA preživlja, ker nameščeni obrazci preživijo format, ki jih je nosil. ISO 32000-1 §12.7.8 opisuje XFA kot vnos /XFA v slovarju AcroForm s tokom paketov XDP, ISO 32000-2 pa celoten mehanizem označuje za opuščenega; opustitev ga je odstranila z načrta, ne z terena, obrazci, napisani po specifikaciji XFA 3.3, pa se še vedno izdajajo in so še vedno pravno zavezujoči. Statični XFA se da zreducirati na navadne anotacije gradnikov, kar HotPDF stori, ko pokličete ApplyXFAAsAcroForm, s kompromisi, opisanimi v sploščevanju XFA obrazcev v polja AcroForm. Dinamični XFA je druga zver: njegovi razponi occur, rastoče besedilo in skripti calculate naredijo množico polj funkcijo podatkov, zato fiksiranega seznama anotacij, na katerega bi sploščili, ni, dokler uporabnik ne preneha tipkati. Prav to vrzel zapolnjuje TXFAWidgetRuntime: XFA DOM drži živega, po vsaki sprejeti spremembi znova izračuna postavitev in gostitelju izroči ravno tabelo pozicioniranih gradnikov za risanje in testiranje zadetkov
Kaj izvajalnik izroči aplikaciji gostitelja?
Izroči vam geometrijo in stanje, ničesar, kar bi predpostavljalo orodje za uporabniški vmesnik. TXFAWidgetRuntime izpostavi WidgetCount in Widgets[I] kot zapise TXFAWidgetState s polji ID, Name, Kind, PageIndex, Bounds v točkah PDF, Value, EditValue ter zastavicami Focused, Editing, ReadOnly in Valid, medtem ko ostajata risanje, izris kazalke in usmerjanje tipkovnice v vaši kodi. Identiteta gradnika je stabilna in zaporedna: vsak gradnik dobi ID oblike name[n], kjer n šteje predhodne pojavitve tistega imena polja v vrstnem redu postavitve, torej je druga vrstica ponavljajočega se podobrazca amount[1]. Prav ta identiteta preživi obnovo in prav nanjo se sklicujejo FocusWidget, BeginEdit, DispatchEvent in HitTest. Za dokument, že odprt v primerku THotPDF, CreateLoadedXFAWidgetRuntime izvleče pakete XDP, za velikost postavitvene strani vzame prvi okvir strani in vrne nil, kadar datoteka sploh 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, ko ni /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 v prostoru strani, zmaga zgornji gradnik
if Runtime.HitTest(0, 120.0, 96.0, WidgetID) then
Runtime.BeginEdit(WidgetID);
finally
Runtime.Free;
end;
finally
Pdf.Free;
end;
end;
Kaj mora biti atomarno, ko se polje potrdi?
Vse, česar se sprememba lahko dotakne, kar je precej več od vrednosti polja. CommitEdit pokliče CaptureSnapshot, preden karkoli zapiše, ta posnetek pa zajema štiri stvari: serializirani XFA DOM iz TXFADocument.SaveToBytes, celotno tabelo interakcijskih zapisov TXFAWidgetState, števca LastCalculationPasses in LastReflowPasses ter trenutni Warnings.Count. Shranjevanje samo vrednosti vozlišč je mamljiva bližnjica in je narobe, ker lahko skript calculate ali nerazrešena vezava pokliče EnsureValueNode in ustvari podatkovna vozlišča, ki ob začetku urejanja še niso obstajala; povrnitev samo vrednosti jih ne more odstraniti, zato bi zavrnjena sprememba v paketu datasets pustila trajni strukturni ostanek. Zaporedje potrditve samo je strogo — zapiše kandidatno vrednost, za urejeno polje požene validate, calculate izvaja do mirujoče točke, nato ponavlja postavitev, dokler ni stabilna — vsaka napaka na kateri koli stopnji pa gre čez FailAndRestore, ki posnetkove bajte znova naloži v svež TXFADocument, obnovi seznam gradnikov, ponovno uporabi zabeležena interakcijska stanja, ponastavi števce in Warnings skrajša na dolžino iz posnetka. LastDiagnostic ob napaki hrani razlog, v patološkem primeru, ko sama povrnitev sproži izjemo, pa dobesedno 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 branje, ali gradnika ni
Current := Runtime.Widgets[Runtime.FocusedIndex].EditValue;
if not Runtime.ReplaceSelection(0, Length(Current), AText) then
begin
Runtime.CancelEdit; // napačen razpon ali razcepljen surrogat
Exit;
end;
Result := Runtime.CommitEdit; // vse ali nič
if not Result then
// dokument, gradniki, števci in opozorila so že nazaj v stanju
// pred urejanjem; osredotočeni gradnik je le označen kot neveljaven
ShowMessage(Runtime.LastDiagnostic);
end;
ReplaceSelection zasluži lastno opombo, ker je to mesto, kjer je nepravilno oblikovan vhod najceneje zavrniti. Zavrne izbiro, ki razcepi par UTF-16 surrogatov, zavrne nadomestno besedilo z neparnim visokim ali nizkim surrogatom in zavrne vsak rezultat, daljši od MaxValueChars. Če to ujamemo na ravni pritiskov tipk, mehanizem transakcij nikoli ne razvija poludpisanega znaka iz astralne ravnine
Obnova v zasebni seznam, objava z enim prirejanjem
Obnova gradnikov nikoli ne sme biti opazna, ko je na pol končana, zato RebuildWidgets zgradi popolnoma ločen lastniški TObjectList in ga na koncu z enim samim prirejanjem zamenja na mestu. Razlog ni estetska: TXFALayoutEngine.ComputeLayout teče, medtem ko obnova še poteka, kliče nazaj v kodo gostitelja prek funkcije MeasureText, ki ste jo podali, in lahko sproži EXFAWidgetRuntimeError, ko se zadene omejitev števila gradnikov. Če bi izvajalnik svoj živi seznam spreminjal na mestu, bi katera koli od obeh poti pustila gostitelju seznam, ki je delno stara in delno nova postavitev, s kazalci DataNode v dokument, ki ga bo kmalu povrnil. Konvergenco ponovne postavitve nato odloča LayoutSignature, niz, zgrajen iz števila gradnikov ter vsakega ID, indeksa strani in omejevalnega okvirja, zaokroženih na štiri decimale: CommitEdit obnovi, primerja podpise in ponavlja, dokler se ne ujemata dva zaporedna podpisa ali dokler ni porabljen proračun ponovitev. Ko se podpis sploh ni spremenil, ostane LastReflowPasses 0, po čemer ločite urejanje samo vrednosti od tistega, ki je obrazec res zrasel, interakcijsko stanje pa preživi vsako obnovo prek ID-ja gradnika, tako da fokus in urejanje v teku preživita vstavljanje vrstice
Zakaj bi vezano polje prebralo napačen zapis?
Ker je skript tekel brez podatkovnega konteksta. Polje z izrazito vezavo <bind match="dataRef" ref="$record.actual"/> in polje, poimenovano po istem podatkovnem vozlišču, sta dva različna gradnika, usmerjena v isto vrednost, ponavljajoči se podobrazec s <occur max="2"/> pa naredi več gradnikov z istim imenom, ki se razlikujejo le po podatkovni vrstici, ki ji pripadajo; če preverjanje in izračun vrednotite proti korenu dokumenta, vsi razrešijo this na prvo ujemajoče se vozlišče v celotnem paketu datasets, zato druga vrstica tiho preverja prvo. HotPDF se temu izogne tako, da ob nastanku postavitve na vsak vnos gradnika shrani razrešeni DataNode, nato pa to vozlišče podaja v oba klica HPDFXFAEvaluateFieldScript, za xfskValidate in xfskCalculate enakovredno. Isti kontekst odloča, proti kateremu vozlišču EnsureValueNode ustvari novo, ko izračun cilja vezavo, ki še ne obstaja, kadar pa vezave ni mogoče razrešiti, potrditev čisto in brez stranskih učinkov odpove z XFA calculation target is not bound, namesto da bi pisala v napačno vrstico. Semantika FormCalc za temi skripti odmeva tisto, kar dokumenti AcroForm dobijo iz dejanj, opisanih v formatu in izračunskih skriptih AcroForm, pravila razreševanja pa so tu vezana na XFA in ne na ime polja
Proračuni se preverjajo pred stranskimi učinki, ne za njimi
Vsaka omejitev v izvajalniku je predpogoj, ker proračun, uveljavljen šele, ko je dodelitev že storjena, ni proračun. TXFAWidgetRuntimeOptions.Default dostavlja MaxWidgets na 10000, MaxValueChars na 1048576, MaxCalculationPasses na 16 in MaxReflowPasses na 4, privzete TXFAFormScriptOptions pa nosijo MaxOperations 100000 in MaxElapsedMilliseconds 500. Spodaj XFA DOM velja svoje TXFADOMLimits: zgornji meji 128 MB za dekompresiran vhod in izhod, največ 1024 sklenjenih paketov, 1000000 vozlišč in globina gnezdenja 256. Dve podrobnosti sta pomembnejši od številk samih. Prva: proračuni skriptov veljajo za celotno transakcijo in ne za posamezen skript — CommitEdit seje en sam števec preostalih operacij in en monotonski rok, vsak klic validate in calculate pa se obračunava na isti števec in dobi le še preostale milisekunde, zato obrazec z dvema sto poljema za izračun ne more polnih 500 ms porabiti dvjestokrat. Druga: rok prihaja iz vbrizgalne funkcije MonotonicMilliseconds, kar pomeni, da je časovno vedenje v testni zbirki ponovljivo in ne igra na srečo na zaposlenem strežniku za gradnjo
var
Options: TXFAWidgetRuntimeOptions;
Runtime: TXFAWidgetRuntime;
begin
Options := TXFAWidgetRuntimeOptions.Default;
Options.MaxWidgets := 2000; // privzeto 10000
Options.MaxCalculationPasses := 8; // privzeto 16
Options.MaxReflowPasses := 2; // privzeto 4
Options.ScriptOptions.Limits.MaxOperations := 20000; // celotna 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; // sproženo le, ko ponovna postavitev res premakne gradnike
end;
// ... upravljanje obrazca ...
finally
Runtime.Free;
end;
end;
Kje se izvajalnik ustavi in zakaj to glasno pove
Izvajalnik namenoma ni splošen skriptni motor za XFA. DispatchEvent dejavnost enter in exit obravnava izvorno s premikom fokusa, za vsako drugo dejavnost s skriptom pa se odkloni z natanko, stabilno diagnostiko, namesto da bi se pretvarjal: skripti, ki omenjajo addInstance, removeInstance ali instanceManager, vrnejo XFA runtime does not support event-driven instance mutation, skripti, ki se dotaknejo .presence, vrnejo ustrezno sporočilo o prisotnosti, vse ostalo pa vrne XFA runtime does not support this event script. Napovedljiva zavrnitev, na kateri lahko vejite, je vredna več kot delna emulacija, ki deluje na vaši vzorčni datoteki in se razbeži pri strankini
Model niti je enako prisopen: en primerek izvajalnika pripada eni niti, brez notranjega zaklepanja, ker se mehanizem postavitve vrača v merilne povratne klice gostitelja, zaklep okoli tega pa je zastoj, ki čaka na ponovni izris. Bogata vsebina znotraj polj sledi isti previdni črti kot drugod po knjižnici, kjer se tovorom exData ravna, kot je opisano v obogatenem besedilu in hiperpovezavah XFA exData, podpisni in gumbni gradniki pa se vrnejo kot ReadOnly, nepodprte vrste uporabniškega vmesnika pa pridejo na dan kot xwkUnsupported in ne kot uredljivo besedilno polje, ki tiho izgublja podatke
Skupaj je to delujoč odgovor na dinamični XFA v Delphiju: DOM naj ostane živ, vsako urejanje naj bo transakcija, ki ali pristane v celoti ali ne pusti ničesar za sabo, vsak prehod naj bo omejen, izven dosega pa naj bo izrecno povedano. Če to vrednotite za potek dela s škodnimi zahtevki, davki ali socialnimi dajstvi, izvajalnik XFA prihaja kot del komponente HotPDF Delphi PDF, skupaj s potmi AcroForm, sploščevanja in izrisa, ki jih ti projekti običajno na koncu potrebujejo skupaj