Tehnični članak

Dinamični XFA izvajalnik v Delphiju: transakcije HotPDF

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

HotPDF obravnava potrditev polja XFA kot eno transakcijo in pred preverjanjem, izračunom in ponovno postavitvijo zajame serializirani DOM, stanja vseh gradnikov, števce ponovitev in števec opozoril, nato pa vse štiri skupaj objavi ali povrne
CommitEdit si pred pisanjem posname štiri vrste stanja, zato spodletelo preverjanje, izračun ali ponovna postavitev ne pusti strukturnega ostanka
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

Izvajalnik XFA v HotPDF obnovi seznam gradnikov v ločen lastniški seznam, medtem ko postavitev teče in kliče merilno kodo gostitelja, dokončani seznam pa objavi z enim prirejanjem, ki ga gostitelj ne more opaziti na pol opravljenega
Obnova poteka v zasebnem seznamu, ker lahko ComputeLayout sproži izjemo sredi dela, LayoutSignature pa odloča, kdaj sta se dve zaporedni ponovni postavitvi ustalili

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

Plasti proračunov v izvajalniku XFA HotPDF: od omejitev gradnikov in vrednosti čez omejitve operacij in časa skriptov do stropov XFA DOM, z enim števcem operacij in enim rokom, ki ju v transakciji si vsi klici
Proračuni skriptov veljajo za celotno transakcijo in ne za posamezen skript, zato ne more vsako od dveh sto izračunskih polj zahtevati svežih 500 ms
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