HotPDF vyplňuje dynamické XFA formuláře v Delphi přes TXFAWidgetRuntime, vrstvu widgetů neutrální k hostu, která bere každou úpravu pole jako jednu transakci: snapshot, validate, calculate, reflow, pak publikace nebo vrácení celku. Běží jednovlákně uvnitř vašeho vlastního hosta VCL nebo FMX, nevyžaduje nainstalovaný Acrobat a vymáhá každý rozpočet dřív, než cokoli alokuje
Scénář zná každý, kdo expedoval dokumentní software do státní správy nebo pojišťovnictví. Přihláška škody nebo daňové přiznání přijde jako PDF, jehož obsah stránky je jediné oznámení „Please wait... if this message is not eventually replaced", a každé skutečné pole žije v paketu XFA, který vykresluje jen Adobe Acrobat. Vaši uživatelé ho chtějí vyplňovat uvnitř vaší aplikace. Ani rasterizací se z toho nedostanete, protože formulář roste o řádky podle zadávaných dat a rozvržení po třetím řádku není rozvržení, které v souboru dorazilo
Proč je dynamické XFA pořád problém za vyřešení
Dynamické XFA přetrvává, protože nasazené formuláře přežijí formát, který je nesl. ISO 32000-1 §12.7.8 popisuje XFA jako položku /XFA ve slovníku AcroForm držící XDP paketový proud a ISO 32000-2 celý mechanismus označuje za zastaralý; deprecace ho sundala z plánu, ne z terénu, a formuláře psané proti specifikaci XFA 3.3 se stále vydávají a jsou stále právně závazné. Statické XFA lze zredukovat na obyčejné widgetové anotace a HotPDF to udělá, zavoláte-li ApplyXFAAsAcroForm, s kompromisy popsanými v zploštění XFA formulářů do polí AcroForm. Dynamické XFA je jiná bestie: jeho rozsahy occur, rostoucí text a skripty calculate dělají ze sady polí funkci dat, takže pevný seznam anotací, na který by se dalo zploštit, neexistuje, dokud uživatel nedopíše. Tu mezeru zaplňuje TXFAWidgetRuntime, který drží XFA DOM naživu, přepočítává rozvržení po každé přijaté úpravě a hostu podává ploché pole umístěných widgetů k vykreslení a hit-testu
Co runtime předává hostitelské aplikaci?
Předává geometrii a stav a nic, co by předpokládalo UI toolkit. TXFAWidgetRuntime vystavuje WidgetCount a Widgets[I] jako záznamy TXFAWidgetState nesoucí ID, Name, Kind, PageIndex, Bounds v PDF bodech, Value, EditValue a příznaky Focused, Editing, ReadOnly, Valid, zatímco malování, kreslení caretu a směrování klávesnice zůstávají ve vašem kódu. Identita widgetu je stabilní a pořadová: každý widget dostane ID ve tvaru name[n], kde n počítá předchozí výskyty toho pole v pořadí rozvržení, takže druhý řádek opakovaného subformu je amount[1]. Ta identita je to, co přežije rebuild, a tím mluví FocusWidget, BeginEdit, DispatchEvent i HitTest. Pro dokument už otevřený v instanci THotPDF extrahuje CreateLoadedXFAWidgetRuntime XDP pakety, bere první page box jako velikost stránky rozvržení a vrací nil, když soubor nenese žádné 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, když chybí /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 stránky, vítězí nejvrchnější widget
if Runtime.HitTest(0, 120.0, 96.0, WidgetID) then
Runtime.BeginEdit(WidgetID);
finally
Runtime.Free;
end;
finally
Pdf.Free;
end;
end;
Co musí být atomické, když se pole zapisuje?
Všechno, čeho se úprava může dotknout, což je značně víc než hodnota pole. CommitEdit volá CaptureSnapshot, než cokoli zapíše, a ten snapshot pokrývá čtyři věci: serializovaný XFA DOM z TXFADocument.SaveToBytes, celé pole interakčních záznamů TXFAWidgetState, počítadla LastCalculationPasses a LastReflowPasses a aktuální Warnings.Count. Ukládat samotné hodnoty uzlů je lákavá zkratka a je to špatně, protože skript calculate nebo nevyřešená vazba může zavolat EnsureValueNode a hmotnit datové uzly, které v době začátku úpravy neexistovaly; obnova jen hodnot je nemá jak odstranit, takže zamítnutá úprava by zanechala trvalý strukturální sediment v paketu datasets. Sama sekvence zápisu je přísná — zapíše kandidátní hodnotu, spustí validate pro upravené pole, spustí calculate k pevnému bodu, pak reflow, dokud rozvržení není stabilní — a jakékoli selhání v jakékoli fázi míří přes FailAndRestore, které nahraje snapshot bajty do čerstvého TXFADocument, znovu postaví seznam widgetů, znovu aplikuje zaznamenané interakční stavy, vynuluje počítadla a zkrátí Warnings zpět na délku snapshotu. LastDiagnostic drží důvod při selhání a drží literál XFA transaction rollback failed v patologickém případě, kdy samotná obnova vyhodí výjimku
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, nebo žádný takový widget
Current := Runtime.Widgets[Runtime.FocusedIndex].EditValue;
if not Runtime.ReplaceSelection(0, Length(Current), AText) then
begin
Runtime.CancelEdit; // špatný rozsah, nebo rozdělený surrogate
Exit;
end;
Result := Runtime.CommitEdit; // všechno nebo nic
if not Result then
// dokument, widgety, počítadla a varování jsou už zpět v
// stavu před úpravou; zaměřený widget je prostě označen jako nevalidní
ShowMessage(Runtime.LastDiagnostic);
end;
ReplaceSelection si zaslouží vlastní poznámku, protože právě tady se špatný vstup odmítá nejlevněji. Odmítá výběr, který rozdělí dvojici UTF-16 surrogate, odmítá náhradní text obsahující nepárový vysoký nebo nízký surrogate a odmítá jakýkoli výsledek delší než MaxValueChars. Chytit to ve vrstvě stisků kláves znamená, že transakční mašinérie už nemusí rozebírat napůl zapsaný znak astrální roviny
Rebuild do soukromého seznamu, publikace jediným swapem
Rebuild widgetů nesmí být nikdy pozorovatelný napůl hotový, takže RebuildWidgets staví zcela samostatný vlastnící TObjectList a vkládá ho na místo jediným přiřazením na konci. Důvod není estetika: TXFALayoutEngine.ComputeLayout běží, zatímco rebuild letí, a volá zpět do kódu hosta přes funkci MeasureText, kterou jste dodali, a může vyhodit EXFAWidgetRuntimeError, když se narazí na limit widgetů. Kdyby runtime měnil svůj živý seznam na místě, nechala by každá z cest hosta držet seznam, který je z části starým a z části novým rozvržením, s ukazateli DataNode do dokumentu, který se zrovna chystá k rollbacku. Konvergence reflow se pak rozhoduje podle LayoutSignature, řetězce složeného z počtu widgetů plus každé ID, page indexu a ohraničujícího rámečku zaokrouhleného na čtyři desetinná místa: CommitEdit rebuilduje, porovnává signatury a opakuje, dokud se dvě po sobě jdoucí signatury nevyrovnají nebo dokud se nevyčerpá rozpočet průchodů. Když se signatura nikdy nezměnila, zůstává LastReflowPasses 0, což je způsob, jak rozeznat úpravu jen hodnoty od úpravy, která formulář skutečně zvětšila, a interakční stav přechází každým rebuildem podle widget ID, takže fokus a rozjetá editace přežijí vložení řádku
Proč by pole s vazbou četlo špatný záznam?
Protože skript běžel bez datového kontextu. Pole nesoucí explicitní <bind match="dataRef" ref="$record.actual"/> a pole pojmenované po tomtéž datovém uzlu jsou dva různé widgety mířící na jednu hodnotu a opakovaný subform s <occur max="2"/> vyprodukuje několik widgetů sdílejících jméno a lišících se jen tím, ke kterému datovému řádku patří; hodnoťte-li validaci a kalkulaci proti kořenu dokumentu, rozezná si každý z nich this na první odpovídající uzel celého paketu datasets, takže řádek dva tiše validuje řádek jedna. HotPDF tomu brání tím, že ukládá vyřešený DataNode do každé položky widgetu, jakmile ho vyprodukuje rozvržení, a protahuje ten uzel oběma voláními HPDFXFAEvaluateFieldScript, pro xfskValidate i xfskCalculate. Týž kontext rozhoduje, proti kterému uzlu EnsureValueNode vytvoří, když kalkulace míří na vazbu, která ještě neexistuje, a když se vazba nedá vyřešit, zápis selže čistě s XFA calculation target is not bound místo zápisu do špatného řádku. Sémantika FormCalc za těmito skripty zrcadlí, co AcroForm dokumenty dostávají z akcí popsaných v skriptech AcroForm format a calculate, ale pravidla rozlišování tady jsou scopovaná podle XFA, ne podle názvu pole
Rozpočty se kontrolují před vedlejšími účinky, ne po nich
Každý limit v runtime je podmínka, protože rozpočet vymáhaný poté, co alokace už proběhla, není rozpočet. TXFAWidgetRuntimeOptions.Default expeduje MaxWidgets na 10000, MaxValueChars na 1048576, MaxCalculationPasses na 16 a MaxReflowPasses na 4 a výchozí TXFAFormScriptOptions nesou MaxOperations na 100000 s MaxElapsedMilliseconds na 500. Pod tím si XFA DOM vymáhá vlastní TXFADOMLimits: stropy 128 MB na dekomprimovaný vstup i výstup, nejvýše 1024 paketů sešitých dohromady, 1000000 uzlů a hloubka zanoření 256. Dva detaily mají větší váhu než samotná čísla. Prvně, rozpočty skriptů jsou celotransakční, ne na skript: CommitEdit zasetuje jedno počítadlo zbývajících operací a jeden monotónní deadline a každé volání validate a calculate z toho téhož počítadla čerpá a dostává jen zbývající milisekundy, takže formulář se dvěma sty kalkulujícími poli nemůže utratit plných 500 ms dvakrát stokrát. Za druhé, deadline přichází z vstřikovatelné funkce MonotonicMilliseconds, což dělá chování uplynulého času reprodukovatelným v testovací sadě místo hodu mincí na zaneprázdněném build agentovi
var
Options: TXFAWidgetRuntimeOptions;
Runtime: TXFAWidgetRuntime;
begin
Options := TXFAWidgetRuntimeOptions.Default;
Options.MaxWidgets := 2000; // výchozí 10000
Options.MaxCalculationPasses := 8; // výchozí 16
Options.MaxReflowPasses := 2; // výchozí 4
Options.ScriptOptions.Limits.MaxOperations := 20000; // celá transakce
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; // spuštěno jen když reflow widgety skutečně pohnul
end;
// ... ovládání formuláře ...
finally
Runtime.Free;
end;
end;
Kde runtime končí a proč to říká nahlas
Runtime záměrně není všeobecný XFA skriptovací engine. DispatchEvent zpracovává aktivity enter a exit nativně přesunutím fokusu a u každé jiné aktivity nesoucí skript odmítá s konkrétní, stabilní diagnostikou místo předstírání: skripty zmiňující addInstance, removeInstance nebo instanceManager vrací XFA runtime does not support event-driven instance mutation, skripty sající na .presence vrací presence ekvivalent a cokoli jiného vrací XFA runtime does not support this event script. Předvídatelné odmítnutí, na kterém se dá větvit, bije částečnou emulaci, která funguje na vašem vzorku a vybočí u zákazníka
Model vláken je stejně přímočarý: jedna instance runtime patří jednomu vláknu, bez vnitřních zámků, protože rozvrhovací engine sahá zpět do měřicích callbacků hosta a zámek kolem toho je deadlock čekající na překreslení. Rich obsah uvnitř polí jde konzervativní linií jako jinde v knihovně, kde se payloady exData zpracovávají, jak popisuje XFA exData rich text a hyperodkazy, a widgety podpisu a tlačítek přicházejí zpět jako ReadOnly zatímco nepodporované druhy UI povrchují jako xwkUnsupported místo editovatelného textového pole, které tiše ztrácí data
Dohromady je to použitelná odpověď na dynamické XFA v Delphi: držte DOM naživu, udělejte z každé úpravy transakci, která buď přistane celá, nebo nezanechá nic, ohraničte každý průchod a buďte explicitní ohledně toho, co je mimo záběr. Pokud toto hodnotíte pro workflow přihlášek, daní nebo dávek, XFA runtime putuje jako součást komponenty HotPDF Delphi PDF spolu s cestami AcroForm, zplošťování a vykreslování, které tyto projekty obvykle nakonec potřebují společně