Tekninen artikkeli

XFA-widget-ajoympäristö Delphissä: HotPDF-transaktiot

HotPDF täyttää dynaamisia XFA-lomakkeita Delphissä TXFAWidgetRuntimen kautta, isäntäneutraalin widget-kerroksen, joka käsittelee jokaista kenttämuokkausta yhtenä transaktiona: tilannekuva, validointi, laskenta, uudelleenasettelu ja sitten julkaisu tai palautus kokonaisuutena. Se toimii yksisäikeisesti oman VCL- tai FMX-isäntäsi sisällä, ei vaadi Acrobatin asennusta ja valvoo jokaisen budjetin ennen kuin varaa mitään

Tilanne on tuttu kaikille, jotka ovat toimittaneet dokumenttiohjelmistoja julkishallinnon tai vakuutusalalle. Vahingonkorvauslomake tai veroilmoitus saapuu PDF:nä, jonka sivusisältö on yksittäinen "Please wait... if this message is not eventually replaced" -ilmoitus, ja jokainen aito kenttä asuu XFA-paketissa, jonka vain Adobe Acrobat piirtää. Käyttäjäsi haluavat täyttää sen omassa sovelluksessasi. Rasteroimallakaan et selviä, koska lomake kasvattaa rivejä tiedon syötön myötä, eikä kolmannen rivin jälkeinen asettelu ole se, joka tiedostoon alun perin toimitettiin

Miksi dynaaminen XFA on edelleen ratkaisemisen arvoinen ongelma

Dynaaminen XFA säilyy, koska käyttöönotetut lomakkeet elävät kauemmin kuin muoto, joka ne toi. ISO 32000-1 §12.7.8 kuvaa XFA:n AcroForm-sanakirjan /XFA-tietueena, joka pitää sisällään XDP-pakettivirran, ja ISO 32000-2 vanhentaa koko mekanismin; vanhentaminen poisti sen tiekartasta, mutta ei kentiltä, ja XFA 3.3 -märittelyä vasten laaditut lomakkeet annetaan yhä eivätkä ne ole menettäneet oikeusvoimaansa. Staattinen XFA voidaan palauttaa tavallisiksi widget-annotaatioiksi, ja HotPDF tekee niin, kun kutsut ApplyXFAAsAcroFormia, jonka kompromissit on käsitelty artikkelissa XFA-lomakkeiden litistäminen AcroForm-kentiksi. Dynaaminen XFA on eri eläin: sen occur-alueet, kasvava teksti ja calculate-skriptit tekevät kenttäjoukosta datan funktion, joten kiinteää annotaatiolistaa, johon litistää, ei ole ennen kuin käyttäjä on lopettanut kirjoittamisen. Se on aukko, jonka TXFAWidgetRuntime täyttää pitämällä XFA-DOMin elossa, laskemalla asettelun uudelleen jokaisen hyväksytyn muokkauksen jälkeen ja antamalla isännällesi tasaisen taulukon sijoitettuja widgetejä piirrettäväksi ja osumatarkistettavaksi

Mitä ajoympäristö antaa isäntäsovellukselle?

Se antaa sinulle geometrian ja tilan, ei mitään, joka olettaa UI-kirjaston. TXFAWidgetRuntime näyttää WidgetCountin ja Widgets[I]in TXFAWidgetState-tietueina, jotka kantavat IDtä, Namea, Kindia, PageIndexia, Boundsia PDF-pisteissä, Valuea, EditValuea sekä Focused-, Editing-, ReadOnly- ja Valid-liput, kun taas piirtäminen, tekstin kohdistimen piirto ja näppäimistöreititys jäävät koodillesi. Widgetin identiteetti on vakaa ja järjestykseen perustuva: jokainen widget saa IDn muodossa name[n], jossa n laskee kyseisen kentän nimen aiemmat esiintymät asettelujärjestyksessä, joten toistuvan subformin toinen rivi on amount[1]. Tuo identiteetti selviää uudelleenrakennuksesta, ja sitä puhuvat FocusWidget, BeginEdit, DispatchEvent ja HitTest. THotPDF-ilmentymässä jo avatulle dokumentille CreateLoadedXFAWidgetRuntime purkaa XDP-paketit, ottaa ensimmäisen sivulaatikon asettelusivun kokona ja palauttaa nilin, kun tiedostossa ei ole lainkaan XFA:ta

