Teknisk artikkel

Dynamisk XFA-skjemakjøretid i Delphi: HotPDF-transaksjoner

HotPDF fyller dynamiske XFA-skjemaer i Delphi gjennom TXFAWidgetRuntime, et vertsnøytralt widget-lag som behandler hver feltredigering som én transaksjon: øyeblikksbilde, valider, beregn, flytt om, og publiser deretter eller rull hele tilbake. Den kjører entrådet inne i din egen VCL- eller FMX-vert, trenger ingen installert Acrobat, og håndhever hvert budsjett før den tildeler noe

Scenariet er kjent for alle som har levert dokumentprogramvare til offentlig eller forsikringsvirksomhet. Et skademeldingsskjema eller en selvangivelse ankommer som en PDF hvis sideinnhold er ett enkelt varsel om Please wait... if this message is not eventually replaced, og alle ekte felt ligger i en XFA-pakke som bare Adobe Acrobat gjengir. Brukerne dine vil fylle den ut inne i programmet ditt. Du kan ikke rasterisere deg ut av det heller, for skjemaet vokser rader etter hvert som data skrives inn, og oppsettet etter tredje rad er ikke oppsettet som fulgte med filen

Hvorfor dynamisk XFA fortsatt er et problem verdt å løse

Dynamisk XFA består fordi utrullede skjemaer overlever formatet som bar dem. ISO 32000-1 §12.7.8 beskriver XFA som en /XFA-oppføring i AcroForm-ordboken som holder en XDP-pakkestrøm, og ISO 32000-2 fraråder hele mekanismen; frarådningen fjernet den fra veikartet, ikke fra feltet, og skjemaer forfattet mot XFA 3.3-spesifikasjonen utstedes fortsatt og er fortsatt juridisk bindende. Statisk XFA kan reduseres til vanlige widget-annotasjoner, og HotPDF gjør det når du kaller ApplyXFAAsAcroForm, med avveiningene dekket i flating av XFA-skjemaer til AcroForm-felt. Dynamisk XFA er et annet dyr: dets occur-rekkevidder, voksende tekst og calculate-skript gjør feltsettet til en funksjon av dataene, så det finnes ingen fast annotasjonsliste å flate til før brukeren er ferdig å skrive. Det er gapet TXFAWidgetRuntime fyller, ved å holde XFA-DOM-en i live, beregne oppsettet på nytt etter hver akseptert redigering, og gi verten din en flat matrise av plasserte widgets å tegne og treff-teste

Hva gir kjøretiden et vertsprogram?

Den gir deg geometri og tilstand, og ingenting som forutsetter et UI-verktøysett. TXFAWidgetRuntime eksponerer WidgetCount og Widgets[I] som TXFAWidgetState-poster som bærer ID, Name, Kind, PageIndex, Bounds i PDF-punkter, Value, EditValue og flaggene Focused, Editing, ReadOnly, Valid, mens maling, markørtegning og tastaturruting forblir i koden din. Widget-identitet er stabil og ordinal: hver widget får en ID på formen name[n], der n teller tidligere forekomster av det feltnavnet i oppsettrekkefølge, så andre rad i et repeterende subskjema er amount[1]. Den identiteten er det som overlever en gjenoppbygging, og det er hva FocusWidget, BeginEdit, DispatchEvent og HitTest alle snakker. For et dokument som allerede er åpent i en THotPDF-instans trekker CreateLoadedXFAWidgetRuntime ut XDP-pakkene, tar den første sideboksen som oppsettsidestørrelse, og returnerer nil når filen ikke bærer XFA i det hele tatt

var
  Pdf: THotPDF;
  Runtime: TXFAWidgetRuntime;
  WidgetID: AnsiString;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('claim-dynamic.pdf');
    Runtime := Pdf.CreateLoadedXFAWidgetRuntime;   // nil når det ikke finnes /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)]));
      // treff-test i siderom, øverste widget vinner
      if Runtime.HitTest(0, 120.0, 96.0, WidgetID) then
        Runtime.BeginEdit(WidgetID);
    finally
      Runtime.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

Hva må være atomisk når et felt forpligtes?

Alt redigeringen kan røre, noe som er betydelig mer enn feltverdien. CommitEdit kaller CaptureSnapshot før den skriver noe, og det øyeblikksbildet dekker fire ting: den serialiserte XFA-DOM-en fra TXFADocument.SaveToBytes, hele matrisen av TXFAWidgetState-interaksjonsposter, tellerne LastCalculationPasses og LastReflowPasses, og gjeldende Warnings.Count. Å lagre nodeverdier alene er den fristende snarveien, og den er gal, for et calculate-skript eller en uavklart binding kan kalle EnsureValueNode og materialisere datanoder som ikke fantes da redigeringen begynte; en ren verdirestoring har ingen måte å fjerne dem på, så en avvist redigering ville etterlate permanent strukturelt resid i datasets-pakken. Forpliktelsessekvensen selv er streng — skriv kandidatverdien, kjør validate for det redigerte feltet, kjør calculate til et fast punkt, flytt deretter om til oppsettet er stabilt — og enhver feil i enhver fase rutes gjennom FailAndRestore, som laster øyeblikksbildebytene inn i en fersk TXFADocument, gjenoppbygger widget-listen, påfører de registrerte interaksjonstilstandene på nytt, nullstiller tellerne og trunkerer Warnings tilbake til sin øyeblikksbildelengde. LastDiagnostic holder årsaken ved feil, og holder den bokstavelige XFA transaction rollback failed i det patologiske tilfellet der selve restaureringen utløser unntak

