Tehnički članak

Dinamički XFA runtime u Delphi-ju: HotPDF transakcije

HotPDF popunjava dinamičke XFA formulare u Delphi-ju kroz TXFAWidgetRuntime, host-neutralni sloj vidžeta koji svaku izmenu polja tretira kao jednu transakciju: snapshot, validate, calculate, reflow, pa objavljivanje ili rollback celog. Radi u jednoj niti unutar vašeg sopstvenog VCL ili FMX domaćina, ne zahteva instaliran Acrobat i sprovodi svaki budžet pre nego što bilo šta alocira

Scenario je poznat svakome ko je isporučio dokumentacioni softver državnim ili osiguravajućim kućama. Prijava štete ili poreska prijava stiže kao PDF čiji je sadržaj stranice jedno obaveštenje „Sačekajte... ako se ova poruka na kraju ne zameni", a svako pravo polje živi u XFA paketu koji renderuje samo Adobe Acrobat. Vaši korisnici žele da ga popune unutar vaše aplikacije. Ne možete se izvući ni rasterizovanjem, jer formular raste redovima kako se podaci unose, a raspored posle trećeg reda nije raspored koji je stigao u fajlu

Zašto je dinamički XFA i dalje problem vredan rešavanja

Dinamički XFA opstaje jer raspoređeni formulari nadžive format koji ih je nosio. ISO 32000-1 §12.7.8 opisuje XFA kao /XFA unos u AcroForm rečniku koji drži XDP paket tok, a ISO 32000-2 ukida ceo mehanizam; ukidanje ga je skinulo sa puteva, ne sa terena, i formulari autirani prema XFA 3.3 specifikaciji i dalje se izdaju i pravno obavezuju. Statički XFA može se svesti na obične widget anotacije, i HotPDF to radi kada pozovete ApplyXFAAsAcroForm, sa kompromisima pokrivenim u prevođenju XFA formulara u AcroForm polja. Dinamički XFA je druga životinja: njegovi occur opsezi, tekst koji raste i calculate skripte čine skup polja funkcijom podataka, pa ne postoji fiksna lista anotacija na koju se može svesti dok korisnik ne završi kucanje. To je praznina koju TXFAWidgetRuntime popunjava, držeći XFA DOM živim, ponovo računajući raspored posle svake prihvaćene izmene i predajući vašem domaćinu ravan niz pozicioniranih vidžeta za crtanje i hit-test

Šta runtime predaje aplikaciji domaćinu?

Predaje vam geometriju i stanje, i ništa što pretpostavlja UI alat. TXFAWidgetRuntime izlaže WidgetCount i Widgets[I] kao TXFAWidgetState zapise koji nose ID, Name, Kind, PageIndex, Bounds u PDF tačkama, Value, EditValue i zastavice Focused, Editing, ReadOnly, Valid, dok crtanje, caret i rutiranje tastature ostaju u vašem kodu. Identitet vidžeta stabilan je i redni: svaki vidžet dobija ID oblika name[n], gde n broji prethodne pojave tog imena polja u redosledu rasporeda, pa drugi red ponavljajućeg subforma je amount[1]. Taj identitet preživi ponovnu izgradnju, i njime govore FocusWidget, BeginEdit, DispatchEvent i HitTest. Za dokument već otvoren u THotPDF instanci, CreateLoadedXFAWidgetRuntime izvlači XDP pakete, uzima prvi page box kao veličinu stranice rasporeda i vraća nil kada fajl uopšte 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 kada nema /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 u prostoru stranice, najviši vidžet pobeđuje
      if Runtime.HitTest(0, 120.0, 96.0, WidgetID) then
        Runtime.BeginEdit(WidgetID);
    finally
      Runtime.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

Šta mora biti atomično kada se polje potvrđuje?

Sve čega se izmena može dohvatis, što je znatno više od vrednosti polja. CommitEdit poziva CaptureSnapshot pre nego što išta upiše, i taj snapshot pokriva četiri stvari: serializovani XFA DOM iz TXFADocument.SaveToBytes, ceo niz TXFAWidgetState zapisa o interakciji, brojače LastCalculationPasses i LastReflowPasses, i trenutni Warnings.Count. Čuvanje samih vrednosti čvorova je primamljiva prečica i pogrešna je, jer calculate skripta ili nerešen binding mogu pozvati EnsureValueNode i materijalizovati čvorove podataka koji nisu postojali kad je izmena počela; obnova samo vrednosti nema načina da ih ukloni, pa bi odbijena izmena ostavila trajan strukturni ostatak u datasets paketu. Sekvenca potvrde sama je strog — upiši kandidat vrednost, izvrši validate za uređivano polje, izvrši calculate do fiksne tačke, pa reflow dok se raspored ne stabilizuje — i svaki neuspeh u bilo kom koraku ide kroz FailAndRestore, koji ponovo učitava snapshot bajtove u svež TXFADocument, obnavlja listu vidžeta, ponovo primenjuje zabeležena interakciona stanja, resetuje brojače i skraćuje Warnings na snapshot dužinu. LastDiagnostic drži razlog pri neuspehu, a u patološkom slučaju kada sama obnova baci izuzetak drži literal XFA transaction rollback failed