var
  Pdf: THotPDF;
  Runtime: TXFAWidgetRuntime;
  WidgetID: AnsiString;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('claim-dynamic.pdf');
    Runtime := Pdf.CreateLoadedXFAWidgetRuntime;   // nil, kun /XFA puuttuu
    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)]));
      // sivuavaruuden osumatarkistus, päällimmäinen widget voittaa
      if Runtime.HitTest(0, 120.0, 96.0, WidgetID) then
        Runtime.BeginEdit(WidgetID);
    finally
      Runtime.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

Mitä on oltava atomista, kun kenttä vahvistetaan?

Kaikki, mitä muokkaus voi koskettaa, mikä on huomattavasti enemmän kuin kentän arvo. CommitEdit kutsuu CaptureSnapshotia ennen kuin kirjoittaa mitään, ja kyseinen tilannekuva kattaa neljä asiaa: sarjallistetun XFA-DOMin lähteestä TXFADocument.SaveToBytes, täyden taulukon TXFAWidgetState-interaktiotietueita, LastCalculationPasses- ja LastReflowPasses-laskurit sekä nykyisen Warnings.Countin. Pelkkien solmuarvojen tallentaminen on houkutteleva oikotie ja se on väärin, koska calculate-skripti tai ratkaisematon sidonta voi kutsua EnsureValueNodeia ja materialisoida datan solmuja, joita ei ollut olemassa muokkauksen alkaessa; pelkkä arvopalautus ei pysty poistamaan niitä, joten hylätty muokkaus jättäisi pysyvän rakenteellisen jäännöksen datasets-pakettiin. Vahvistusjärjestys itsessään on tiukka — kirjoita ehdokasarvo, aja validate muokatulle kentälle, aja calculate kiintopisteeseen asti ja asettele uudelleen, kunnes asettelu on vakaa — ja mikä tahansa epäonnistuminen missä tahansa vaiheessa ohjautuu FailAndRestorelle, joka lataa tilannekuvan tavut tuoreeseen TXFADocumentiin, rakentaa widget-listan uudelleen, soveltaa tallennetut interaktiotilat uudelleen, nollaa laskurit ja katkaisee Warningsin takaisin tilannekuvan mittaan. LastDiagnostic pitää sisällään syyn epäonnistuessa ja kirjaimellisen XFA transaction rollback failedin patologisessa tapauksessa, jossa palautus itsessään nostaa poikkeuksen

HotPDF käsittelee XFA-kentän vahvistusta yhtenä transaktiona vangiten sarjallistetun DOMin, jokaisen widget-tilan, kierroslaskurit ja varoituslukumäärän ennen validointia, laskentaa ja uudelleenasettelua ja julkaisee tai palauttaa sitten kaikki neljä yhdessä
CommitEdit ottaa tilannekuvan neljästä tilalajista ennen kuin kirjoittaa mitään, joten epäonnistunut validate, calculate tai reflow ei jätä rakenteellista jäännöstä jälkeensä
function EditAmount(Runtime: TXFAWidgetRuntime;
  const AWidgetID: AnsiString; const AText: UnicodeString): Boolean;
var
  Current: UnicodeString;
begin
  Result := False;
  if not Runtime.BeginEdit(AWidgetID) then
    Exit;                                   // vain luku, tai widgetiä ei ole
  Current := Runtime.Widgets[Runtime.FocusedIndex].EditValue;
  if not Runtime.ReplaceSelection(0, Length(Current), AText) then
  begin
    Runtime.CancelEdit;                     // virheellinen alue tai jaettu surrogaatti
    Exit;
  end;
  Result := Runtime.CommitEdit;             // kaikki tai ei mitään
  if not Result then
    // dokumentti, widgetit, laskurit ja varoitukset ovat jo palanneet
    // muokkausta edeltävään tilaan; kohdistettu widget vain merkitään
    // kelvottomaksi
    ShowMessage(Runtime.LastDiagnostic);
end;

ReplaceSelection ansaitsee oman muistiinpanonsa, koska se on paikka, jossa muodottoman syötteen hylkääminen on halvinta. Se hylkää valinnan, joka jakaa UTF-16-surrogaattiparin, hylkää korvaavan tekstin, joka sisältää parittoman ylä- tai alasurrogaatin, ja hylkää minkä tahansa MaxValueCharsia pidemmän tuloksen. Sen havaitseminen näppäintasolla tarkoittaa, ettei transaktiokoneiston koskaan tarvitse purkaa puoliksi kirjoitettua astralitason merkkiä

Rakenna uudelleen yksityiseen listaan, julkaise yhdellä vaihdolla

