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
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
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
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