Műszaki cikk

Dinamikus XFA űrlapfutató Delphiben: HotPDF tranzakciók

A HotPDF dinamikus XFA űrlapokat tölt ki Delphiben a TXFAWidgetRuntime útján, egy gazdsemleges widget réteggel, amely minden mezőszerkesztést egyetlen tranzakcióként kezel: snapshot, validate, calculate, reflow, majd egészben kikerül vagy egésze visszagördül. Egyetlen szálú módon fut az Ön saját VCL vagy FMX gazdája belsejében, nem igényel telepített Acrobatot, és minden keretet léptet érvénybe, még mielőtt bármit allokálna

A forgatókönyv ismerős mindenki számára, aki szállított már dokumentumszoftvert államigazgatási vagy biztosítási munkára. Egy kárigénylő űrlap vagy adóbevallás PDF-ként érkezik, amelynek oldaltartalma egyetlen Please wait... if this message is not eventually replaced értesítés, és minden valódi mező egy XFA csomagban él, amelyet csak az Adobe Acrobat renderel. Az Ön felhasználói az Ön alkalmazásán belül akarják kitölteni. Raszterizálással sem jut ki ebből, mert az űrlap sorokat növeszt, ahogy az adat bekerül, és a harmadik sor utáni elrendezés nem az, amely a fájlban szállítva volt

Miért marad a dinamikus XFA megoldásra érdemes probléma

A dinamikus XFA kitart, mert a kihelyezett űrlapok túlélik a formátumot, amely hordozta őket. Az ISO 32000-1 §12.7.8 az XFA-t az AcroForm szótár /XFA bejegyzéseként írja le, amely XDP csomagstreamet tart, az ISO 32000-2 pedig elavulttá nyilvánítja az egész mechanizmust; az elavulás kivette az útitervből, nem a terepről, és az XFA 3.3 specifikáció ellen szerkesztett űrlapok ma is kiadásra kerülnek és ma is jogilag kötelezőek. A statikus XFA közönséges widget annotációkká redukálható, és ezt teszi a HotPDF, amikor meghívja az ApplyXFAAsAcroForm-ot, az XFA űrlapok AcroForm mezőkké lapításában tárgyalt kompromisszumokkal. A dinamikus XFA másfajta állat: az occur tartományai, a növeszthető szöveg és a calculate szkriptek a mezőkészletet az adat függvényévé teszik, így nincs rögzített annotációlista, amelyre lapítani lehetne, amíg a felhasználó be nem fejezte a gépelést. Ezt a rést tölti ki a TXFAWidgetRuntime, az XFA DOM élőben tartásával, minden elfogadott szerkesztés után újraszámolt elrendezéssel, és azzal, hogy a gazdának pozicionált widgetek lapos tömbjét adja rajzolásra és találat-tesztelésre

Mit ad át a futtatókörnyezet egy gazdaalkalmazásnak?

Geometriát és állapotot ad át, és semmit, ami UI eszközkészletet tételezne fel. A TXFAWidgetRuntime a WidgetCount-ot és a Widgets[I]-t TXFAWidgetState rekordokként tárja fel, amelyek ID-t, Name-t, Kind-t, PageIndex-t, PDF pontokban Bounds-t, Value-t, EditValue-t és a Focused, Editing, ReadOnly, Valid jelzőket hordozzák, miközben a festés, a kurzor rajzolása és a billentyűzet-útválasztás az Ön kódjában marad. A widget identitás stabil és sorszámú: minden widget name[n] alakú ID-t kap, ahol az n az adott mezőnév korábbi előfordulásait számolja elrendezési sorrendben, így egy ismétlődő subform második sora az amount[1]. Ez az identitás az, amely túléli az újjáépítést, és ez az, amelyen a FocusWidget, BeginEdit, DispatchEvent és HitTest mind beszélnek. Egy THotPDF példányban már nyitott dokumentumhoz a CreateLoadedXFAWidgetRuntime kinyeri az XDP csomagokat, az első oldalboxot veszi elrendezési oldalméretnek, és nil-t ad vissza, ha a fájl egyáltalán nem hordoz XFA-t