Widgetin uudelleenrakennusta ei saa koskaan näkyä puolivalmiina, joten RebuildWidgets rakentaa täysin erillisen omistavan TObjectListin ja vaihtaa sen paikalleen yhdellä sijoituksella lopussa. Syy ei ole estetiikka: TXFALayoutEngine.ComputeLayout toimii, kun uudelleenrakennus on käynnissä, ja kutsuu takaisin isäntäkoodiin toimittamasi MeasureText-funktion kautta, ja se voi nostaa EXFAWidgetRuntimeErrorin, kun widget-raja saavutetaan. Jos ajympäristö muuttaisi elävää listaansa paikallaan, kumpikin polku jättäisi isännän haltuun listan, joka on osin vanhaa asettelua ja osin uutta, ja DataNode-osoittimet dokumenttiin, joka on aikeissa palautua. Uudelleenasettelun suppenemisen päättää sitten LayoutSignature, merkkijono, joka rakentuu widgetien lukumäärästä sekä jokaisesta IDstä, sivuindeksistä ja neljään desimaaliin pyöristetystä rajalaatikosta: CommitEdit rakentaa uudelleen, vertaa allekirjoituksia ja toistaa, kunnes kaksi peräkkäistä allekirjoitusta täsmää tai kierrosbudjetti on käytetty. Kun allekirjoitus ei koskaan muuttunut, LastReflowPasses pysyy arvossa 0, mistä erottaa pelkän arvomuokkauksen muokkauksesta, joka oikeasti kasvatti lomaketta, ja interaktiotila siirtyy jokaisen uudelleenrakennuksen yli widgetin IDn avulla, joten kohdistus ja kesken oleva muokkaus selviävät rivin lisäyksestä

HotPDF:n XFA-ajympäristö rakentaa widget-listansa uudelleen erilliseen omistavaan listaan, kun asettelu toimii ja kutsuu takaisin isännän mittauskoodiin, ja julkaisee valmiin listan yhdellä sijoituksella, jota isäntä ei voi nähdä puolivalmiina
Uudelleenrakennus tapahtuu yksityisessä listassa, koska ComputeLayout voi nostaa poikkeuksen kesken kaiken, ja LayoutSignature päättää, milloin kaksi peräkkäistä uudelleenasettelua on suppunut

Miksi sidottu kenttä lukisi väärän tietueen?

Koska skripti toimi ilman datakontekstia. Kenttä, joka kantaa nimenomaista <bind match="dataRef" ref="$record.actual"/>ia, ja kenttä, joka on nimetty saman datan solmun mukaan, ovat kaksi eri widgetiä osoittamassa yhteen arvoon, ja toistuva subform <occur max="2"/> tuottaa useita widgetejä, jotka jakavat nimen ja eroavat vain siinä, mihin datariviin ne kuuluvat; arvioi validointi ja laskenta dokumentin juurta vasten, ja jokainen niistä ratkaisee thisn koko datasets-paketin ensimmäiseksi täsmääväksi solmuksi, joten rivi kaksi validoi hiljaa rivin yhden. HotPDF välttää sen tallentamalla ratkaistun DataNodein jokaiseen widget-merkintään, kun asettelu tuottaa sen, ja kutomalla kyseisen solmun kummankin HPDFXFAEvaluateFieldScript-kutsun läpi, niin xfskValidatelle kuin xfskCalculatellekin. Sama konteksti päättää, mitä solmua vasten EnsureValueNode luo, kun laskenta kohdistuu sidontaan, jota ei vielä ole, ja kun mitään sidontaa ei voi ratkaista, vahvistus epäonnistuu siististi virheellä XFA calculation target is not bound kirjoittamatta väärään riviin. Näiden skriptien taustalla oleva FormCalc-semantiikka muistuttaa sitä, mitä AcroForm-dokumentit saavat artikkelissa AcroForm-muoto ja calculate-skriptit kuvatuista toiminnoista, mutta ratkaisusäännöt ovat täällä XFA:n laajuisia, eivät kentännimen laajuisia

Budjetit tarkistetaan ennen sivuvaikutuksia, eikä niiden jälkeen

