HotPDF vult dynamische XFA-formulieren in Delphi via TXFAWidgetRuntime, een hostneutrale widgetlaag die elke veldbewerking als één transactie behandelt: snapshot, validate, calculate, reflow en dan publiceren of volledig teruggedraaien. De laag draait single-threaded binnen uw eigen VCL- of FMX-host, heeft geen Acrobat-installatie nodig en handhaaft elk budget voordat er ook maar iets wordt toegewezen
Het scenario is bekend voor iedereen die documentsoftware heeft geleverd voor overheden of verzekeraars. Een schadeformulier of belastingaangifte komt binnen als PDF waarvan de pagina-inhoud één enkele "Please wait... if this message is not eventually replaced"-melding is, en elk echt veld ligt in een XFA-pakket dat alleen Adobe Acrobat rendert. Uw gebruikers willen het in uw eigen applicatie invullen. Uit rasteriseren komt u ook niet, want het formulier groeit rijen aan wanneer data wordt ingevoerd, en de lay-out na de derde rij is niet de lay-out die in het bestand zat
Waarom dynamische XFA nog steeds het oplossen waard is
Dynamische XFA blijft bestaan omdat uitgerolde formulieren het formaat dat ze droeg overleven. ISO 32000-1 §12.7.8 beschrijft XFA als een /XFA-entry in het AcroForm-dictionary met een XDP-pakketstream, en ISO 32000-2 markeert het hele mechanisme als verouderd; die depreciatie haalde het van de roadmap, niet uit het veld, en formulieren op basis van de XFA 3.3-specificatie worden nog steeds uitgegeven en zijn nog steeds juridisch bindend. Statische XFA is te reduceren tot gewone widgetannotaties, en HotPDF doet dat wanneer u ApplyXFAAsAcroForm aanroept, met de afwegingen die in XFA-formulieren afvlakken naar AcroForm-velden worden behandeld. Dynamische XFA is een ander beestje: de occur-bereiken, meegroeiende tekst en calculate-scripts maken de veldset een functie van de data, dus er is geen vaste annotatielijst om naar af te vlakken zolang de gebruiker nog aan het typen is. Precies dat gat vult TXFAWidgetRuntime, door de XFA-DOM levend te houden, na elke geaccepteerde bewerking de lay-out opnieuw te berekenen en uw host een platte array van gepositioneerde widgets te geven om te tekenen en te hit-testen
Wat geeft de runtime een hostapplicatie?
U krijgt geometrie en toestand, en niets dat een UI-toolkit veronderstelt. TXFAWidgetRuntime legt WidgetCount en Widgets[I] bloot als TXFAWidgetState-records met ID, Name, Kind, PageIndex, Bounds in PDF-punten, Value, EditValue en de vlaggen Focused, Editing, ReadOnly en Valid, terwijl tekenen, caret-tekening en toetsenbordroutering in uw eigen code blijven. Widgetidentiteit is stabiel en ordinaal: elke widget krijgt een ID van de vorm name[n], waarin n eerdere voorkomens van die veldnaam in lay-outvolgorde telt, dus de tweede rij van een herhalende subform is amount[1]. Die identiteit overleeft een rebuild, en het is ook wat FocusWidget, BeginEdit, DispatchEvent en HitTest allemaal spreken. Voor een document dat al open ligt in een THotPDF-instantie extraheert CreateLoadedXFAWidgetRuntime de XDP-pakketten, neemt de eerste page box als lay-outpaginaformaat en geeft nil terug wanneer het bestand helemaal geen XFA bevat
var
Pdf: THotPDF;
Runtime: TXFAWidgetRuntime;
WidgetID: AnsiString;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('claim-dynamic.pdf');
Runtime := Pdf.CreateLoadedXFAWidgetRuntime; // nil wanneer er geen /XFA is
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 in paginaruimte, de bovenste widget wint
if Runtime.HitTest(0, 120.0, 96.0, WidgetID) then
Runtime.BeginEdit(WidgetID);
finally
Runtime.Free;
end;
finally
Pdf.Free;
end;
end;
Wat moet atomisch zijn wanneer een veld wordt gecommit?
Alles wat de bewerking kan raken, en dat is aanzienlijk meer dan de veldwaarde. CommitEdit roept CaptureSnapshot aan voordat er iets wordt weggeschreven, en die snapshot omvat vier dingen: de geserialiseerde XFA-DOM uit TXFADocument.SaveToBytes, de volledige array met TXFAWidgetState-interactierecords, de tellers LastCalculationPasses en LastReflowPasses, en de huidige Warnings.Count. Alleen knooppuntwaarden opslaan is de verleidelijke shortcut en die is fout, want een calculate-script of een onopgeloste binding kan EnsureValueNode aanroepen en datanodes materialiseren die nog niet bestonden toen de bewerking begon; een herstel van alleen waarden kan ze niet weghalen, dus een afgewezen bewerking zou blijvende structurele restanten in het datasets-pakket achterlaten. De commitvolgorde zelf is streng — de kandidaatwaarde schrijven, validate draaien voor het bewerkte veld, calculate draaien tot een vast punt, en dan reflowen tot de lay-out stabiel is — en elke mislukking in elke fase loopt via FailAndRestore, dat de snapshotbytes in een verse TXFADocument laadt, de widgetlijst herbouwt, de vastgelegde interactietoestanden opnieuw toepast, de tellers reset en Warnings terugknipt tot zijn snapshotlengte. LastDiagnostic bevat bij mislukking de reden, en bevat de letterlijke tekst XFA transaction rollback failed in het pathologische geval waarin het herstel zelf een exceptie gooit
function EditAmount(Runtime: TXFAWidgetRuntime;
const AWidgetID: AnsiString; const AText: UnicodeString): Boolean;
var
Current: UnicodeString;
begin
Result := False;
if not Runtime.BeginEdit(AWidgetID) then
Exit; // alleen-lezen, of widget bestaat niet
Current := Runtime.Widgets[Runtime.FocusedIndex].EditValue;
if not Runtime.ReplaceSelection(0, Length(Current), AText) then
begin
Runtime.CancelEdit; // ongeldige range, of gesplitst surrogaat
Exit;
end;
Result := Runtime.CommitEdit; // alles-of-niets
if not Result then
// document, widgets, tellers en waarschuwingen staan alweer op de
// toestand van vóór de bewerking; de gefocuste widget is alleen
// als ongeldig gemarkeerd
ShowMessage(Runtime.LastDiagnostic);
end;
ReplaceSelection verdient een eigen noot, want hier is misvormde input het goedkoopst af te wijzen. Het weigert een selectie die een UTF-16-surrogaatpaar splitst, weigert vervangende tekst met een ongepaarde hoge of lage surrogaat, en weigert elk resultaat langer dan MaxValueChars. Dat op toetsniveau afvangen betekent dat de transactiemachinerie nooit een halfgeschreven teken van het astrale vlak hoeft terug te draaien
Herbouwen in een privélijst, publiceren in één swap
Een widget-rebuild mag nooit half af waarneembaar zijn, dus RebuildWidgets bouwt een volledig aparte owning TObjectList en zet die op het einde met één toewijzing op zijn plek. De reden is geen esthetiek: TXFALayoutEngine.ComputeLayout draait terwijl de rebuild nog loopt en roept hostcode terug via de door u geleverde functie MeasureText, en kan een EXFAWidgetRuntimeError gooien zodra de widgetlimiet is bereikt. Als de runtime zijn live lijst ter plekke zou muteren, zou elk van beide paden de host met een lijst achterlaten die deels de oude en deels de nieuwe lay-out is, met DataNode-pointers naar een document dat op het punt staat teruggerold te worden. Reflow-convergentie wordt daarna beslist door LayoutSignature, een string opgebouwd uit het widgetaantal plus elke ID, pagina-index en bounding box afgerond op vier decimalen: CommitEdit herbouwt, vergelijkt signaturen en herhaalt tot twee opeenvolgende signaturen overeenkomen of het passbudget uitgeput is. Als de signature helemaal nooit veranderde, blijft LastReflowPasses op 0 staan, waaraan u een bewerking die alleen een waarde wijzigt onderscheidt van één die het formulier echt laat groeien, en interactietoestand wordt bij elke rebuild meegevoerd via de widget-ID, zodat focus en lopende bewerking een rij-insertie overleven
Waarom zou een gebonden veld de verkeerde record lezen?
Omdat het script zonder datacontext draaide. Een veld met een expliciete <bind match="dataRef" ref="$record.actual"/> en een veld dat naar diezelfde datanode is genoemd zijn twee verschillende widgets die naar één waarde wijzen, en een herhalende subform met <occur max="2"/> levert meerdere widgets die een naam delen en alleen verschillen in de datarij waartoe ze behoren; valideert en berekent u tegen de documentroot, dan lost elk van hen this op naar de eerste matchende node in het hele datasets-pakket, dus rij twee valideert stilletjes rij één. HotPDF voorkomt dat door de opgeloste DataNode op elke widgetentry op te slaan zodra de lay-out die oplevert, en die node daarna door beide HPDFXFAEvaluateFieldScript-aanroepen te voeren, voor xfskValidate en xfskCalculate eveneens. Dezelfde context bepaalt tegen welke node EnsureValueNode aanmaakt wanneer een berekening een binding richt die nog niet bestaat, en wanneer geen enkele binding op te lossen is, faalt de commit netjes met XFA calculation target is not bound in plaats van in de verkeerde rij te schrijven. De FormCalc-semantiek achter die scripts klinkt als wat AcroForm-documenten krijgen uit de actions die in AcroForm-format- en calculate-scripts worden beschreven, maar de resolutieregels hier zijn XFA-scoped en niet veldnaam-scoped
Budgets worden vóór bijwerkingen gecontroleerd, niet erna
Elke limiet in de runtime is een preconditie, want een budget dat pas wordt gehandhaafd nadat de toewijzing al is gebeurd is geen budget. TXFAWidgetRuntimeOptions.Default levert MaxWidgets op 10000, MaxValueChars op 1048576, MaxCalculationPasses op 16 en MaxReflowPasses op 4, en de standaard TXFAFormScriptOptions hebben MaxOperations op 100000 met MaxElapsedMilliseconds op 500. Daaronder hanteert de XFA-DOM zijn eigen TXFADOMLimits: plafonds van 128 MB op gedecomprimeerde in- en uitvoer, hooguit 1024 aan elkaar genaaide pakketten, 1000000 nodes en een nestingsdiepte van 256. Twee details wegen zwaarder dan de getallen zelf. Ten eerste zijn de scriptbudgets transactiebreed in plaats van per script: CommitEdit zaait één resterende-operaties-teller en één monotone deadline, en elke validate- en calculate-aanroep trekt aan diezelfde teller en krijgt alleen de nog resterende milliseconden, dus een formulier met tweehonderd rekenvelden kan de volledige 500 ms niet tweehonderd keer opmaken. Ten tweede komt de deadline uit een injecteerbare functie MonotonicMilliseconds, wat verstreken-tijdgedrag reproduceerbaar maakt in een testsuite in plaats van een muntgooi op een drukke build-agent
var
Options: TXFAWidgetRuntimeOptions;
Runtime: TXFAWidgetRuntime;
begin
Options := TXFAWidgetRuntimeOptions.Default;
Options.MaxWidgets := 2000; // standaard 10000
Options.MaxCalculationPasses := 8; // standaard 16
Options.MaxReflowPasses := 2; // standaard 4
Options.ScriptOptions.Limits.MaxOperations := 20000; // hele transactie
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; // wordt alleen gegeven wanneer reflow widgets echt verplaatst
end;
// ... het formulier bedienen ...
finally
Runtime.Free;
end;
end;
Waar de runtime stopt, en waarom het dat luidop zegt
De runtime is met opzet geen algemene XFA-scriptingengine. DispatchEvent behandelt de enter- en exit-activiteiten native door de focus te verplaatsen, en voor elke andere activiteit met een script weigert het met een specifieke, stabiele diagnostic in plaats van te doen alsof: scripts die addInstance, removeInstance of instanceManager noemen geven XFA runtime does not support event-driven instance mutation terug, scripts die .presence raken geven het presence-equivalent, en al het restant geeft XFA runtime does not support this event script terug. Een voorspelbare weigering waar u in code op kunt inspelen verslaat een partiële emulatie die op uw voorbeeldbestand werkt en bij de klant uit de pas loopt
Het threadingmodel is even bot: één runtime-instantie hoort bij één thread, zonder interne locking, want de lay-outengine reikt terug naar hostmeetcallbacks en een lock daaromheen is een deadlock die op een repaint wacht. Rich content in velden volgt dezelfde conservatieve lijn als elders in de bibliotheek, waar exData-payloads worden afgehandeld zoals beschreven in XFA exData rich text en hyperlinks, en signature- en button-widgets komen als ReadOnly terug terwijl niet-ondersteunde UI-soorten als xwkUnsupported naar boven komen in plaats van als een bewerkbaar tekstvak dat stilletjes data verliest
Alles bij elkaar is dat een werkbaar antwoord op dynamische XFA in Delphi: houd de DOM levend, maak van elke bewerking een transactie die of volledig landt of niets achterlaat, zet elke pass op een budget, en wees expliciet over wat buiten scope ligt. Wie dit evalueert voor een workflow rond schadeclaims, belastingen of uitkeringen: de XFA-runtime wordt geleverd als onderdeel van de HotPDF Delphi PDF-component, samen met de AcroForm-, afvlakkings- en renderingpaden die zulke projecten doorgaans uiteindelijk gezamenlijk nodig hebben