var
  Pdf: THotPDF;
  Runtime: TXFAWidgetRuntime;
  WidgetID: AnsiString;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('claim-dynamic.pdf');
    Runtime := Pdf.CreateLoadedXFAWidgetRuntime;   // nil, ha nincs /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)]));
      // oldal-térbeli találatteszt, a legfelső widget nyer
      if Runtime.HitTest(0, 120.0, 96.0, WidgetID) then
        Runtime.BeginEdit(WidgetID);
    finally
      Runtime.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

Minek kell atominak lennie, amikor egy mező jóváhagyásra kerül?

Minden, amit a szerkesztés érinthet, ami jóval több, mint a mező értéke. A CommitEdit meghívja a CaptureSnapshot-ot, mielőtt bármit ír, és az a snapshot négy dolgot fed le: a TXFADocument.SaveToBytes szerializált XFA DOM-ját, a TXFAWidgetState interakciós rekordok teljes tömbjét, a LastCalculationPasses és LastReflowPasses számlálókat, és a mostani Warnings.Count-ot. Csak a csomópontértékek mentése a kísértésbe ejtő rövidítés, és rossz, mert egy calculate szkript vagy egy fel nem oldott kötés meghívhatja az EnsureValueNode-ot, és anyagossá tehet olyan adatcsomópontokat, amelyek nem léteztek, amikor a szerkesztés elkezdődött; egy csak-érték helyreállításnak nincs módja azok eltávolítására, így egy elutasított szerkesztés maradandó szerkezeti maradványt hagyna a datasets csomagban. Maga a jóváhagyási sorrend szigorú — megírja a jelölt értéket, lefuttatja a szerkesztett mező validate-jét, calculate-t futtat fixpontig, majd újra tördeli, amíg az elrendezés stabil nem lesz — és bármely hiba bármely szakaszon a FailAndRestore-n át vezet, amely a snapshot bájtokat friss TXFADocument-be tölti, újjáépíti a widgetlistát, újra alkalmazza a rögzített interakciós állapotokat, visszaállítja a számlálókat, és a Warnings-et vissza metszi a snapshot hosszra. A LastDiagnostic hibánál az okot tartja, és a patológiás esetben, amikor maga a helyreállítás dob, a szó szerinti XFA transaction rollback failed-t tartja

A HotPDF az XFA mező jóváhagyását egyetlen tranzakcióként kezeli, rögzíti a szerializált DOM-ot, minden widget állapotát, a menetszámlálókat és a figyelmeztetésszámot, mielőtt validálna, számolna és újratördelne, majd mind a négyet együtt kiadja vagy visszaállítja
A CommitEdit négyféle állapotot rögzít, mielőtt bármit ír, így egy elbukó validate, calculate vagy reflow nem hagy maga után szerkezeti maradványt
function EditAmount(Runtime: TXFAWidgetRuntime;
  const AWidgetID: AnsiString; const AText: UnicodeString): Boolean;
var
  Current: UnicodeString;
begin
  Result := False;
  if not Runtime.BeginEdit(AWidgetID) then
    Exit;                                   // csak olvasható, vagy nincs ilyen widget
  Current := Runtime.Widgets[Runtime.FocusedIndex].EditValue;
  if not Runtime.ReplaceSelection(0, Length(Current), AText) then
  begin
    Runtime.CancelEdit;                     // rossz tartomány, vagy szétszakított surrogate
    Exit;
  end;
  Result := Runtime.CommitEdit;             // minden-vagy-semmi
  if not Result then
    // a dokumentum, a widgetek, a számlálók és a figyelmeztetések már vissza vannak
    // a szerkesztés előtti állapotba; a fókuszált widget egyszerűen érvénytelennek van jelölve
    ShowMessage(Runtime.LastDiagnostic);
end;

A ReplaceSelection megérdemli a saját jegyzetét, mert ott a legolcsóbb elutasítani a hibás bemenetet. Elutasítja azt a kijelölést, amely szétszakít egy UTF-16 surrogate párt, elutasítja azt a helyettesítő szöveget, amely páratlan magas vagy alacsony surrogate-et tartalmaz, és elutasít minden MaxValueChars-nál hosszabb eredményt. Ennek elkapása a billentyűleütés rétegében azt jelenti, hogy a tranzakciógépnek sosem kell kibontan egy félig megírt asztrális síkbeli karaktert

Újjáépítés privát listába, kiadás egyetlen cserével

