HotPDF vyplňuje dynamické XFA formuláre v Delphi cez TXFAWidgetRuntime, vrstvu widgetov neutrálnu k hostiteľovi, ktorá berie každú úpravu poľa ako jednu transakciu: snímka, validácia, calculate, reflow, potom publikovanie alebo rollback celku. Beží jednovláknovo vo vašom vlastnom hostiteľovi VCL alebo FMX, nepotrebuje nainštalovaný Acrobat a vynucuje každý rozpočet skôr, než čokoľvek alokuje
Scenár pozná každý, kto dodal dokumentový softvér do štátnej správy alebo poisťovníctva. Prihláška škody alebo daňové priznanie prídu ako PDF, ktorého obsah stránky je jediné hlásenie „Please wait... if this message is not eventually replaced" a všetky skutočné polia žijú v XFA pakete, ktorý vykresľuje len Adobe Acrobat. Vaši používatelia ho chcú vyplniť vo vašej aplikácii. Nevyjdete ani rasterizáciou, lebo formulár rastie o riadky s každým zadaním a rozloženie po treťom riadku nie je rozloženie, ktoré prišlo v súbore
Prečo je dynamické XFA stále problém, ktorý stojí za riešenie
Dynamické XFA pretrváva, lebo nasadené formuláre prežijú formát, ktorý ich niesol. ISO 32000-1 §12.7.8 opisuje XFA ako položku /XFA na slovníku AcroForm držiacu stream XDP paketu a ISO 32000-2 celý mechanizmus označuje za zavrhnutý; zavrhnutie ho odstránilo z plánov, nie z terénu a formuláre písané proti špecifikácii XFA 3.3 sa stále vydávajú a stále sú právne záväzné. Statické XFA sa dá redukovať na obyčajné widgetové anotácie a HotPDF to robí, keď zavoláte ApplyXFAAsAcroForm, s kompromismi popísanými v článku splošťovanie XFA formulárov do polí AcroForm. Dynamické XFA je iná zver: jeho rozsahy occur, rastúci text a skripty calculate robia množinu polí funkciou dát, takže pevný zoznam anotácií, na ktorý by sa dalo sploštiť, neexistuje, pokým používateľ nedokončí písanie. Presne tú medzeru zapĺňa TXFAWidgetRuntime, ktorý drží XFA DOM nažive, po každej akceptovanej úprave prepočítava rozloženie a hostiteľovi podáva ploché pole umiestnených widgetov na kreslenie a zásahové testy
Čo runtime odovzdá hostiteľskej aplikácii?
Odovzdáva geometriu a stav a nič, čo by predpokladalo UI toolkit. TXFAWidgetRuntime vystavuje WidgetCount a Widgets[I] ako záznamy TXFAWidgetState nesúce ID, Name, Kind, PageIndex, Bounds v bodoch PDF, Value, EditValue a príznaky Focused, Editing, ReadOnly, Valid, kým maľovanie, kreslenie kurzora a smerovanie kláves zostáva vo vašom kóde. Identita widgetu je stabilná a poradová: každý widget dostane ID tvaru name[n], kde n počíta predchádzajúce výskyty daného mena poľa v poradí rozloženia, takže druhý riadok opakovaného subformu je amount[1]. Tá identita prežíva prestavbu a touto rečou hovoria FocusWidget, BeginEdit, DispatchEvent aj HitTest. Pre dokument už otvorený v inštancii THotPDF extrahuje CreateLoadedXFAWidgetRuntime XDP pakety, berie prvý page box ako veľkosť layoutovej strany a vracia nil, keď súbor XFA vôbec nenosí
var
Pdf: THotPDF;
Runtime: TXFAWidgetRuntime;
WidgetID: AnsiString;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('claim-dynamic.pdf');
Runtime := Pdf.CreateLoadedXFAWidgetRuntime; // nil, keď nie je žiadne /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 priestore strany, vyhráva najvrchnejší widget
if Runtime.HitTest(0, 120.0, 96.0, WidgetID) then
Runtime.BeginEdit(WidgetID);
finally
Runtime.Free;
end;
finally
Pdf.Free;
end;
end;
Čo musí byť atómové pri potvrdení poľa?
Všetko, čoho sa úprava môže dotknúť, a to je značne viac než hodnota poľa. CommitEdit volá CaptureSnapshot skôr, než čokoľvek zapíše, a tá snímka pokrýva štyri veci: serializovaný XFA DOM z TXFADocument.SaveToBytes, celé pole interakčných záznamov TXFAWidgetState, počítadlá LastCalculationPasses a LastReflowPasses a aktuálny Warnings.Count. Ukladať len hodnoty uzlov je lákavá skratka a je zlá, lebo skript calculate alebo nevyriešená väzba môže zavolať EnsureValueNode a hmotne vytvoriť dátové uzly, ktoré neexistovali, keď úprava začala; obnova len hodnôt ich nemá ako odstrániť, takže zamietnutá úprava by v pakete datasets nechala trvalý štrukturálny zvyšok. Samotná sekvencia commitu je prísna — zapísať kandidátnu hodnotu, spustiť validate pre upravené pole, dohnať calculate k pevnému bodu, potom reflow, kým sa rozloženie nestabilizuje — a každé zlyhanie na ľubovoľnom štádiu vedie cez FailAndRestore, ktoré načíta bajty snímky do čerstvého TXFADocument, prestavia zoznam widgetov, znovu aplikujú zaznamenané interakčné stavy, vynulujú počítadlá a skrátia Warnings späť na dĺžku snímky. LastDiagnostic drží príčinu pri zlyhaní a v patologickom prípade, keď samotná obnova vyvolá výnimku, drží doslovný text 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; // len na čítanie, alebo taký widget neexistuje
Current := Runtime.Widgets[Runtime.FocusedIndex].EditValue;
if not Runtime.ReplaceSelection(0, Length(Current), AText) then
begin
Runtime.CancelEdit; // zlý rozsah, alebo rozdelený surrogate
Exit;
end;
Result := Runtime.CommitEdit; // všetko alebo nič
if not Result then
// dokument, widgety, počítadlá a varovania sú už späť v stave
// pred úpravou; zameraný widget je len označený za neplatný
ShowMessage(Runtime.LastDiagnostic);
end;
ReplaceSelection si zaslúži vlastnú poznámku, lebo práve tu je zamietnutie znetvoreného vstupu najlacnejšie. Odmietne výber, ktorý rozdeľuje pár UTF-16 surrogátov, odmietne náhradný text s nespárovaným vysokým alebo nízkym surrogátom a odmietne akýkoľvek výsledok dlhší než MaxValueChars. Chytiť to na vrstve klávesy znamená, že transakčná mechanika nikdy nemusí odrolovať napolo zapísaný znak z astrálnej roviny
Prestavba do súkromného zoznamu, publikovanie jednou výmenou
Prestavba widgetov nesmie byť nikdy pozorovateľná v napolo hotovom stave, takže RebuildWidgets stavia úplne samostatný vlastniaci TObjectList a na konci ho jediným priradením vymení na miesto. Dôvod nie je estetika: TXFALayoutEngine.ComputeLayout beží, kým je prestavba v pohybe, a volá späť do hostiteľského kódu cez funkciu MeasureText, ktorú ste dodali, a môže vyvolať EXFAWidgetRuntimeError, keď sa zasiahne limit widgetov. Keby runtime menil svoj živý zoznam na mieste, každá z oboch ciest by nechala hostiteľa držať zoznam, ktorý je z časti staré a z časti nové rozloženie, s ukazovateľmi DataNode do dokumentu, ktorý sa má práve odrolovať. Konvergenciu reflow potom rozhoduje LayoutSignature, reťazec postavený z počtu widgetov plus každého ID, indexu strany a ohraničenia zaokrúhleného na štyri desatiny: CommitEdit prestavia, porovná podpisy a opakuje, kým sa dva po sebe idúce podpisy nezhodujú alebo sa nevyčerpá rozpočet priebehov. Keď sa podpis vôbec nezmenil, LastReflowPasses ostáva 0, čím rozoznáte úpravu len hodnoty od takej, ktorá formulár reálne zväčšila, a interakčný stav prenáša cez každú prestavbu widgetové ID, takže fokus a rozbehnutá editácia prežijú vloženie riadku
Prečo by väzbou viazané pole čítalo nesprávny záznam?
Pretože skript bežal bez dátového kontextu. Pole s explicitným <bind match="dataRef" ref="$record.actual"/> a pole pomenované po tom istom dátovom uzle sú dva rozdielne widgety ukazujúce na jednu hodnotu a opakovaný subform s <occur max="2"/> produkuje niekoľko widgetov zdieľajúcich meno, ktoré sa líšia len tým, ku ktorému dátovému riadku patria; vyhodnocovať validáciu a kalkuláciu proti koreňu dokumentu znamená, že každý z nich vyrieši this na prvý zodpovedajúci uzol v celom pakete datasets, takže riadok dva poticho validuje riadok jeden. HotPDF sa tomu vyhýba tak, že pri produkcii rozloženia ukladá vyriešený DataNode ku každému vstupu widgetu a potom tým uzlom pretkáva obidve volania HPDFXFAEvaluateFieldScript, pre xfskValidate aj xfskCalculate. Ten istý kontext rozhoduje, proti ktorému uzlu EnsureValueNode vytvorí chýbajúcu väzbu, a keď sa väzba nedá vyriešiť, commit zlyhá čisto s XFA calculation target is not bound namiesto zápisu do nesprávneho riadku. FormCalc sémantika za týmito skriptami odráža to, čo dokumenty AcroForm dostávajú z akcií popísaných v článku skripty formátu a kalkulácie AcroForm, ale pravidlá rozlíšovania sú tu rámcované na XFA, nie na meno poľa
Rozpočty sa kontrolujú pred vedľajšími efektmi, nie po nich
Každý limit v runtime je predpodmienka, lebo rozpočet vynútený po tom, čo sa alokácia stala, nie je rozpočet. TXFAWidgetRuntimeOptions.Default dodáva MaxWidgets 10000, MaxValueChars 1048576, MaxCalculationPasses 16 a MaxReflowPasses 4 a predvolené TXFAFormScriptOptions nesú MaxOperations 100000 s MaxElapsedMilliseconds 500. Pod tým aplikuje XFA DOM vlastné TXFADOMLimits: stropy 128 MB na dekomprimovaný vstup aj výstup, najviac 1024 spojených paketov, 1000000 uzlov a hĺbka zanorenia 256. Dva detaily záležia viac než čísla samé. Po prvé, skriptové rozpočty sú cez celú transakciu, nie na skript: CommitEdit zasiaje jediné počítadlo zostávajúcich operácií a jeden monotónny termín a každé volanie validate a calculate z neho čerpá a dostáva len milisekundy, ktoré ešte zostali, takže formulár s dvesto kalkulujúcimi poľami nemôže minúť celých 500 ms dvestokrát. Po druhé, termín prichádza z injektovateľnej funkcie MonotonicMilliseconds, čo robí časové správanie reprodukovateľné v testovacej sade namiesto hodu mincou na vytáčenom build agentovi
var
Options: TXFAWidgetRuntimeOptions;
Runtime: TXFAWidgetRuntime;
begin
Options := TXFAWidgetRuntimeOptions.Default;
Options.MaxWidgets := 2000; // predvolené 10000
Options.MaxCalculationPasses := 8; // predvolené 16
Options.MaxReflowPasses := 2; // predvolené 4
Options.ScriptOptions.Limits.MaxOperations := 20000; // celá transakcia
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; // spustené len keď reflow widgety skutočne pohnul
end;
// ... riadiť formulár ...
finally
Runtime.Free;
end;
end;
Kde runtime končí a prečo to hovorí nahlas
Runtime zámerne nie je všeobecný XFA skriptovací engine. DispatchEvent natívne obslúži aktivity enter a exit presunom fokusu a pre každú inú aktivitu so skriptom odmieta so špecifickou, stabilnou diagnózou namiesto predstierania: skripty spomínajúce addInstance, removeInstance alebo instanceManager vrátia XFA runtime does not support event-driven instance mutation, skripty dotýkajúce sa .presence vrátia prezenčný ekvivalent a čokoľvek iné vráti XFA runtime does not support this event script. Predvídateľné zamietnutie, podľa ktorého sa dá vetviť, porazí čiastočnú emuláciu, ktorá funguje na vašom vzorovom súbore a diverguje na súbore zákazníka
Model vlákien je rovnako úprimný: jedna inštancia runtime patrí jednému vláknu, bez vnútorných zámkov, lebo layout engine siaha späť do hostiteľských meracích callbackov a zámok okolo toho je deadlock čakajúci na prekreslenie. Bohatý obsah vnútri polí sleduje tú istú konzervatívnu líniu ako inde v knižnici, kde sa dátové bloky exData obsluhujú popisom v článku bohatý text a hypertextové odkazy XFA exData a widgety podpisov a tlačidiel sa vracajú ako ReadOnly, kým nepodporované druhy UI vystupujú ako xwkUnsupported, nie ako editovateľné textové pole, ktoré poticho stráca dáta
Súhrnne je to pracovná odpoveď na dynamické XFA v Delphi: držať DOM nažive, robiť z každej úpravy transakciu, ktorá buď dopadne celá, alebo nezanechá nič, ohraničiť každý priebeh a byť výrečný v tom, čo je mimo rozsahu. Ak toto vyhodnocujete pre workflow prihlášok, daní alebo dávok, XFA runtime dodáva ako súčasť komponentu HotPDF Delphi PDF, po boku ciest AcroForm, splošťovania a vykresľovania, ktoré také projekty zvyčajne nakoniec potrebujú spolu