HotPDF wypełnia dynamiczne formularze XFA w Delphi przez TXFAWidgetRuntime, warstwę widżetów neutralną wobec hosta, która traktuje każdą edycję pola jako jedną transakcję: snapshot, walidacja, calculate, reflow, a potem publikacja albo wycofanie w całości. Działa jednowątkowo wewnątrz własnego hosta VCL albo FMX, nie wymaga zainstalowanego Acrobata i egzekwuje każdy budżet, zanim cokolwiek zaalokuje
Scenariusz zna każdy, kto wydał oprogramowanie dokumentowe do pracy dla rządu albo ubezpieczycieli. Formularz roszczenia albo zeznania podatkowego przychodzi jako PDF, którego treść strony to pojedynczy komunikat "Please wait... if this message is not eventually replaced", a każde prawdziwe pole żyje w pakiecie XFA, który renderuje wyłącznie Adobe Acrobat. Twoi użytkownicy chcą go wypełnić wewnątrz twojej aplikacji. Nie uciekniesz też w rastrowanie, bo formularz dokłada wiersze w miarę wpisywania danych, a układ po trzecim wierszu to nie ten układ, który przyjechał w pliku
Dlaczego dynamiczne XFA to wciąż problem wart rozwiązania
Dynamiczne XFA przetrwało, bo wdrożone formularze żyją dłużej niż format, który je niósł. ISO 32000-1 §12.7.8 opisuje XFA jako wpis /XFA w słowniku AcroForm trzymający strumień pakietu XDP, a ISO 32000-2 oznacza cały mechanizm jako przestarzały; deprecacja usunęła go z mapy drogowej, ale nie z terenu, a formularze tworzone według specyfikacji XFA 3.3 są wciąż wystawiane i wciąż prawnie wiążące. Statyczne XFA da się zredukować do zwykłych adnotacji widżetów i HotPDF to robi, gdy wywołasz ApplyXFAAsAcroForm, z kompromisami opisanymi w spłaszczaniu formularzy XFA do pól AcroForm. Dynamiczne XFA to inne zwierzę: jego zakresy occur, rozrastalny tekst i skrypty calculate czynią zbiór pól funkcją danych, więc nie ma stałej listy adnotacji, do której można spłaszczyć, dopóki użytkownik nie skończy pisać. To jest luka, którą wypełnia TXFAWidgetRuntime: trzyma XFA DOM żywy, przelicza układ po każdej przyjętej edycji i wręcza twojemu hostowi płaską tablicę wypozycjonowanych widżetów do rysowania i testowania trafień
Co runtime wręcza aplikacji hosta?
Wręcza ci geometrię i stan, i nic, co zakładałoby toolkit UI. TXFAWidgetRuntime wystawia WidgetCount i Widgets[I] jako rekordy TXFAWidgetState niosące ID, Name, Kind, PageIndex, Bounds w punktach PDF, Value, EditValue oraz flagi Focused, Editing, ReadOnly, Valid, podczas gdy malowanie, rysowanie karetki i routing klawiatury zostają w twoim kodzie. Tożsamość widżetu jest stabilna i porządkowa: każdy widżet dostaje ID postaci name[n], gdzie n liczy wcześniejsze wystąpienia tej nazwy pola w kolejności układu, więc drugi wiersz powtarzalnego subformularza to amount[1]. Ta tożsamość przeżywa przebudowę i nią mówią FocusWidget, BeginEdit, DispatchEvent i HitTest. Dla dokumentu już otwartego w instancji THotPDF CreateLoadedXFAWidgetRuntime wyciąga pakiety XDP, bierze pole pierwszej strony jako rozmiar strony układu i zwraca nil, gdy plik w ogóle nie niesie 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, gdy nie ma /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)]));
// hit test w przestrzeni strony, wygrywa widżet najwyżej
if Runtime.HitTest(0, 120.0, 96.0, WidgetID) then
Runtime.BeginEdit(WidgetID);
finally
Runtime.Free;
end;
finally
Pdf.Free;
end;
end;
Co musi być atomowe przy zatwierdzeniu pola?
Wszystko, czego edycja może dotknąć, a to znacznie więcej niż wartość pola. CommitEdit wywołuje CaptureSnapshot, zanim cokolwiek zapisze, a ten snapshot obejmuje cztery rzeczy: zserializowany XFA DOM z TXFADocument.SaveToBytes, pełną tablicę interakcyjnych rekordów TXFAWidgetState, liczniki LastCalculationPasses i LastReflowPasses oraz bieżące Warnings.Count. Zapisywanie samych wartości węzłów to kuszący skrót i jest błędny, bo skrypt calculate albo nierozwiązane powiązanie może wywołać EnsureValueNode i zmaterializować węzły danych, które nie istniały, gdy edycja się zaczęła; odtworzenie samych wartości nie ma jak ich usunąć, więc odrzucona edycja zostawiłaby trwały strukturalny osad w pakiecie datasets. Sama sekwencja zatwierdzenia jest surowa — zapisz wartość kandydata, wykonaj validate dla edytowanego pola, wykonaj calculate do punktu stałego, potem reflow aż układ się ustabilizuje — a każde niepowodzenie na każdym etapie przechodzi przez FailAndRestore, które wczytuje bajty snapshotu do świeżego TXFADocument, przebudowuje listę widżetów, nanosi zapisane stany interakcji, zeruje liczniki i skraca Warnings z powrotem do długości ze snapshotu. LastDiagnostic trzyma powód niepowodzenia, a w patologicznym przypadku, gdy sama procedura odtwarzania rzuci wyjątek, trzyma literał XFA transaction rollback failed
function EditAmount(Runtime: TXFAWidgetRuntime;
const AWidgetID: AnsiString; const AText: UnicodeString): Boolean;
var
Current: UnicodeString;
begin
Result := False;
if not Runtime.BeginEdit(AWidgetID) then
Exit; // tylko do odczytu albo brak takiego widżetu
Current := Runtime.Widgets[Runtime.FocusedIndex].EditValue;
if not Runtime.ReplaceSelection(0, Length(Current), AText) then
begin
Runtime.CancelEdit; // zły zakres albo rozcięty surogat
Exit;
end;
Result := Runtime.CommitEdit; // wszystko albo nic
if not Result then
// dokument, widżety, liczniki i ostrzeżenia są już z powrotem
// w stanie sprzed edycji; fokusowy widżet jest po prostu nieprawidłowy
ShowMessage(Runtime.LastDiagnostic);
end;
ReplaceSelection zasługuje na własną wzmiankę, bo to najtańsze miejsce na odrzucenie zniekształconego wejścia. Odmawia zaznaczenia rozcinającego parę surogatów UTF-16, odmawia tekstu zastępującego zawierającego niesparowany surogat wysoki albo niski i odmawia wyniku dłuższego niż MaxValueChars. Wyłapanie tego na warstwie klawisza oznacza, że mechanizm transakcji nigdy nie musi odwijać półwpisanego znaku z płaszczyzny astralnej
Przebudowa do prywatnej listy, publikacja jednym swapem
Przebudowa widżetów nigdy nie może być obserwowalna w połowie gotowa, więc RebuildWidgets buduje zupełnie osobną, będącą na własność listę TObjectList i wstawia ją na miejsce jednym przypisaniem na końcu. Powód nie jest estetyczny: TXFALayoutEngine.ComputeLayout działa, dopóki przebudowa jest w locie, i wywołuje kod hosta przez dostarczoną przez ciebie funkcję MeasureText, a może rzucić EXFAWidgetRuntimeError, gdy zostanie trafiony limit widżetów. Gdyby runtime mutował swoją żywą listę w miejscu, każda z tych ścieżek zostawiłaby hosta z listą częściowo starą, częściowo nową, ze wskaźnikami DataNode w dokument, który zaraz zostanie wycofany. Zbieżność reflow rozstrzyga potem LayoutSignature, napis zbudowany z liczby widżetów plus każdego ID, indeksu strony i ramki zaokrąglonej do czterech miejsc: CommitEdit przebudowuje, porównuje sygnatury i powtarza, aż dwie kolejne sygnatury się zgodzą albo budżet przebiegów się wyczerpie. Gdy sygnatura w ogóle się nie zmieniła, LastReflowPasses zostaje 0, po czym poznajesz edycję samych wartości od takiej, która faktycznie rozrosła formularz, a stan interakcji jest przenoszony przez każdą przebudowę po ID widżetu, więc fokus i edycja w toku przeżywają wstawienie wiersza
Dlaczego powiązane pole mogłoby odczytać zły rekord?
Bo skrypt działał bez kontekstu danych. Pole niosące jawne <bind match="dataRef" ref="$record.actual"/> i pole nazwane jak ten sam węzeł danych to dwa różne widżety wskazujące na jedną wartość, a powtarzalny subformularz z <occur max="2"/> produkuje kilka widżetów, które dzielą nazwę i różnią się tylko tym, do którego wiersza danych należą; oceń walidację i wyliczanie względem korzenia dokumentu, a każdy z nich rozwiąże this na pierwszy pasujący węzeł w całym pakiecie datasets, więc wiersz drugi po cichu waliduje wiersz pierwszy. HotPDF unika tego, przechowując rozwiązany DataNode na każdym wpisie widżetu, gdy tylko układ go wyprodukuje, a potem przepuszczając ten węzeł przez oba wywołania HPDFXFAEvaluateFieldScript, dla xfskValidate i xfskCalculate jednakowo. Ten sam kontekst rozstrzyga, względem którego węzła EnsureValueNode tworzy, gdy wyliczanie celuje w powiązanie, które jeszcze nie istnieje, a gdy żadnego powiązania nie da się rozwiązać, zatwierdzenie kończy się czysto z XFA calculation target is not bound zamiast pisać w zły wiersz. Semantyka FormCalc za tymi skryptami odpowiada temu, co dokumenty AcroForm dostają z akcji opisanych w skryptach format i calculate AcroForm, ale reguły rozwiązywania tutaj są zakresem XFA, a nie nazwy pola
Budżety są sprawdzane przed efektami ubocznymi, nie po nich
Każdy limit w runtime jest warunkiem wstępnym, bo budżet egzekwowany po tym, jak alokacja już się wydarzyła, nie jest budżetem. TXFAWidgetRuntimeOptions.Default przychodzi z MaxWidgets 10000, MaxValueChars 1048576, MaxCalculationPasses 16 i MaxReflowPasses 4, a domyślne TXFAFormScriptOptions niosą MaxOperations 100000 i MaxElapsedMilliseconds 500. Pod spodem XFA DOM nakłada własne TXFADOMLimits: sufity 128 MB na rozpakowane wejście i wyjście, najwyżej 1024 zszyte pakiety, 1000000 węzłów i głębokość zagnieżdżenia 256. Dwa detale ważą więcej niż same liczby. Po pierwsze, budżety skryptów są transakcyjne, a nie na skrypt: CommitEdit zasiewa jeden licznik pozostałych operacji i jeden monotoniczny deadline, a każde wywołanie validate i calculate dobiera z tego samego licznika i dostaje tylko te milisekundy, które jeszcze zostały, więc formularz z dwustoma polami wyliczającymi nie może wydać pełnych 500 ms dwaset razy. Po drugie, deadline pochodzi z wstrzykiwalnej funkcji MonotonicMilliseconds, co czyni zachowanie czasowe odtwarzalnym w teście zamiast rzutem monetą na zajęty agent budowania
var
Options: TXFAWidgetRuntimeOptions;
Runtime: TXFAWidgetRuntime;
begin
Options := TXFAWidgetRuntimeOptions.Default;
Options.MaxWidgets := 2000; // domyślnie 10000
Options.MaxCalculationPasses := 8; // domyślnie 16
Options.MaxReflowPasses := 2; // domyślnie 4
Options.ScriptOptions.Limits.MaxOperations := 20000; // cała transakcja
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; // odpalane tylko, gdy reflow faktycznie ruszył widżety
end;
// ... obsługa formularza ...
finally
Runtime.Free;
end;
end;
Gdzie runtime się zatrzymuje i dlaczego mówi to wprost
Runtime celowo nie jest ogólnym silnikiem skryptowym XFA. DispatchEvent obsługuje natywnie aktywności enter i exit przez przesunięcie fokusa, a dla każdej innej aktywności niosącej skrypt odmawia ze specyficzną, stabilną diagnostyką zamiast udawać: skrypty wspominające addInstance, removeInstance albo instanceManager zwracają XFA runtime does not support event-driven instance mutation, skrypty dotykające .presence zwracają odpowiednik dla presence, a wszystko inne zwraca XFA runtime does not support this event script. Przewidywalna odmowa, na której można się rozgałęzić, bije częściową emulację, która działa na twoim pliku próbnym i rozjeżdża się u klienta
Model wątków jest równie bezceremonialny: jedna instancja runtime należy do jednego wątku, bez wewnętrznych blokad, bo silnik układu sięga z powrotem do wywołań zwrotnych pomiaru hosta, a blokada wokół tego to czekający na przemalowanie deadlock. Rich content wewnątrz pól idzie tą samą konserwatywną linią co gdzie indziej w bibliotece, gdzie ładunki exData są obsługiwane tak, jak opisano w tekstach sformatowanych i hiperłączach XFA exData, a widżety podpisu i przycisku wracają jako ReadOnly, podczas gdy nieobsługiwane rodzaje UI wynurzają się jako xwkUnsupported, a nie jako edytowalne pole tekstowe po cichu gubiące dane
Złożone razem, to wykonalna odpowiedź na dynamiczne XFA w Delphi: trzymaj DOM żywy, zrób z każdej edycji transakcję, która albo ląduje w całości, albo nie zostawia niczego, ogranicz każdy przebieg i mów wprost, co jest poza zakresem. Jeśli oceniasz to dla przepływu roszczeń, podatków albo świadczeń, runtime XFA przychodzi jako część komponentu HotPDF Delphi PDF, obok ścieżek AcroForm, spłaszczania i renderowania, których te projekty zwykle i tak potrzebują razem