Tehnički članak

Dinamički XFA u Delphiju: HotPDF transakcijski runtime

HotPDF ispunjava dinamičke XFA obrasce u Delphiju kroz TXFAWidgetRuntime, sloj widgeta neovisan o domaćinu koji svaku izmjenu polja tretira kao jednu transakciju: snimka stanja, validacija, izračun, ponovni raspored, a zatim objava ili vraćanje u cijelosti. Radi u jednoj niti unutar vašeg vlastitog VCL ili FMX domaćina, ne traži instalirani Acrobat i svaki budžet nametne prije nego što išta alocira

Scenarij je poznat svakome tko je isporučivao softver za dokumente u državnu upravu ili osiguranje. Obrazac za prijavu štete ili poreznu prijavu stigne kao PDF čiji je sadržaj stranice jedna jedina obavijest „Please wait... if this message is not eventually replaced“, a sva stvarna polja žive u XFA paketu koji renderira samo Adobe Acrobat. Vaši korisnici žele ispuniti ga unutar vaše aplikacije. Izlaska nema ni rasterizacijom, jer obrazac dodaje redove kako se podaci unose, a raspored nakon trećeg retka nije raspored iz isporučene datoteke

Zašto je dinamički XFA i dalje problem vrijedan rješavanja

Dinamički XFA opstaje jer raspoređeni obrasci nadžive format koji ih je nosio. ISO 32000-1 §12.7.8 opisuje XFA kao /XFA unos u AcroForm rječniku koji drži tok XDP paketa, a ISO 32000-2 cijeli mehanizam označava kao zastario; ta oznaka uklonila ga je iz planova, ali ne iz terena, pa se obrasci pisani prema specifikaciji XFA 3.3 i dalje izdaju i i dalje pravno obvezuju. Statički XFA može se svesti na obične widget anotacije, i to HotPDF radi kada pozovete ApplyXFAAsAcroForm, uz kompromise obrađene u članku o pretvaranju XFA obrazaca u AcroForm polja. Dinamički XFA sasvim je druga priča: njegovi occur rasponi, tekst koji raste i calculate skripte čine skup polja funkcijom podataka, pa fiksni popis anotacija za pretvaranje ne postoji dok korisnik ne završi s tipkanjem. Upravo tu prazninu popunjava TXFAWidgetRuntime: drži XFA DOM živim, ponovno računa raspored nakon svake prihvaćene izmjene i vašem domaćinu predaje ravan niz pozicioniranih widgeta za crtanje i provjeru pogodaka

Što runtime predaje aplikaciji domaćinu?

Predaje vam geometriju i stanje, i ništa što pretpostavlja UI alatnik. TXFAWidgetRuntime izlaže WidgetCount i Widgets[I] kao zapise TXFAWidgetState koji nose ID, Name, Kind, PageIndex, Bounds u PDF točkama, Value, EditValue te zastavice Focused, Editing, ReadOnly i Valid, dok crtanje, ispis kursora i usmjeravanje tipkovnice ostaju u vašem kodu. Identitet widgeta stabilan je i redni: svaki widget dobiva ID oblika name[n], gdje n broji prijašnje pojave tog imena polja u redoslijedu rasporeda, pa je drugi redak ponavljajućeg subforma amount[1]. Taj identitet preživljava ponovnu izgradnju i njime govore FocusWidget, BeginEdit, DispatchEvent i HitTest. Za dokument već otvoren u instanci THotPDF, CreateLoadedXFAWidgetRuntime izvlači XDP pakete, uzima kutiju prve stranice kao veličinu stranice rasporeda i vraća nil kada datoteka uopće 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, pobjeđuje najgornji widget
      if Runtime.HitTest(0, 120.0, 96.0, WidgetID) then
        Runtime.BeginEdit(WidgetID);
    finally
      Runtime.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

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

Sve što izmjena može dirnuti, a to je znatno više od vrijednosti polja. CommitEdit poziva CaptureSnapshot prije nego što išta zapiše, a ta snimka obuhvaća četiri stvari: serijalizirani XFA DOM iz TXFADocument.SaveToBytes, cijeli niz zapisa interakcije TXFAWidgetState, brojače LastCalculationPasses i LastReflowPasses te trenutni Warnings.Count. Spremiti samo vrijednosti čvorova primamljiv je prečac i pogrešan je, jer calculate skripta ili neriješeno povezivanje mogu pozvati EnsureValueNode i materijalizirati podatkovne čvorove koji nisu postojali kad je izmjena počela; vraćanje samo vrijednosti nema načina da ih ukloni, pa bi odbijena izmjena u paketu datasets ostavila trajni strukturni ostatak. Sami slijed potvrde strog je — zapiši predloženu vrijednost, provrti validate za uređivano polje, provrti calculate do fiksne točke, a zatim ponavljaj raspored dok se ne ustali — i svaki neuspjeh u bilo kojoj fazi ide kroz FailAndRestore, koji snimljene bajtove učitava u svježi TXFADocument, ponovno gradi popis widgeta, vraća zabilježena stanja interakcije, resetira brojače i skraćuje Warnings natrag na snimljenu duljinu. LastDiagnostic na neuspjehu drži razlog, a u patološkom slučaju kada podigne i samo vraćanje drži literal XFA transaction rollback failed

