HotPDF udfylder dynamiske XFA-formularer i Delphi gennem TXFAWidgetRuntime, et host-neutralt widget-lag, der behandler hver feltredigering som én transaktion: snapshot, validate, calculate, reflow, og derefter publicér eller rul helt tilbage. Den kører enkelttrådet inde i din egen VCL- eller FMX-host, behøver ingen Acrobat installeret og håndhæver hvert budget, før den allocerer noget
Scenariet er velkendt for enhver, der har leveret dokumentsoftware til offentlige eller forsikringsopgaver. En skadesformular eller selvangivelse ankommer som en PDF, hvis sideindhold er en enkelt "Please wait... if this message is not eventually replaced"-meddelelse, og alle rigtige felter bor i en XFA-pakke, som kun Adobe Acrobat renderer. Dine brugere vil udfylde den inde i din applikation. Du kan heller ikke rasterisere dig ud af det, for formularen vokser rækker, efterhånden som data indtastes, og layoutet efter tredje række er ikke det layout, der fulgte med filen
Hvorfor dynamisk XFA stadig er et problem værd at løse
Dynamisk XFA består, fordi udrullede formularer overlever formatet, der bar dem. ISO 32000-1 §12.7.8 beskriver XFA som en /XFA-indgang på AcroForm-ordbogen, der holder en XDP-pakkestream, og ISO 32000-2 deprecierer hele mekanismen; depreciering fjernede den fra roadmapet, ikke fra praksis, og formularer forfattet efter XFA 3.3-specifikationen udstedes stadig og er stadig juridisk bindende. Statisk XFA kan reduceres til almindelige widget-annotationer, og HotPDF gør det, når du kalder ApplyXFAAsAcroForm, med afvejningerne dækket i fladning af XFA-formularer til AcroForm-felter. Dynamisk XFA er et andet dyr: dets occur-intervaller, vækstbar tekst og calculate-scripts gør felt sættet til en funktion af dataene, så der er ingen fast annotationsliste at flade til, før brugeren er færdig med at taste. Det er hullet TXFAWidgetRuntime fylder ved at holde XFA-DOM'et levende, genberegne layoutet efter hver accepteret redigering og give din host et fladt array af positionerede widgets at tegne og hit-teste
Hvad giver runtimeen en hostapplikation?
Den giver dig geometri og tilstand, og intet, der antager et UI-toolkit. TXFAWidgetRuntime eksponerer WidgetCount og Widgets[I] som TXFAWidgetState-records, der bærer ID, Name, Kind, PageIndex, Bounds i PDF-punkter, Value, EditValue og flagene Focused, Editing, ReadOnly, Valid, mens maling, caret-tegning og tastatur-routing forbliver i din kode. Widget-identitet er stabil og ordinal: hver widget får et ID af formen name[n], hvor n tæller tidligere forekomster af det feltnavn i layoutorden, så anden række af en gentagende subform er amount[1]. Den identitet er, hvad der overlever en genopbygning, og det er, hvad FocusWidget, BeginEdit, DispatchEvent og HitTest alle taler. For et dokument allerede åbent i en THotPDF-instans udtrækker CreateLoadedXFAWidgetRuntime XDP-pakkerne, tager første page box som layoutsidestørrelse og returnerer nil, når filen slet ikke bærer 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, når der ikke er nogen /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)]));
// page-space hit-test, øverste widget vinder
if Runtime.HitTest(0, 120.0, 96.0, WidgetID) then
Runtime.BeginEdit(WidgetID);
finally
Runtime.Free;
end;
finally
Pdf.Free;
end;
end;
Hvad skal være atomart, når et felt committes?
Alt, redigeringen kan røre, hvilket er betydeligt mere end feltværdien. CommitEdit kalder CaptureSnapshot, før den skriver noget, og det snapshot dækker fire ting: den serialiserede XFA-DOM fra TXFADocument.SaveToBytes, det fulde array af TXFAWidgetState-interaktionsrecords, tællerne LastCalculationPasses og LastReflowPasses samt den aktuelle Warnings.Count. Kun at gemme nodeværdier er den fristende genvej, og den er forkert, fordi et calculate-script eller en uopløst binding kan kalde EnsureValueNode og materialisere datanoder, der ikke eksisterede, da redigeringen begyndte; en værdi-only-gendannelse har ingen måde at fjerne dem på, så en afvist redigering ville efterlade permanent strukturelt rester i datasets-pakken. Commitsekvensen selv er streng — skriv kandidatværdien, kør validate for det redigerede felt, kør calculate til et fast punkt, reflow derefter, indtil layoutet er stabilt — og enhver fejl på ethvert trin ruter gennem FailAndRestore, som genindlæser snapshot-bytes i et friskt TXFADocument, genopbygger widgetlisten, genanvender de registrerede interaktionstilstande, nulstiller tællerne og afkorter Warnings tilbage til sin snapshot-længde. LastDiagnostic holder årsagen ved fejl og holder den bogstavelige XFA transaction rollback failed i det patologiske tilfælde, hvor gendannelsen selv rejser
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, eller ingen sådan widget
Current := Runtime.Widgets[Runtime.FocusedIndex].EditValue;
if not Runtime.ReplaceSelection(0, Length(Current), AText) then
begin
Runtime.CancelEdit; // dårligt interval, eller splittet surrogate
Exit;
end;
Result := Runtime.CommitEdit; // alt-eller-intet
if not Result then
// dokument, widgets, tællere og advarsler er allerede tilbage i
// før-redigerings-tilstanden; den fokuserede widget markeres blot ugyldig
ShowMessage(Runtime.LastDiagnostic);
end;
ReplaceSelection fortjener sin egen note, fordi det er dér, misdannet input er billigst at afvise. Den afviser en markering, der splitter et UTF-16-surrogatepar, afviser erstatningstekst, der indeholder en uparet høj eller lav surrogate, og afviser ethvert resultat længere end MaxValueChars. At fange det ved tastetryk-laget betyder, at transaktionsmaskineriet aldrig behøver at rulle et halvskrevet astralplan-tegn tilbage
Genopbyg i en privat liste, publicér i ét swap
En widget-genopbygning må aldrig kunne observeres halvfærdig, så RebuildWidgets bygger en helt separat ejende TObjectList og swapper den på plads med en enkelt tildeling til sidst. Årsagen er ikke æstetik: TXFALayoutEngine.ComputeLayout kører, mens genopbygningen er i luften, og kalder tilbage til hostkode gennem den MeasureText-funktion, du leverede, og den kan rejse EXFAWidgetRuntimeError, når widget-grænsen rammes. Hvis runtimeen muterede sin levende liste på stedet, ville enhver af stierne efterlade hosten med en liste, der delvis er det gamle layout og delvis det nye, med DataNode-pointere ind i et dokument, der er ved at blive rullet tilbage. Reflow-konvergens afgøres derefter af LayoutSignature, en streng bygget af widget-antallet plus hvert ID, sideindeks og bounding box afrundet til fire decimaler: CommitEdit genopbygger, sammenligner signaturer og gentager, indtil to på hinanden følgende signaturer matcher, eller pass-budgettet er udtømt. Når signaturen slet ikke ændredes, forbliver LastReflowPasses 0, hvilket er, hvordan du skelner en værdi-only-redigering fra en, der reelt voksede formularen, og interaktionstilstand bæres over hver genopbygning ved widget-ID, så fokus og igangværende redigering overlever en rækkeindsættelse
Hvorfor ville et bundet felt læse den forkerte post?
Fordi scriptet kørte uden en datakontekst. Et felt med en eksplicit <bind match="dataRef" ref="$record.actual"/> og et felt opkaldt efter samme datanode er to forskellige widgets peget på én værdi, og en gentagende subform med <occur max="2"/> producerer flere widgets, der deler et navn og kun adskiller sig i, hvilken datarække de tilhører; evaluér validering og beregning mod dokumentroden, og hver enkelt af dem opløser this til den første matchende node i hele datasets-pakken, så række to stiltiende validerer række én. HotPDF undgår det ved at gemme den opløste DataNode på hver widget-indgang, når layoutet producerer den, og derefter trække den node gennem begge HPDFXFAEvaluateFieldScript-kald, for xfskValidate og xfskCalculate ligeledes. Samme kontekst afgør, hvilken node EnsureValueNode opretter imod, når en beregning målretter en binding, der endnu ikke findes, og når ingen binding kan opløses, fejler commitet rent med XFA calculation target is not bound i stedet for at skrive ind i den forkerte række. FormCalc-semantikken bag disse scripts ekkoer det, AcroForm-dokumenter får fra de handlinger, der beskrives i AcroForm-format og calculate-scripts, men opløsningsreglerne her er XFA-scoped snarere end feltnavn-scoped
Budgetter tjekkes før bivirkninger, ikke efter
Hver grænse i runtimeen er en præcondition, for et budget, der håndhæves efter at allokeringen allerede er sket, er ikke et budget. 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. Underne anvender XFA-DOM'et sine egne TXFADOMLimits: 128 MB-lofter på dekomprimeret input og output, højst 1024 pakker syet sammen, 1000000 noder og en nestingdybde på 256. To detaljer betyder mere end tallene selv. For det første er scriptbudgetterne transaktionsomfattende snarere end pr. script: CommitEdit såer en enkelt resterende-operationer-tæller og én monoton deadline, og hvert validate- og calculate-kald trækker ned i samme tæller og modtager kun de millisekunder, der stadig er tilbage, så en formular med to hundrede beregnende felter ikke kan bruge de fulde 500 ms to hundrede gange. For det andet kommer deadlinen fra en injicerbar MonotonicMilliseconds-funktion, hvilket er det, der gør forløbstidsadfærd reproducerbar i en test-suite i stedet for et møntkast på en travl build-agent
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 transaktionen
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; // affyres kun, når reflow reelt flyttede widgets
end;
// ... betjen formularen ...
finally
Runtime.Free;
end;
end;
Hvor runtimeen stopper, og hvorfor den siger det højt
Runtimeen er bevidst ikke en generel XFA-scriptmotor. DispatchEvent håndterer enter- og exit-aktiviteterne nativt ved at flytte fokus, og for enhver anden aktivitet med et script nægter den med en specifik, stabil diagnostic i stedet for at lade som: scripts, der nævner addInstance, removeInstance eller instanceManager, returnerer XFA runtime does not support event-driven instance mutation, scripts, der rører .presence, returnerer presence-ækvivalenten, og alt andet returnerer XFA runtime does not support this event script. En forudsigelig nægtelse, du kan forgrene på, slår en delvis emulering, der virker på din prøvefil og divergerer på kundens
Trådmodellen er lige så umiddelbar: én runtime-instans tilhører én tråd uden intern låsning, fordi layoutmotoren når tilbage ind i hostmålings-callbacks, og en lås omkring det er en deadlock, der venter på en repaint. Rig content inde i felter følger samme konservative linje som andre steder i biblioteket, hvor exData-payloads håndteres som beskrevet i XFA exData rich text og hyperlinks, og signatur- og knap-widgets kommer tilbage som ReadOnly, mens ikke-understøttede UI-arter viser sig som xwkUnsupported snarere end som en redigerbar tekstboks, der stiltiende mister data
Sat sammen er det et brugbart svar på dynamisk XFA i Delphi: hold DOM'et levende, gør hver redigering til en transaktion, der enten lander fuldstændigt eller efterlader ingenting, afgræns hvert pass, og vær eksplicit om, hvad der er uden for scope. Hvis du evaluerer dette til et skades-, skatte- eller ydelser-workflow, leveres XFA-runtimeen som en del af HotPDF Delphi PDF-komponenten, sammen med AcroForm-, fladnings- og renderingsstierne, som de projekter normalt ender med at behøve sammen