Odborný článok

Runtime dynamických XFA formulárov v Delphi: HotPDF

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

HotPDF berie commit poľa XFA ako jednu transakciu, zachytáva serializovaný DOM, každý stav widgetu, počítadlá priebehov a počet varovaní skôr, než validuje, kalkuluje a reflovwuje, potom publikuje alebo obnoví všetky štyri veci spolu
CommitEdit robí snímku štyroch druhov stavu skôr, než čokoľvek zapíše, takže zlyhaná validácia, calculate alebo reflow nezanecháva žiadny štrukturálny zvyšok
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

XFA runtime HotPDF prestavia zoznam widgetov do samostatného vlastniaceho zoznamu, kým beží rozloženie a volá späť do hostiteľského meracieho kódu, potom publikuje hotový zoznam jediným priradením, ktoré hostiteľ nemôže pozorovať v napolo hotovom stave
Prestavba sa deje v súkromnom zozname, lebo ComputeLayout môže v polovici letu vyvolať výnimku, a LayoutSignature rozhoduje, kedy sa dva po sebe idúce reflowy zkonvergovali

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

Vrstvy rozpočtov v XFA runtime HotPDF, od limitov widgetov a hodnôt cez limity operácií a času skriptov až po stropy XFA DOM, s jedným počítadlom operácií a jedným termínom zdieľaným každým volaním v transakcii
Skriptové rozpočty sú cez celú transakciu, nie na skript, takže dvesto kalkulujúcich polí si nemôže každé nárokovať čerstvých 500 ms
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