HotPDF potvrdu XFA polja tretira kao jednu transakciju: snima serijalizirani DOM, stanje svakog widgeta, brojače prolaza i broj upozorenja prije validacije, izračuna i ponovnog rasporeda, a zatim objavljuje ili vraća sve četiri skupa zajedno
CommitEdit snima četiri vrste stanja prije nego što išta zapiše, pa neuspjela validacija, izračun ili ponovni raspored ne ostavljaju strukturne ostatke
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 čitanje, ili nema takvog widgeta
  Current := Runtime.Widgets[Runtime.FocusedIndex].EditValue;
  if not Runtime.ReplaceSelection(0, Length(Current), AText) then
  begin
    Runtime.CancelEdit;                     // neispravan raspon, ili razdvojen surrogat
    Exit;
  end;
  Result := Runtime.CommitEdit;             // sve ili ništa
  if not Result then
    // dokument, widgeti, brojači i upozorenja već su vraćeni u
    // stanje prije izmjene; widget u fokusu samo je označen nevažećim
    ShowMessage(Runtime.LastDiagnostic);
end;

ReplaceSelection zaslužuje vlastitu napomenu, jer je to mjesto gdje je neispravan ulaz najjeftinije odbiti. Odbija selekciju koja razdvaja UTF-16 surrogatni par, odbija zamjenski tekst koji sadrži neupareni visoki ili niski surrogat i odbija svaki rezultat dulji od MaxValueChars. Hvatanje toga na razini tipke znači da mehanizam transakcije nikada ne mora razmotavati napola zapisani znak iz astralne ravnine

Ponovna izgradnja u privatni popis, objava jednom zamjenom

Ponovna izgradnja widgeta nikada ne smije biti motriva u napola dovršenom stanju, pa RebuildWidgets gradi potpuno odvojeni vlasnički TObjectList i na kraju ga ugrađuje jednom jedinom dodjelom. Razlog nije estetski: TXFALayoutEngine.ComputeLayout radi dok je izgradnja u tijeku i poziva kod domaćina kroz funkciju MeasureText koju ste dostavili, a može podići EXFAWidgetRuntimeError kada se pogodi ograničenje widgeta. Da je runtime mijenjao svoj živi popis na licu mjesta, bilo koji od tih putova ostavio bi domaćina s popisom koji je djelomično stari, a djelomično novi raspored, s DataNode pokazivačima u dokument koji će uskoro biti vraćen. Konvergenciju ponovnog rasporeda zatim odlučuje LayoutSignature, niz sastavljen od broja widgeta plus svakog ID, indeksa stranice i okvirne kutije zaokružene na četiri decimale: CommitEdit gradi ponovno, uspoređuje potpise i ponavlja dok se dva uzastopna potpisa ne poklope ili dok se ne iscrpi budžet prolaza. Kada se potpis uopće nije mijenjao, LastReflowPasses ostaje 0, po čemu razlikujete izmjenu samo vrijednosti od one koja je stvarno proširila obrazac, a stanje interakcije nosi se kroz svaku izgradnju po widget ID-u, pa fokus i izmjena u tijeku prežive umetanje retka

XFA runtime u HotPDF-u gradi svoj popis widgeta u zasebni vlasnički popis dok raspored radi i poziva mjerni kod domaćina, a gotov popis objavljuje jednom dodjelom koju domaćin ne može zateći napola dovršenu
Izgradnja se odvija u privatnom popisu jer ComputeLayout može podići iznimku usred rada, a LayoutSignature odlučuje kada su se dva uzastopna ponovna rasporeda konvergirala

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

Jer je skripta radila bez podatkovnog konteksta. Polje s izričitim <bind match="dataRef" ref="$record.actual"/> i polje imenovano po tom istom podatkovnom čvoru dva su različita widgeta usmjerena na jednu vrijednost, a ponavljajući subform s <occur max="2"/> proizvodi više widgeta koji dijele ime i razlikuju se samo u tome kojem retku podataka pripadaju; ako validaciju i izračun vrednujete od korijena dokumenta, svaki od njih riješit će this na prvi odgovarajući čvor u cijelom paketu datasets, pa drugi redak tiho validira prvi. HotPDF to izbjegava tako da prilikom izrade rasporeda na svaki unos widgeta pohrani riješeni DataNode, a zatim taj čvor provlači kroz oba poziva HPDFXFAEvaluateFieldScript, jednako za xfskValidate i xfskCalculate. Isti kontekst odlučuje na koji će čvor EnsureValueNode stvarati kada izračun cilja povezivanje koje još ne postoji, a kada se nijedno povezivanje ne može riješiti, potvrda čisto propada s XFA calculation target is not bound umjesto da piše u pogrešan redak. FormCalc semantika iza tih skripti odjekuje ono što AcroForm dokumenti dobivaju od akcija opisanih u članku AcroForm format i calculate skripte, ali se pravila rješavanja ovdje vežu uz XFA, a ne uz ime polja