HotPDF behandler en XFA-feltforpliktelse som én transaksjon, og fanger den serialiserte DOM-en, hver widget-tilstand, gjennomgangstellerne og advarselsantallet før validering, beregning og omflytting, og publiserer eller restaurerer deretter alle fire sammen
CommitEdit tar øyeblikksbilde av fire slags tilstander før den skriver noe, så en mislykket validate, calculate eller reflow ikke etterlater strukturelt resid
function EditAmount(Runtime: TXFAWidgetRuntime;
  const AWidgetID: AnsiString; const AText: UnicodeString): Boolean;
var
  Current: UnicodeString;
begin
  Result := False;
  if not Runtime.BeginEdit(AWidgetID) then
    Exit;                                   // skrivebeskyttet, eller ingen slik widget
  Current := Runtime.Widgets[Runtime.FocusedIndex].EditValue;
  if not Runtime.ReplaceSelection(0, Length(Current), AText) then
  begin
    Runtime.CancelEdit;                     // ugyldig rekkevidde, eller splittet surrogate
    Exit;
  end;
  Result := Runtime.CommitEdit;             // alt-eller-ingenting
  if not Result then
    // dokument, widgets, tellere og advarsler er allerede tilbake i
    // tilstanden før redigeringen; det fokuserte feltet merkes bare ugyldig
    ShowMessage(Runtime.LastDiagnostic);
end;

ReplaceSelection fortjener et notat for seg selv, for det er der feilformet inndata er billigst å avvise. Den nekter en markering som splitter et UTF-16-surrogatepar, nekter erstattningstekst som inneholder en uparet høy eller lav surrogate, og nekter ethvert resultat lengre enn MaxValueChars. Å fange det på tastetrykklaget betyr at transaksjonsmaskineriet aldri må rulle tilbake et halvskrevet astralplan-tegn

Gjenoppbygg i en privat liste, publiser i én bytting

En widget-gjenoppbygging må aldri være observerbar halvferdig, så RebuildWidgets bygger en fullstendig separat eiet TObjectList og bytter den på plass med en enkelt tildeling til slutt. Årsaken er ikke estetikk: TXFALayoutEngine.ComputeLayout kjører mens gjenoppbyggingen er i luften og ringer tilbake til vertskode gjennom MeasureText-funksjonen du leverte, og den kan utløse EXFAWidgetRuntimeError når widget-grensen treffes. Muterte kjøretiden sin live-liste på stedet, ville enten bane etterlate verten med en liste som delvis er det gamle oppsettet og delvis det nye, med DataNode-pekere inn i et dokument som er i ferd med å rulles tilbake. Reflow-konvergens avgjøres da av LayoutSignature, en streng bygget fra widget-antallet pluss hver ID, sideindeks og avgrensningsboks rundet til fire desimaler: CommitEdit gjenoppbygger, sammenligner signaturer, og gjentar til to etterfølgende signaturer stemmer eller gjennomgangsbudsjettet er utmattet. Når signaturen aldri endret seg i det hele tatt, forblir LastReflowPasses 0, noe som er hvordan du skiller en ren verdiredigering fra en som faktisk vokste skjemaet, og interaksjonstilstand bæres over hver gjenoppbygging ved widget-ID, så fokus og pågående redigering overlever en radinnsetting

HotPDF XFA-kjøretiden gjenoppbygger widget-listen sin i en separat eiet liste mens oppsettet kjører og ringer tilbake til vertens målingskode, og publiserer deretter den ferdige listen med en enkelt tildeling som verten ikke kan observere halvferdig
Gjenoppbyggingen skjer i en privat liste fordi ComputeLayout kan utløse unntak i farten, og LayoutSignature avgjør når to etterfølgende reflows har konvergert

Hvorfor ville et bundet felt lese feil post?

