HotPDF mengisi form XFA dinamis di Delphi melalui TXFAWidgetRuntime, lapisan widget netral-host yang memperlakukan setiap edit field sebagai satu transaksi: snapshot, validate, calculate, reflow, lalu publikasikan atau batalkan seluruhnya. Ia berjalan single-threaded di dalam host VCL atau FMX milik Anda sendiri, tidak butuh Acrobat terpasang, dan menegakkan setiap batas sebelum mengalokasikan apa pun
Skenario ini akrab bagi siapa pun yang pernah mengirim perangkat lunak dokumen ke pekerjaan pemerintah atau asuransi. Form klaim atau pelaporan pajak tiba sebagai PDF yang konten halamannya hanya satu pemberitahuan "Please wait... if this message is not eventually replaced", dan semua field sungguhan berada dalam paket XFA yang hanya dirender Adobe Acrobat. Pengguna Anda ingin mengisinya di dalam aplikasi Anda. Anda juga tidak bisa keluar dengan merasterisasi, karena form bertambah baris saat data dimasukkan, dan tata letak setelah baris ketiga bukan tata letak yang dikirim dalam file
Mengapa XFA dinamis masih menjadi masalah yang layak dipecahkan
XFA dinamis bertahan karena form yang sudah terpasang hidup lebih lama daripada format yang membawanya. ISO 32000-1 §12.7.8 menggambarkan XFA sebagai entri /XFA pada kamus AcroForm yang memuat stream paket XDP, dan ISO 32000-2 mendepraciasi seluruh mekanisme itu; deprekasi mengeluarkannya dari peta jalan, bukan dari lapangan, dan form yang dibuat terhadap spesifikasi XFA 3.3 masih diterbitkan dan masih sah secara hukum. XFA statis bisa direduksi menjadi anotasi widget biasa, dan HotPDF melakukan itu saat Anda memanggil ApplyXFAAsAcroForm, dengan trade-off yang dibahas di meratakan form XFA menjadi field AcroForm. XFA dinamis adalah binatang yang berbeda: rentang occur-nya, teks yang bisa tumbuh, dan skrip calculate menjadikan kumpulan field sebuah fungsi dari data, sehingga tidak ada daftar anotasi tetap untuk diratakan sampai pengguna selesai mengetik. Celah itulah yang diisi TXFAWidgetRuntime, dengan menjaga DOM XFA tetap hidup, menghitung ulang tata letak setelah setiap edit yang diterima, dan menyerahkan ke host Anda array datar berisi widget berposisi untuk digambar dan diuji hit
Apa yang runtime serahkan pada aplikasi host?
Ia menyerahkan geometri dan state, dan tidak ada yang mengasumsikan toolkit UI. TXFAWidgetRuntime mengekspos WidgetCount dan Widgets[I] sebagai record TXFAWidgetState yang membawa ID, Name, Kind, PageIndex, Bounds dalam poin PDF, Value, EditValue, serta flag Focused, Editing, ReadOnly, Valid, sementara penggambaran, caret, dan perutean keyboard tetap di kode Anda. Identitas widget stabil dan ordinal: setiap widget mendapat ID berbentuk name[n], dengan n menghitung kemunculan sebelumnya dari nama field itu dalam urutan tata letak, sehingga baris kedua dari subform berulang adalah amount[1]. Identitas itulah yang selamat dari rebuild, dan itulah bahasa yang dipakai FocusWidget, BeginEdit, DispatchEvent, dan HitTest. Untuk dokumen yang sudah terbuka di instance THotPDF, CreateLoadedXFAWidgetRuntime mengekstrak paket XDP, mengambil page box pertama sebagai ukuran halaman tata letak, dan mengembalikan nil saat file tidak membawa XFA sama sekali
var
Pdf: THotPDF;
Runtime: TXFAWidgetRuntime;
WidgetID: AnsiString;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('claim-dynamic.pdf');
Runtime := Pdf.CreateLoadedXFAWidgetRuntime; // nil saat tidak ada /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 ruang halaman, widget teratas menang
if Runtime.HitTest(0, 120.0, 96.0, WidgetID) then
Runtime.BeginEdit(WidgetID);
finally
Runtime.Free;
end;
finally
Pdf.Free;
end;
end;
Apa yang harus atomik saat sebuah field dikomit?
Segala yang bisa disentuh edit itu, dan itu jauh lebih banyak daripada nilai field. CommitEdit memanggil CaptureSnapshot sebelum menulis apa pun, dan snapshot itu mencakup empat hal: DOM XFA terserialisasi dari TXFADocument.SaveToBytes, seluruh array record interaksi TXFAWidgetState, counter LastCalculationPasses dan LastReflowPasses, serta Warnings.Count saat ini. Hanya menyimpan nilai node adalah jalan pintas yang menggoda dan itu keliru, karena skrip calculate atau binding yang belum terselesaikan bisa memanggil EnsureValueNode dan mematerialkan node data yang tidak ada saat edit dimulai; pemulihan hanya-nilai tidak punya cara menghapusnya, sehingga edit yang ditolak akan meninggalkan residu struktural permanen di paket datasets. Urutan komitnya sendiri ketat — tulis nilai kandidat, jalankan validate untuk field yang diedit, jalankan calculate sampai titik tetap, lalu reflow hingga tata letak stabil — dan kegagalan di tahap mana pun bermuara ke FailAndRestore, yang memuat ulang byte snapshot ke TXFADocument baru, membangun ulang daftar widget, menerapkan kembali state interaksi yang tercatat, mereset counter, dan memangkas Warnings kembali ke panjang snapshot-nya. LastDiagnostic menyimpan alasannya saat gagal, dan menyimpan literal XFA transaction rollback failed dalam kasus patologis saat pemulihan itu sendiri melempar exception
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, atau widget tidak ada
Current := Runtime.Widgets[Runtime.FocusedIndex].EditValue;
if not Runtime.ReplaceSelection(0, Length(Current), AText) then
begin
Runtime.CancelEdit; // rentang buruk, atau surrogate terpisah
Exit;
end;
Result := Runtime.CommitEdit; // semua-atau-tidak-sama-sekali
if not Result then
// dokumen, widget, counter, dan peringatan sudah kembali ke state
// pra-edit; widget yang fokus hanya ditandai tidak valid
ShowMessage(Runtime.LastDiagnostic);
end;
ReplaceSelection layak mendapat catatan tersendiri, karena di situlah input cacat paling murah untuk ditolak. Ia menolak seleksi yang membelah pasangan surrogate UTF-16, menolak teks pengganti yang memuat high atau low surrogate tanpa pasangan, dan menolak hasil apa pun yang lebih panjang dari MaxValueChars. Menangkapnya di lapisan ketukan keyboard berarti mesin transaksi tidak pernah perlu menguraikan karakter astral yang tercatat setengah jadi
Membangun ulang ke daftar privat, mempublikasikan dalam satu swap
Pembangunan ulang widget tidak boleh terlihat setengah jadi, jadi RebuildWidgets membangun TObjectList pemilik yang benar-benar terpisah dan menempatkannya dengan satu assignment di akhir. Alasannya bukan estetika: TXFALayoutEngine.ComputeLayout berjalan saat rebuild sedang berlangsung dan memanggil balik ke kode host melalui fungsi MeasureText yang Anda suplai, dan ia bisa melempar EXFAWidgetRuntimeError saat batas widget tercapai. Jika runtime memutasi daftar hidupnya di tempat, salah satu jalur itu akan menyisakan host memegang daftar yang sebagian tata letak lama dan sebagian baru, dengan pointer DataNode menuju dokumen yang akan di-rollback. Konvergensi reflow kemudian diputuskan oleh LayoutSignature, sebuah string yang dibangun dari jumlah widget ditambah setiap ID, indeks halaman, dan bounding box yang dibulatkan ke empat desimal: CommitEdit membangun ulang, membandingkan signature, dan mengulang sampai dua signature berturut-turut cocok atau anggaran pass habis. Saat signature tidak pernah berubah sama sekali, LastReflowPasses tetap 0, dan itulah cara Anda membedakan edit hanya-nilai dari edit yang benar-benar menumbuhkan form, sementara state interaksi terbawa melintasi setiap rebuild lewat ID widget, sehingga fokus dan edit yang sedang berlangsung selamat dari penyisipan baris
Mengapa field terbinding membaca record yang salah?
Karena skripnya berjalan tanpa konteks data. Field yang membawa <bind match="dataRef" ref="$record.actual"/> eksplisit dan field yang dinamai sama dengan node data itu adalah dua widget berbeda yang menunjuk satu nilai, dan subform berulang dengan <occur max="2"/> menghasilkan beberapa widget yang berbagi nama dan hanya berbeda pada baris data tempat mereka bernaung; evaluasi validation dan calculation terhadap akar dokumen membuat semuanya me-resolve this ke node pertama yang cocok di seluruh paket datasets, sehingga baris dua diam-diam memvalidasi baris satu. HotPDF menghindarinya dengan menyimpan DataNode yang sudah di-resolve pada setiap entri widget saat layout menghasilkannya, lalu menyisipkan node itu ke kedua panggilan HPDFXFAEvaluateFieldScript, baik untuk xfskValidate maupun xfskCalculate. Konteks yang sama menentukan node mana yang dibuat EnsureValueNode saat sebuah calculation menyasar binding yang belum ada, dan saat tidak ada binding yang bisa di-resolve, komit gagal bersih dengan XFA calculation target is not bound alih-alih menulis ke baris yang salah. Semantik FormCalc di balik skrip-skrip itu menggemakan apa yang dokumen AcroForm dapatkan dari actions yang dibahas di skrip format dan calculate AcroForm, tetapi aturan resolusi di sini berskop XFA, bukan berskop nama field
Anggaran diperiksa sebelum efek samping, bukan sesudahnya
Setiap batas di runtime adalah prekondisi, karena anggaran yang ditegakkan setelah alokasi terjadi bukanlah anggaran. TXFAWidgetRuntimeOptions.Default mengirim MaxWidgets sebesar 10000, MaxValueChars sebesar 1048576, MaxCalculationPasses sebesar 16 dan MaxReflowPasses sebesar 4, sedangkan TXFAFormScriptOptions bawaan membawa MaxOperations sebesar 100000 dengan MaxElapsedMilliseconds sebesar 500. Di bawahnya, XFA DOM menerapkan TXFADOMLimits miliknya sendiri: batas 128 MB untuk input dan output terdekompresi, paling banyak 1024 paket yang disambung, 1000000 node, dan kedalaman nesting 256. Dua detail lebih penting daripada angka-angkanya. Pertama, anggaran skrip bersifat se-transaksi, bukan per skrip: CommitEdit menyiapkan satu counter sisa-operasi dan satu deadline monotonik, dan setiap invokasi validate serta calculate mengurangi counter yang sama dan hanya menerima milidetik yang masih tersisa, sehingga form dengan dua ratus field calculating tidak bisa memakai 500 ms penuh dua ratus kali. Kedua, deadline berasal dari fungsi MonotonicMilliseconds yang bisa disuntikkan, dan itulah yang membuat perilaku waktu-elapsed dapat direproduksi di test suite alih-alih jadi peruntukan di build agent yang sibuk
var
Options: TXFAWidgetRuntimeOptions;
Runtime: TXFAWidgetRuntime;
begin
Options := TXFAWidgetRuntimeOptions.Default;
Options.MaxWidgets := 2000; // default 10000
Options.MaxCalculationPasses := 8; // default 16
Options.MaxReflowPasses := 2; // default 4
Options.ScriptOptions.Limits.MaxOperations := 20000; // whole transaction
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; // fired only when reflow actually moved widgets
end;
// ... drive the form ...
finally
Runtime.Free;
end;
end;
Di mana runtime berhenti, dan mengapa ia mengatakannya dengan jelas
Runtime ini sengaja bukan mesin skrip XFA serbaguna. DispatchEvent menangani aktivitas enter dan exit secara native dengan memindahkan fokus, dan untuk setiap aktivitas lain yang membawa skrip ia menolak dengan diagnostik spesifik dan stabil alih-alih berpura-pura: skrip yang menyebut addInstance, removeInstance atau instanceManager mengembalikan XFA runtime does not support event-driven instance mutation, skrip yang menyentuh .presence mengembalikan padanan presence-nya, dan sisanya mengembalikan XFA runtime does not support this event script. Penolakan yang bisa diprediksi dan bisa Anda cabangkan lebih baik daripada emulasi parsial yang bekerja pada file contoh Anda dan menyimpang di file milik pelanggan
Model threading-nya sama blak-blakannya: satu instance runtime milik satu thread, tanpa penguncian internal, karena mesin tata letak memanggil balik ke callback pengukuran host dan kunci di sekitar itu adalah deadlock yang menunggu repaint. Konten kaya di dalam field mengikuti garis konservatif yang sama seperti bagian lain pustaka ini, tempat payload exData ditangani seperti dibahas di teks kaya dan hyperlink XFA exData, sementara widget signature dan button kembali sebagai ReadOnly dan jenis UI yang tak didukung muncul sebagai xwkUnsupported alih-alih kotak teks yang bisa diedit namun diam-diam kehilangan data
Dijumlahkan, itu adalah jawaban yang bisa dipakai untuk XFA dinamis di Delphi: jaga DOM tetap hidup, jadikan setiap edit satu transaksi yang mendarat penuh atau tidak menyisakan apa pun, batasi setiap pass dengan anggaran, dan bersikap eksplisit tentang apa yang berada di luar lingkup. Jika Anda mengevaluasinya untuk alur kerja klaim, pajak, atau tunjangan, runtime XFA dikirim sebagai bagian dari komponen PDF Delphi HotPDF, bersama jalur AcroForm, flattening, dan rendering yang biasanya proyek-proyek itu perlukan sekaligus