Budžeti se provjeravaju prije nuspojava, a ne poslije

Svako ograničenje u runtimeu preduvjet je, jer budžet nametnut nakon što se alokacija već dogodila nije budžet. TXFAWidgetRuntimeOptions.Default isporučuje MaxWidgets na 10000, MaxValueChars na 1048576, MaxCalculationPasses na 16 i MaxReflowPasses na 4, a zadani TXFAFormScriptOptions nose MaxOperations na 100000 s MaxElapsedMilliseconds na 500. Ispod toga XFA DOM primjenjuje vlastite TXFADOMLimits: plafon od 128 MB na dekomprimirani ulaz i izlaz, najviše 1024 međusobno spojenih paketa, 1000000 čvorova i dubina ugnježđivanja od 256. Dvije pojedinosti važnije su od samih brojeva. Prvo, skriptni budžeti vrijede za cijelu transakciju, a ne po skripti: CommitEdit zasije jedan brojač preostalih operacija i jedan monotoni rok, a svaki poziv validate i calculate troši taj isti brojač i dobiva samo još preostale milisekunde, pa obrazac s dvjesto izračunskih polja ne može potrošiti punih 500 ms dvjesto puta. Drugo, rok dolazi iz ubacive funkcije MonotonicMilliseconds, čime je ponašanje po proteku vremena reproducibilno u testnom skupu umjesto bacanja novčića na zauzetom build agentu

Slojevi budžeta u XFA runtimeu HotPDF-a, od ograničenja widgeta i vrijednosti preko operacija i vremenskih ograničenja skripti do plafona XFA DOM-a, s jednim brojačem operacija i jednim rokom koje dijele svi pozivi u transakciji
Skriptni budžeti vrijede za cijelu transakciju, a ne po skripti, pa dvjesto izračunskih polja ne može svako zatražiti svježih 500 ms
var
  Options: TXFAWidgetRuntimeOptions;
  Runtime: TXFAWidgetRuntime;
begin
  Options := TXFAWidgetRuntimeOptions.Default;
  Options.MaxWidgets := 2000;                              // zadano 10000
  Options.MaxCalculationPasses := 8;                       // zadano 16
  Options.MaxReflowPasses := 2;                            // zadano 4
  Options.ScriptOptions.Limits.MaxOperations := 20000;     // cijela 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;   // aktivira se samo kada reflow stvarno pomakne widgete
      end;
    // ... upravljaj obrascem ...
  finally
    Runtime.Free;
  end;
end;

Gdje runtime staje i zašto to govori naglas

Runtime namjerno nije općeniti XFA skriptni motor. DispatchEvent izvorno rukuje aktivnostima enter i exit pomicanjem fokusa, a za svaku drugu aktivnost koja nosi skriptu odbija s konkretnom, stabilnom dijagnostikom umjesto da se pravi: skripte koje spominju addInstance, removeInstance ili instanceManager vraćaju XFA runtime does not support event-driven instance mutation, skripte koje diraju .presence vraćaju odgovarajuću poruku za presence, a sve ostalo vraća XFA runtime does not support this event script. Predvidljivo odbijanje na kojem možete granati bolje je od djelomične emulacije koja radi na vašoj oglednoj datoteci, a skreće na datoteci kupca

Model radnih niti jednako je direktan: jedna instanca runtimea pripada jednoj niti, bez internog zaključavanja, jer layout motor poseže natrag u povratne pozive za mjerenje kod domaćina, a brava oko toga deadlock je koji čeka repaint. Bogat sadržaj unutar polja slijedi istu konzervativnu liniju kao i ostatak knjižnice, gdje se exData paketi rukuju kako je opisano u članku XFA exData obogaćeni tekst i hiperpoveznice, a widgeti potpisa i gumba vraćaju se kao ReadOnly, dok nepodržane UI vrste iskaču kao xwkUnsupported umjesto kao tekstualna kutija za uređivanje koja tiho gubi podatke

Zbrojeno, to je izvodljiv odgovor na dinamički XFA u Delphiju: držite DOM živim, učinite svaku izmjenu transakcijom koja ili sasvim sjedne ili ne ostavi ništa, ograničite svaki prolaz i budite izričiti oko onoga što je izvan opsega. Ako ovo vrednujete za tokove rada oko prijava šteta, poreza ili socijalnih davanja, XFA runtime isporučuje se kao dio HotPDF Delphi PDF komponente, uz putanje za AcroForm, pretvaranje i renderiranje koje ti projekti obično na kraju trebaju zajedno