Fordi skriptet kjørte uten en datakontekst. Et felt som bærer en eksplisitt <bind match="dataRef" ref="$record.actual"/> og et felt oppkalt etter den samme datanoden er to forskjellige widgets pekt mot én verdi, og et repeterende subskjema med <occur max="2"/> produserer flere widgets som deler et navn og bare skiller seg i hvilken datarad de tilhører; evaluerer du validering og beregning mot dokumentroten, løser hver eneste av dem this til den første matchende noden i hele datasets-pakken, så rad to validerer i stillhet rad én. HotPDF unngår det ved å lagre den løste DataNode på hver widget-oppføring når oppsettet produserer den, og deretter tråde den noden gjennom begge HPDFXFAEvaluateFieldScript-kallene, for xfskValidate og xfskCalculate alike. Den samme konteksten avgjør hvilken node EnsureValueNode oppretter mot når en beregning sikter mot en binding som ikke finnes ennå, og når ingen binding kan løses, feiler forpliktelsen rent med XFA calculation target is not bound i stedet for å skrive inn i feil rad. FormCalc-semantikken bak disse skriptene gjenlyder hva AcroForm-dokumenter får fra handlingene beskrevet i AcroForm-format- og calculate-skript, men løsningsreglene her er XFA-omfangsbestemt snarere enn feltnavn-omfangsbestemt

Budsjetter sjekkes før bieffekter, ikke etter

Hver grense i kjøretiden er en forutsetning, for et budsjett håndhevet etter at tildelingen allerede skjedde er ikke et budsjett. TXFAWidgetRuntimeOptions.Default leverer MaxWidgets på 10000, MaxValueChars på 1048576, MaxCalculationPasses på 16 og MaxReflowPasses på 4, og standard TXFAFormScriptOptions bærer MaxOperations på 100000 med MaxElapsedMilliseconds på 500. Under dem anvender XFA-DOM-en sine egne TXFADOMLimits: 128 MB tak på dekomprimert inndata og utdata, høyst 1024 pakker sydd sammen, 1000000 noder, og en hekkedybde på 256. To detaljer betyr mer enn tallene selv. For det første er skriptbudsjettene transaksjonsvide snarere enn per skript: CommitEdit sår en enkelt gjenstående operasjonsteller og én monoton frist, og hvert validate- og calculate-kall trekker ned samme teller og mottar bare millisekundene som gjenstår, så et skjema med to hundre beregnende felt ikke kan bruke fulle 500 ms to hundre ganger. For det andre kommer fristen fra en injiserbar MonotonicMilliseconds-funksjon, noe som er det som gjør forløpt tidsatferd reproduserbar i en testpakke i stedet for et myntkast på en travel byggagent

Budsjettlag i HotPDF XFA-kjøretiden, fra widget- og verdigrenser gjennom skriptoperasjons- og tidsgrenser ned til XFA DOM-takene, med én operasjonsteller og én frist delt av hvert kall i en transaksjon
Skriptbudsjettene er transaksjonsvide snarere enn per skript, så to hundre beregnende felt kan ikke hver kreve ferske 500 ms
var
  Options: TXFAWidgetRuntimeOptions;
  Runtime: TXFAWidgetRuntime;
begin
  Options := TXFAWidgetRuntimeOptions.Default;
  Options.MaxWidgets := 2000;                              // standard 10000
  Options.MaxCalculationPasses := 8;                       // standard 16
  Options.MaxReflowPasses := 2;                            // standard 4
  Options.ScriptOptions.Limits.MaxOperations := 20000;     // hele transaksjonen
  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;   // utløses bare når reflow faktisk flyttet widgets
      end;
    // ... drive skjemaet ...
  finally
    Runtime.Free;
  end;
end;

Der kjøretiden stopper, og hvorfor den sier det høyt

Kjøretiden er med vilje ikke en generell XFA-skriptmotor. DispatchEvent håndterer enter- og exit-aktivitetene nativt ved å flytte fokus, og for hver annen aktivitet som bærer et skript nekter den med en spesifikk, stabil diagnostikk i stedet for å late som: skript som nevner addInstance, removeInstance eller instanceManager returnerer XFA runtime does not support event-driven instance mutation, skript som rører .presence returnerer presence-ekvivalenten, og alt annet returnerer XFA runtime does not support this event script. En forutsigbar nektelse du kan forgrene på, slår en delvis emulasjon som fungerer på eksempelfilen din og avviker på kundens

Trådmodellen er like prosaisk: én kjøretidsinstans tilhører én tråd, uten intern låsing, for oppsettsmotoren når tilbake til vertens målings-callbacks, og en lås rundt det er en vranglås som venter på en repaint. Rikt innhold inne i felt følger den samme konservative linjen som ellers i biblioteket, der exData-nyttelaster håndteres som beskrevet i XFA exData rik tekst og hyperlenker, og signatur- og knapp-widgets kommer tilbake som ReadOnly mens ustøttede UI-slag dukker opp som xwkUnsupported i stedet for som en redigerbar tekstboks som stille mister data

Satt sammen er det et brukbart svar på dynamisk XFA i Delphi: hold DOM-en i live, gjør hver redigering til en transaksjon som enten lander fullstendig eller etterlater ingenting, begrens hver gjennomgang, og vær eksplisitt om hva som er utenfor omfang. Evaluerer du dette for en skade-, skatte- eller trygdearbeidsflyt, leveres XFA-kjøretiden som del av HotPDF Delphi PDF component, sammen med AcroForm-, flatings- og gjengivelsesbanene de prosjektene vanligvis ender opp med å trenge sammen