HotPDF tretira potvrdu XFA polja kao jednu transakciju, hvatajući serializovani DOM, svako stanje vidžeta, brojače prolaza i broj upozorenja pre validacije, računanja i reflow-a, pa objavljuje ili obnavlja sve četiri zajedno
CommitEdit snima četiri vrste stanja pre nego što išta upiše, pa neuspešan validate, calculate ili reflow ne ostavlja strukturni ostatak
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, ili nema takvog vidžeta
  Current := Runtime.Widgets[Runtime.FocusedIndex].EditValue;
  if not Runtime.ReplaceSelection(0, Length(Current), AText) then
  begin
    Runtime.CancelEdit;                     // loš opseg, ili podeljen surrogate
    Exit;
  end;
  Result := Runtime.CommitEdit;             // sve-ili-ništa
  if not Result then
    // dokument, vidžeti, brojači i upozorenja već su vraćeni u stanje
    // pre izmene; fokusirani vidžet samo je označen kao nevalidan
    ShowMessage(Runtime.LastDiagnostic);
end;

ReplaceSelection zaslužuje svoju belešku, jer je to mesto gde je deformisan ulaz najjeftinije odbaciti. Odbija selekciju koja preseca UTF-16 surrogate par, odbija zamenski tekst koji sadrži neuparen visoki ili niski surrogate, i odbija svaki rezultat duži od MaxValueChars. Hvatanje toga na sloju tastera znači da mašinerija transakcije nikad ne mora da razmotava upola napisan karakter sa astralnog plana

Gradi u privatnu listu, objavi jednim zamenom

Ponovna izgradnja vidžeta nikad ne sme biti vidljiva napola gotova, pa RebuildWidgets gradi potpuno odvojenu vlasničku TObjectList i zamenjuje je na mestu jednom dodelom na kraju. Razlog nije estetika: TXFALayoutEngine.ComputeLayout radi dok je izgradnja u toku i zove host kod kroz MeasureText funkciju koju ste dali, i može baciti EXFAWidgetRuntimeError kada se pogodi limit vidžeta. Da runtime menja živu listu na mestu, bilo koji od ta dva puta ostavio bi domaćina sa listom koja je delom stari, delom novi raspored, sa DataNode pokazivačima u dokument koji će biti vraćen. Konvergenciju reflow-a zatim odlučuje LayoutSignature, string građen iz broja vidžeta plus svakog ID, indeksa stranice i okvira zaokruženog na četiri decimale: CommitEdit gradi ponovo, poredi potpise i ponavlja dok se dva uzastopna potpisa ne poklope ili dok se budžet prolaza ne iscrpi. Kada se potpis nikad nije promenio, LastReflowPasses ostaje 0, čime razlikujete izmenu samo vrednosti od one koja je zaista porasla formular, a interakciono stanje prenosi se kroz svaku izgradnju po vidžet ID-u, pa fokus i izmena u toku prežive umetanje reda

HotPDF XFA runtime gradi svoju listu vidžeta u odvojenu vlasničku listu dok raspored radi i zove host merenje koda, pa objavljuje gotovu listu jednom dodelom koju domaćin ne može videti napola gotovu
Izgradnja se dešava u privatnoj listi jer ComputeLayout može baciti izuzetak u letu, a LayoutSignature odlučuje kada su se dva uzastopna reflow-a konvergirala

Zašto bi vezano polje pročitalo pogrešan zapis?

Jer je skripta radila bez konteksta podataka. Polje koje nosi eksplicitno <bind match="dataRef" ref="$record.actual"/> i polje imenovano po tom istom podatkovnom čvoru dva su različita vidžeta usmerena na jednu vrednost, a ponavljajući subform sa <occur max="2"/> proizvodi više vidžeta koji dele ime i razlikuju se samo u tome kom redu podataka pripadaju; evaluacija validacije i računanja od korena dokumenta svakom od njih razrešava this u prvi odgovarajući čvor u celom datasets paketu, pa drugi red tiho validira prvi. HotPDF to izbegava čuvajući razrešeni DataNode na svakom unosu vidžeta kada ga raspored proizvede, pa taj čvor provlači kroz oba HPDFXFAEvaluateFieldScript poziva, za xfskValidate i xfskCalculate jednako. Isti kontekst odlučuje protiv kog čvora EnsureValueNode stvara kada računanje cilja binding koji još ne postoji, a kad se nijedan binding ne može razrešiti potvrda čisto pada sa XFA calculation target is not bound umesto da upiše u pogrešan red. FormCalc semantika iza tih skripti odjekuje ono što AcroForm dokumenti dobijaju od akcija opisanih u AcroForm format i calculate skriptama, ali pravila razrešavanja ovde su XFA opsega, a ne imena polja