Egy widget-újjáépítés sosem lehet megfigyelhető félig befejezettként, ezért a RebuildWidgets teljesen külön tulajdonló TObjectList-et épít, és a végén egyetlen hozzárendeléssel cseréli a helyére. Az ok nem esztétikai: a TXFALayoutEngine.ComputeLayout fut, miközben az újjáépítés levegőben van, és visszahív az Ön által szolgáltatott MeasureText függvényen keresztül a gazdakódba, és EXFAWidgetRuntimeError-t dobhat, amikor a widget-korlát eléri. Ha a futtatókörnyezet helyben változtatná az élő listáját, bármelyik útvonal azt hagyná, hogy a gazda olyan listát tartson, amely részben a régi, részben az új elrendezés, DataNode mutatókkal egy dokumentumba, amelyet épp visszagördenek. Az újratördelés konvergenciáját ezután a LayoutSignature dönti el, egy sztring, amelyet a widgetszámból plusz minden ID-ből, oldalindexből és négy tizedre kerekített határoló dobozból építenek: a CommitEdit újjáépít, összeveti az aláírásokat, és ismétel, amíg két egymást követő aláírás egyezik, vagy a menetkeret ki nem merül. Amikor az aláírás egyáltalán nem változott, a LastReflowPasses 0 marad, így különböztethető meg a csak-érték szerkesztés attól, amely ténylegesen növelte az űrlapot, és az interakciós állapot widget ID által vándorol át minden újjáépítésen, így a fókusz és a folyamatban lévő szerkesztés túléli a sor beszúrását

A HotPDF XFA futtatókörnyezete külön tulajdonló listába építi újjá a widgetlistáját, miközben az elrendezés fut és visszahív a gazda mérőkódjába, majd egyetlen hozzárendeléssel adja ki a befejezett listát, amelyet a gazda nem láthat félig késznek
Az újjáépítés privát listában történik, mert a ComputeLayout menet közben is dobhat, a LayoutSignature pedig eldönti, mikor konvergált két egymást követő újratördelés

Miért olvasna rossz rekordot egy kötött mező?

Mert a szkript adatkörnyezet nélkül futott. Egy mező, amely explicit <bind match="dataRef" ref="$record.actual"/>-et hordoz, és egy mező, amely ugyanarról az adatcsomópontról kapta a nevét, két különböző widget egyetlen értékre mutatva, és egy <occur max="2"/>-ös ismétlődő subform több widgetet termel, amelyek nevet osztanak meg, és csak abban térnek el, melyik adatsorhoz tartoznak; értékelje a validációt és a számítást a dokumentum gyökeréhez képest, és mindegyik a this-t az egész datasets csomag első egyező csomópontjára oldja fel, így a második sor csendben az első sort validálja. A HotPDF úgy kerüli el, hogy a feloldott DataNode-ot tárolja minden widget bejegyzésen, amikor az elrendezés előállítja, majd mindkét HPDFXFAEvaluateFieldScript híváson átfűzi azt a csomópontot, xfskValidate és xfskCalculate esetén egyaránt. Ugyanez a kontextus dönti el, mely csomóponthoz készít csomópontot az EnsureValueNode, amikor egy számítás olyan kötésre céloz, amely még nem létezik, és amikor nem oldható fel kötés, a jóváhagyás tisztán megbukik, XFA calculation target is not bound-dal, ahelyett hogy rossz sorba írna. E szkriptek mögötti FormCalc szemantika azt visszhangozza, amit az AcroForm dokumentumok kapnak a AcroForm formátum és calculate szkriptek című cikkben tárgyalt akciókból, de a feloldási szabályok itt XFA hatókörűek, nem mezőnév-hatókörűek

A kereteket mellékhatások előtt ellenőrzik, nem után

