HotPDF заповнює динамічні XFA-форми в Delphi через TXFAWidgetRuntime, нейтральний до хоста шар віджетів, що трактує кожне редагування поля як одну транзакцію: знімок, валідація, обчислення, reflow, а потім публікація або повний відкат. Він працює в одну нитку всередині вашого власного хоста VCL чи FMX, не потребує встановленого Acrobat і застосовує кожен бюджет до того, як щось виділить
Сценарій знайомий кожному, хто відгружав документний софт у державні чи страхові проєкти. Форма претензії чи податкова декларація приходить як PDF, чиїй сторінковий вміст — одиночне повідомлення «Please wait... if this message is not eventually replaced», а всі справжні поля живуть у XFA-пакеті, який рендерить лише Adobe Acrobat. Ваші користувачі хочуть заповнювати його всередині вашої програми. Ви не можете вийти з ситуації растеризацією, бо форма вирощує рядки в міру введення даних, і компонування після третього рядка — не те, що було відвантажене у файлі
Чому динамічний XFA досі проблема, варта розвʼязання
Динамічний XFA виживає, бо розгорнуті форми переживають формат, що їх ніс. ISO 32000-1 §12.7.8 описує XFA як запис /XFA у словнику AcroForm, що тримає пакетний потік XDP, а ISO 32000-2 виводить увесь механізм з ужитку; deprecation прибрав його з дорожньої карти, а не з поля, і форми, створені за специфікацією XFA 3.3, досі випускаються і досі мають юридичну силу. Статичний XFA можна згорнути до звичайних анотацій-віджетів, і HotPDF це робить, коли ви викликаєте ApplyXFAAsAcroForm, з компромісами, покритими в згортанні XFA-форм у поля AcroForm. Динамічний XFA — звір інший: його діапазони occur, текст, що росте, і скрипти calculate роблять набір полів функцією даних, тож фіксованого списку анотацій, до якого можна згорнути, не існує, доки користувач не скінчив друкувати. Саме ту прогалину заповнює TXFAWidgetRuntime, тримаючи XFA DOM живим, перераховуючи компонування після кожного прийнятого редагування і віддаючи вашому хосту плоский масив позиціонованих віджетів для малювання та перевірки влучань
Що рантайм віддає хост-застосунку?
Він віддає вам геометрію і стан, і ніщо, що припускає UI-тулкіт. TXFAWidgetRuntime відкриває WidgetCount і Widgets[I] як записи TXFAWidgetState, що несуть ID, Name, Kind, PageIndex, Bounds у PDF-пунктах, Value, EditValue і прапорці Focused, Editing, ReadOnly, Valid, тоді як малювання, каретка і маршрутизація клавіатури лишаються у вашому коді. Ідентичність віджета стабільна й ординальна: кожен віджет отримує ID форми name[n], де n рахує попередні входження того імені поля в порядку компонування, тож другий рядок повторюваного субформу — це amount[1]. Та ідентичність переживає перебудову, і нею говорять FocusWidget, BeginEdit, DispatchEvent і HitTest. Для документа, вже відкритого в екземплярі THotPDF, CreateLoadedXFAWidgetRuntime витягує XDP-пакети, бере перший сторінковий бокс як розмір сторінки компонування і повертає nil, коли файл узагалі не несе 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, коли /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)]));
// перевірка влучання у просторі сторінки, найвищий віджет виграє
if Runtime.HitTest(0, 120.0, 96.0, WidgetID) then
Runtime.BeginEdit(WidgetID);
finally
Runtime.Free;
end;
finally
Pdf.Free;
end;
end;
Що має бути атомарним, коли поле комітується?
Усе, чого може торкнутися редагування, а це помітно більше за значення поля. CommitEdit викликає CaptureSnapshot перед будь-яким записом, і той знімок покриває чотири речі: серіалізований XFA DOM із TXFADocument.SaveToBytes, повний масив інтеракційних записів TXFAWidgetState, лічильники LastCalculationPasses і LastReflowPasses та поточний Warnings.Count. Зберігати самі значення вузлів — спокусливий обхідний шлях, і він неправильний, бо скрипт calculate чи невирішена привʼязка може викликати EnsureValueNode і матеріалізувати вузли даних, яких не існувало, коли редагування починалося; відновлення лише значень не має способу їх прибрати, тож відхилене редагування лишило б постійний структурний осад у пакеті datasets. Сама послідовність коміту строга — записати кандидата-значення, прогнати validate для редагованого поля, прогнати calculate до нерухомої точки, потім reflow, доки компонування не стабілізується, — і будь-яка невдача на будь-якій стадії йде через FailAndRestore, який перезавантажує байти знімка в свіжий TXFADocument, перебудовує список віджетів, повторно застосовує записані інтеракційні стани, скидає лічильники і обрізає Warnings назад до довжини знімка. LastDiagnostic тримає причину при невдачі і тримає літеральний 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; // лише для читання, або такого віджета немає
Current := Runtime.Widgets[Runtime.FocusedIndex].EditValue;
if not Runtime.ReplaceSelection(0, Length(Current), AText) then
begin
Runtime.CancelEdit; // поганий діапазон, або розрізаний сурогат
Exit;
end;
Result := Runtime.CommitEdit; // усе-або-нічого
if not Result then
// документ, віджети, лічильники і попередження вже повернулися
// до передредаційного стану; фокусний віджет просто позначений невалідним
ShowMessage(Runtime.LastDiagnostic);
end;
ReplaceSelection заслуговує власної нотатки, бо саме там пошкоджений вхід найдешевше відхилити. Вона відмовляє вибору, що розрізає сурогатну пару UTF-16, відмовляє тексту заміни з непарним старшим чи молодшим сурогатом і відмовляє будь-якому результату довжиною понад MaxValueChars. Впіймати це на шарі клавіш означає, що транзакційній машині ніколи не доведеться розмотувати напівзаписаний символ астральної площини
Перебудова в приватний список, публікація одним свопом
Перебудова віджетів ніколи не повинна бути спостережуваною напівзавершеною, тож RebuildWidgets будує цілком окремий власний TObjectList і підміняє його на місце одним присвоєнням у кінці. Причина не в естетиці: TXFALayoutEngine.ComputeLayout працює, поки перебудова в польоті, і телефонує в хост-код через надану вами функцію MeasureText, і вона може кинути EXFAWidgetRuntimeError, коли досягнуто ліміту віджетів. Якби рантайм мутував свій живий список на місці, будь-який із шляхів лишив би хост зі списком, частково старого компонування і частково нового, з вказівниками DataNode у документ, що ось-ось відкотиться. Збіжність reflow тоді вирішується LayoutSignature, рядком, зібраним з кількості віджетів плюс кожен ID,pageIndex і обмежувальний бокс, округлені до чотирьох знаків: CommitEdit перебудовує, порівнює підписи і повторює, доки два послідовні підписи не збігаються або не вичерпано бюджет проходів. Коли підпис узагалі не змінився, LastReflowPasses лишається 0 — так ви відрізняєте редагування лише значення від того, що справді виростило форму, — а інтеракційний стан переноситься крізь кожну перебудову за ID віджета, тож фокус і редагування, що триває, переживають вставлення рядка
Чому привʼязане поле читає неправильний запис?
Бо скрипт виконався без контексту даних. Поле з явним <bind match="dataRef" ref="$record.actual"/> і поле, назване на честь того самого вузла даних, — два різні віджети, вказані на одне значення, а повторюваний субформ із <occur max="2"/> породжує кілька віджетів, що поділяють імʼя і відрізняються лише тим, до якого рядка даних належать; оцініть валідацію та обчислення від кореня документа — і кожен із них розвʼяже this в перший збіжний вузол усього пакету datasets, тож рядок два тихо валідує рядок один. HotPDF уникає цього, зберігаючи розвʼязаний DataNode у кожному записі віджета, коли компонування його породжує, а потім протягуючи той вузол крізь обидва виклики HPDFXFAEvaluateFieldScript, для xfskValidate і xfskCalculate однаково. Той самий контекст вирішує, проти якого вузла EnsureValueNode створює, коли обчислення цілиться в привʼязку, що ще не існує, а коли жодну привʼязку розвʼязати неможливо, коміт чисто провалюється з XFA calculation target is not bound, замість писати в неправильний рядок. Семантика FormCalc за тими скриптами віддзеркалює те, що AcroForm-документи отримують від дій, описаних у форматі AcroForm і скриптах calculate, але правила розвʼязання тут мають сферу XFA, а не імені поля
Бюджети перевіряються до побічних ефектів, а не після
Кожен ліміт у рантаймі — передумова, бо бюджет, застосований після того, як виділення вже сталося, — не бюджет. TXFAWidgetRuntimeOptions.Default постачає MaxWidgets у 10000, MaxValueChars у 1048576, MaxCalculationPasses у 16 і MaxReflowPasses у 4, а типові TXFAFormScriptOptions несуть MaxOperations у 100000 з MaxElapsedMilliseconds у 500. Знизу XFA DOM застосовує власні TXFADOMLimits: стелі 128 МБ на декомпресований вхід і вихід, щонайбільше 1024 зшитих пакетів, 1000000 вузлів і глибина вкладеності 256. Дві деталі важать більше за самі числа. По-перше, скриптові бюджети — на всю транзакцію, а не на скрипт: CommitEdit засіває один лічильник залишкових операцій і один монотонний дедлайн, і кожен виклик validate і calculate знімає з того самого лічильника і отримує лише мілісекунди, що ще лишилися, тож форма з двома сотнями обчислювальних полів не може витратити повні 500 мс двісті разів. По-друге, дедлайн походить з інʼєктовної функції MonotonicMilliseconds, і саме це робить поведінку часу відтворюваною в тестовому наборі, а не кидком монети на завантаженому build-агенті
var
Options: TXFAWidgetRuntimeOptions;
Runtime: TXFAWidgetRuntime;
begin
Options := TXFAWidgetRuntimeOptions.Default;
Options.MaxWidgets := 2000; // типово 10000
Options.MaxCalculationPasses := 8; // типово 16
Options.MaxReflowPasses := 2; // типово 4
Options.ScriptOptions.Limits.MaxOperations := 20000; // вся транзакція
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; // вогонь лише коли reflow справді рушив віджети
end;
// ... ведіть форму ...
finally
Runtime.Free;
end;
end;
Де рантайм зупиняється і чому каже це вголос
Рантайм навмисно не є універсальним скриптовим рушієм XFA. DispatchEvent обробляє активності enter і exit нативно переміщенням фокуса, а для кожної іншої активності зі скриптом відмовляє специфічною, стабільною діагностикою, замість прикидатися: скрипти, що згадують addInstance, removeInstance чи instanceManager, повертають XFA runtime does not support event-driven instance mutation, скрипти, що торкаються .presence, повертають presence-еквівалент, а все інше повертає XFA runtime does not support this event script. Передбачувана відмова, на якій можна розгілкуватися, краща за часткову емуляцію, що працює на вашому прикладному файлі і розходиться на файлі клієнта
Ниткова модель так само пряма: один екземпляр рантайма належить одній нитці, без внутрішнього блокування, бо механізм компонування тягнеться назад у хост-колбеки вимірювання, а блокування навколо того — дедлок у очікуванні перемальовки. Багатий вміст усередині полів іде тією самою консервативною лінією, що й решта бібліотеки, де навантаження exData обробляються, як описано в XFA exData, збагаченому тексті та гіперпосиланнях, а віджети підпису і кнопки повертаються як ReadOnly, тоді як непідтримувані UI-види виринають як xwkUnsupported, а не як текстовий бокс для редагування, що тихо втрачає дані
Разом узяте, це робоча відповідь на динамічний XFA в Delphi: тримати DOM живим, робити кожне редагування транзакцією, що або приземляється повністю, або не лишає нічого, обмежувати кожен прохід і бути явним щодо того, що поза сферою. Якщо ви оцінюєте це для робочого потоку претензій, податків чи допомоги, XFA-рантайм постачається як частина HotPDF Delphi PDF component, поруч зі шляхами AcroForm, згортання та рендерингу, які ті проєкти зазвичай кінчають потребувати разом