Jokainen ajympäristön raja on esiehto, koska budjetti, jota valvotaan sen jälkeen, kun varaus on jo tapahtunut, ei ole budjetti. TXFAWidgetRuntimeOptions.Default toimittaa MaxWidgetsin arvolla 10000, MaxValueCharsin arvolla 1048576, MaxCalculationPassesin arvolla 16 ja MaxReflowPassesin arvolla 4, ja oletusarvoiset TXFAFormScriptOptions kantavat MaxOperationsin arvolla 100000 ja MaxElapsedMillisecondsin arvolla 500. Alaspäin XFA-DOM soveltaa omia TXFADOMLimitsejään: 128 MB:n katot pakkauksesta puretulle syötölle ja tulosteelle, enintään 1024 yhteen ommeltua pakettia, 1000000 solmua ja pesityssyvyys 256. Kaksi yksityiskohtaa merkitsevät enemmän kuin luvut itse. Ensinnäkin skriptibudjetit ovat transaktion laajuisia, eivät skriptiä kohti: CommitEdit kylvää yhden jäljellä olevien operaatioiden laskurin ja yhden monotonisen määräajan, ja jokainen validate- ja calculate-kutsu kuluttaa samaa laskuria ja saa vain ne millisekunnit, jotka ovat vielä jäljellä, joten lomake, jossa on kaksisataa laskevaa kenttää, ei voi kuluttaa täyttä 500 ms:ää kaksisataa kertaa. Toiseksi määräaika tulee injektoitavasta MonotonicMilliseconds-funktiosta, mikä tekee kuluneen ajan käyttäytymisestä toistettavan testisarjassa koneen heiton sijaan kiireisellä koontiagentilla

Budjettikerrokset HotPDF:n XFA-ajympäristössä widget- ja arvorajoista skriptioperaatio- ja aikarajojen kautta XFA-DOMin kattoihin asti, yhdellä operaatiolaskurilla ja yhdellä määräajalla, jotka jokainen transaktion kutsu jakaa
Skriptibudjetit ovat transaktion laajuisia, eivät skriptiä kohden, joten kaksisataa laskevaa kenttää ei voi kukin vaatia tuoretta 500 ms:ää
var
  Options: TXFAWidgetRuntimeOptions;
  Runtime: TXFAWidgetRuntime;
begin
  Options := TXFAWidgetRuntimeOptions.Default;
  Options.MaxWidgets := 2000;                              // oletus 10000
  Options.MaxCalculationPasses := 8;                       // oletus 16
  Options.MaxReflowPasses := 2;                            // oletus 4
  Options.ScriptOptions.Limits.MaxOperations := 20000;     // koko transaktio
  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;   // lauaisee vain, kun reflow oikeasti siirsi widgetejä
      end;
    // ... aja lomaketta ...
  finally
    Runtime.Free;
  end;
end;

Missä ajoympäristö pysähtyy ja miksi se sanoo sen ääneen

Ajympäristö ei tahallaan ole yleinen XFA-skriptimoottori. DispatchEvent käsittelee enter- ja exit-aktiviteetit natiivisti siirtämällä kohdistusta, ja jokaisen muun skriptiä kantavan aktiviteetin kohdalla se kieltäytyy tarkalla, vakaalla diagnoosilla teeskentelemisen sijaan: skriptit, jotka mainitsevat addInstancein, removeInstancein tai instanceManagerin, palauttavat XFA runtime does not support event-driven instance mutationin, skriptit, jotka koskettavat .presenceä, palauttavat presence-vastineen, ja kaikki muu palauttaa XFA runtime does not support this event scriptin. Ennustettava kieltäytyminen, jonka varaan voit haarautua, lyö osittaisen emuloinnin, joka toimii esimerkkitiedostossasi ja haarautuu asiakkaan tiedostossa

Säikeistusmalli on yhtä suora: yksi ajympäristöilmentymä kuuluu yhdelle säikeelle ilman sisäistä lukitusta, koska asettelumoottori ulottuu takaisin isännän mittauskutsuihin, ja lukko sen ympärillä tarkoittaisi lukkiutumista, joka odottaa uudelleenpiirtoa. Kenttien sisällä oleva rikas sisältö seuraa samaa konservatiivista linjaa kuin muuallakin kirjastossa, jossa exData-sisällöt käsitellään artikkelissa XFA exData -rikasteksti ja hyperlinkit kuvatulla tavalla, ja allekirjoitus- ja painike-widgetit palautuvat ReadOnlyna, kun taas tukemattomat UI-lajit ilmenevät xwkUnsupportedina sen sijaan, että olisivat muokattava tekstilaatikko, joka häviää dataa hiljaa

Yhteenlaskettuna se on toimiva vastaus dynaamiseen XFA:aan Delphissä: pidä DOM elossa, tee jokaisesta muokkauksesta transaktio, joka joko laskeutuu kokonaan tai jättää mitään jälkeensä, rajaa jokainen kierros ja ole nimenomainen siitä, mikä jää soveltamisalan ulkopuolelle. Jos arvioit tätä vahinko-, vero- tai etuustyönkulkuun, XFA-ajympäristö kuuluu osaksi HotPDF Delphi PDF component -komponenttia rinnakkain AcroForm-, litistys- ja piirtopolkujen kanssa, joita kyseiset projektit yleensä päätyvät tarvitsemaan yhdessä