A futtatókörnyezet minden korlátja előfeltétel, mert egy olyan keret, amelyet az allokáció már megtörtént után léptetnek érvénybe, nem keret. A TXFAWidgetRuntimeOptions.Default MaxWidgets-et szállít 10000-en, MaxValueChars-t 1048576-on, MaxCalculationPasses-t 16-on és MaxReflowPasses-t 4-en, az alap TXFAFormScriptOptions pedig MaxOperations-t hordoz 100000-en, MaxElapsedMilliseconds-szal 500-on. Alatta az XFA DOM a saját TXFADOMLimits-ét lépteti érvénybe: 128 MB plafon a kicsomagolt bemenetre és kimenetre, legfeljebb 1024 egymásra fűzött csomag, 1000000 csomópont, és 256 beágyazási mélység. Két részlet fontosabb, mint maguk a számok. Először is a szkriptkeretek tranzakció-szélesek, nem szkriptenkéntiek: a CommitEdit egyetlen megmaradó-művelet számlálót és egyetlen monoton határidőt vet be, és minden validate és calculate hívás ugyanazt a számlálót meríti le, és csak a még megmaradt ezredmásodperceket kapja, így egy kétszáz számító mezős űrlap nem költheti el a teljes 500 ms-t kétszázszor. Másodszor a határidő egy befecskendezhető MonotonicMilliseconds függvényből jön, ami az, ami az eltelt-idő viselkedését reprodukálhatóvá teszi egy tesztkészletben, ahelyett hogy érmefeldobás lenne egy elfoglalt build agenten

Keretrétegek a HotPDF XFA futtatókörnyezetben, a widget- és értékkorlátoktól a szkript-művelet és időkorlátokon át az XFA DOM plafonokig, egyetlen műveletszámlálóval és egyetlen határidővel, amelyet a tranzakció minden hívása megoszt
A szkriptkeretek tranzakció-szélesek, nem szkriptenkéntiek, így kétszáz számító mező nem követelhet mindegyike friss 500 ms-t
var
  Options: TXFAWidgetRuntimeOptions;
  Runtime: TXFAWidgetRuntime;
begin
  Options := TXFAWidgetRuntimeOptions.Default;
  Options.MaxWidgets := 2000;                              // alap 10000
  Options.MaxCalculationPasses := 8;                       // alap 16
  Options.MaxReflowPasses := 2;                            // alap 4
  Options.ScriptOptions.Limits.MaxOperations := 20000;     // a teljes tranzakció
  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;   // csak akkor tüzel, amikor a reflow ténylegesen mozgott widgeteket
      end;
    // ... az űrlap vezetése ...
  finally
    Runtime.Free;
  end;
end;

Hol áll meg a futtatókörnyezet, és miért mondja ki hangosan

A futtatókörnyezet szándékosan nem általános XFA szkriptmotor. A DispatchEvent natívan kezeli az enter és exit tevékenységeket a fókusz mozgatásával, és minden más, szkriptet hordozó tevékenységnél konkrét, stabil diagnosztikával utasít el, nem színlel: az addInstance, removeInstance vagy instanceManager szót említő szkriptek XFA runtime does not support event-driven instance mutation-t adnak vissza, a .presence-t érintők a presence-megfelelőt, minden más pedig XFA runtime does not support this event script-et. Egy kiszámítható elutasítás, amelyre elágazhat, legyőz egy részleges emulációt, amely az Ön mintafájlján működik, és az ügyfélén tér szét

A szálmodell ugyanilyen szókimondó: egy futtatókörnyezet-példány egyetlen szálhoz tartozik, belső zárolás nélkül, mert az elrendezésmotor visszanyúl a gazda mérő-visszahívásaiba, és körülötte egy zárolás újrafestésre váró holtpont. A mezőkön belüli gazdag tartalom ugyanazt a konzervatív vonalat követi, mint a könyvtár többi része, ahol az exData tartalmakat úgy kezelik, ahogy az XFA exData rich text és hiperhivatkozások írja, az aláírás- és gombwidgetek ReadOnly-ként érkeznek vissza, a nem támogatott UI fajták pedig xwkUnsupported-ként kerülnek felszínre, nem pedig olyan szerkeszthető szövegdobozként, amely csendben adatot veszít

Egybevetve ez egy működőképes válasz a dinamikus XFA-ra Delphiben: tartsa élőben a DOM-ot, tegyen minden szerkesztést olyan tranzakcióvá, amely vagy teljesen leérkezik, vagy semmit nem hagy maga után, kösse korlátba minden menetet, és legyen explicit az abban, ami hatókörön kívül esik. Ha kárigénylési, adó- vagy juttatási munkafolyamathoz mérlegeli, az XFA futtatókörnyezet a HotPDF Delphi PDF komponens részeként szállít, az AcroForm, lapítás és renderelés útvonalak mellett, amelyeket ezek a projektek általában együtt végeznek szükségessé