Budžeti se proveravaju pre sporednih efekata, ne posle

Svaki limit u runtime-u je preduslov, jer budžet sproveden posle alociranja već učinjenog nije budžet. TXFAWidgetRuntimeOptions.Default isporučuje MaxWidgets na 10000, MaxValueChars na 1048576, MaxCalculationPasses na 16 i MaxReflowPasses na 4, a podrazumevane TXFAFormScriptOptions nose MaxOperations na 100000 sa MaxElapsedMilliseconds na 500. Ispod, XFA DOM primenjuje sopstvene TXFADOMLimits: 128 MB ograničenja na dekompresovan ulaz i izlaz, najviše 1024 paketa spojena, 1000000 čvorova i dubina ugnježđavanja 256. Dva detalja su važnija od samih brojeva. Prvo, skript budžeti važe za celu transakciju, a ne po skripti: CommitEdit seje jedan brojač preostalih operacija i jedan monotonski rok, i svaki validate i calculate poziv crtaju iz istog brojača i dobijaju samo milisekunde koje su još ostale, pa formular sa dvesta računajućih polja ne može potrošiti punih 500 ms dvesta puta. Drugo, rok dolazi iz injektovane MonotonicMilliseconds funkcije, što ponašanje proteklog vremena čini reprodukovanim u testnom paketu umesto bacanja novčića na zauzetom build agentu

Slojevi budžeta u HotPDF XFA runtime-u, od limita vidžeta i vrednosti preko limita operacija i vremena skripti do XFA DOM ograničenja, sa jednim brojačem operacija i jednim rokom koje dele svi pozivi u transakciji
Skript budžeti važe za celu transakciju, ne po skripti, pa dvesta računajućih polja ne može svako da zahteva svežih 500 ms
var
  Options: TXFAWidgetRuntimeOptions;
  Runtime: TXFAWidgetRuntime;
begin
  Options := TXFAWidgetRuntimeOptions.Default;
  Options.MaxWidgets := 2000;                              // podrazumevano 10000
  Options.MaxCalculationPasses := 8;                       // podrazumevano 16
  Options.MaxReflowPasses := 2;                            // podrazumevano 4
  Options.ScriptOptions.Limits.MaxOperations := 20000;     // cela 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;   // okida se samo kada reflow zaista pomeri vidžete
      end;
    // ... pokreći formular ...
  finally
    Runtime.Free;
  end;
end;

Gde runtime staje, i zašto to kaže naglas

Runtime namerno nije opšti XFA skripting engine. DispatchEvent rodom obrađuje enter i exit aktivnosti pomeranjem fokusa, a za svaku drugu aktivnost koja nosi skriptu odbija sa specifičnom, stabilnom dijagnostikom umesto da se pretvara: skripte koje pominju addInstance, removeInstance ili instanceManager vraćaju XFA runtime does not support event-driven instance mutation, skripte koje dodiruju .presence vraćaju presence ekvivalent, a sve ostalo vraća XFA runtime does not support this event script. Predvidivo odbijanje na kojem možete granati bolje je od delimične emulacije koja radi na vašem oglednom fajlu i račva se na fajlu kupca

Model nitenja jednako je direktna: jedna runtime instanca pripada jednoj niti, bez unutrašnjeg zaključavanja, jer layout engine poseže nazad u host callback-ove merenja, a brava oko toga je deadlock koji čeka repaint. Bogat sadržaj unutar polja sledi istu konzervativnu liniju kao i drugde u biblioteci, gde se exData sadržaji obrađuju kao što je opisano u XFA exData rich text i hiperlinkovima, a signature i button vidžeti vraćaju se kao ReadOnly dok nepodržane UI vrste iskaču kao xwkUnsupported umesto kao editable text box koji tiho gubi podatke

Zajedno, to je upotrebljiv odgovor na dinamički XFA u Delphi-ju: držite DOM živim, činite svaku izmenu transakcijom koja ili dospeva celog sebe ili ne ostavlja ništa, ograničite svaki prolaz, i budite eksplicitni oko toga šta je van opsega. Ako ovo procenjujete za prijave šteta, poreske ili socijalne tokove rada, XFA runtime stiže kao deo HotPDF Delphi PDF component, uz AcroForm, flattening i renderovanje puteve koje ti projekti obično na kraju trebaju zajedno