PDFium Component menyimpan nilai form XFA hasil edit secara persis, melewati save dan buka ulang, ketika ia menjalankan runtime Windows V8 pdfium.v8.dll yang dikirim sejak v3.125.2 atau lebih baru. Runtime lebih lama menambahkan line feed ke nilai field, memotong emoji menjadi karakter BMP yang tak berhubungan, diam-diam melewatkan save XFA single-stream, dan bisa menelan tulis akhir yang gagal. Satu gejala buka ulang sama sekali bukan cacat library: form dynamic yang subform root-nya tak punya restoreState="auto" membangun ulang layout-nya dari template
Laporan bug untuk ini semuanya tampak serupa. Customer mengisi form klaim XFA di viewer Delphi, menyimpan, membuka ulang, dan ada yang sedikit meleset. Kotak komentar yang kosong kini memuat satu baris kosong, dan setelah save kedua memuat dua. Nama yang diketik dengan emoji kembali dengan glyph private-use. Tak seorang pun menerima error, dan itulah yang membuat bug-bug ini mahal: gesernya muncul berminggu-minggu kemudian di ekspor milik orang lain
Apa yang salah ketika form XFA disimpan dan dibuka ulang?
Empat cacat terpisah di jalur save XFA native menyebabkan pergeseran nilai, dan masing-masing bersembunyi di balik save yang tampak sukses. Dua datang dari serialisasi, satu dari layout penyimpanan single-stream, dan satu dari writer PDF itu sendiri. Tabel berikut memetakan tiap gejala ke penyebabnya dan ke rilis tempat PDFium Component memperbaikinya
| Gejala setelah buka ulang | Penyebab | Diperbaiki di |
|---|---|---|
| Field kosong memuat line feed; nilai tumbuh satu newline per save | Kedua writer XFA menyisipkan newline layout setelah start tag | v3.125.2, pdfium.v8.dll |
| U+1F642 kembali sebagai U+F642, atau emoji lenyap dari form packet | Truncasi wchar_t 16-bit saat dekode; penyaringan surrogate di form serializer | v3.125.2, pdfium.v8.dll |
| Edit di dokumen XFA single-stream sekadar lenyap | Save native menolak layout stream, tapi nilai kembalian diabaikan | v3.125.2; comment dan processing instruction dipertahankan sejak v3.126.0 |
| File terpotong padahal save melaporkan sukses | Tulis buffered terakhir gagal setelah writer sudah mengembalikan sukses | Runtime V8 v3.125.2; pdfium.dll biasa v3.125.3 |
| Form dynamic tiga halaman terbuka ulang sebagai dua halaman | Subform root tak meminta restoreState="auto" | Penulisan form, bukan cacat library |
Tulisan-tulisan sebelumnya menyimpulkan bahwa edit field XFA tak bisa dipertahankan dengan PDFium sama sekali, yang akurat untuk runtime semasa itu. Runtime V8 yang lebih baru menyimpan nilai XFA secara native, sehingga edit yang dibuat di form live sampai ke datasets packet tersimpan tanpa operasi packet di sisi Anda
Runtime PDFium mana yang menyimpan nilai XFA?
Fidelitas save XFA bergantung pada DLL native, bukan pada wrapper Delphi, sehingga pemeriksaan pertama adalah runtime mana yang benar-benar termuat di proses Anda. PDFium Component mengirim dua build Windows per arsitektur: pdfium.dll biasa yang dibangun tanpa V8 dan XFA, serta pdfium.v8.dll yang membawa JavaScript engine dan runtime form XFA. Hanya pdfium.v8.dll yang bisa menjalankan form XFA, jadi setiap perbaikan XFA yang dibahas di sini berada di sana, dimulai dari library V8 Win32 dan Win64 yang dibangun ulang di v3.125.2
Perbaikan tulis-akhirnya adalah kode writer PDF generik, jadi ia juga penting untuk dokumen biasa. v3.125.3 membangun ulang library pdfium.dll biasa untuk membawa perbaikan yang sama. Source bersama bukan bukti perilaku bersama: sampai binary dibangun ulang, DLL lama tetap membawa bug lama
Jebakan kedua duduk di loader. Sebelum v3.125.2, menyetel EnableV8Engine ke True membuat binding memilih nama default pdfium.v8.dll dan mengabaikan path lengkap di LibraryName. Aplikasi yang menunjuk runtime yang baru di-deploy bisa terus memuat salinan lama dari folder lain. Sejak v3.125.2, LibraryName yang memuat direktori memilih tepat file itu di mode engine mana pun, dan path yang hilang gagal alih-alih fallback ke library bawaan lain
uses
System.SysUtils, PDFium;
procedure SelectXfaRuntime;
begin
// Direktori di LibraryName meng-pin tepat file ini (v3.125.2 dan seterusnya);
// jika file hilang, pemuatan melempar alih-alih fallback
{$IFDEF WIN64}
PDFium.LibraryName := ExtractFilePath(ParamStr(0)) + 'DLLs\Win64\pdfium.v8.dll';
{$ELSE}
PDFium.LibraryName := ExtractFilePath(ParamStr(0)) + 'DLLs\Win32\pdfium.v8.dll';
{$ENDIF}
PDFium.EnableV8Engine := True;
PDFium.LoadLibrary; // gagal saat startup, bukan saat save pertama
end;
Setelah membuka dokumen, TPdf.XFA memberi tahu Anda file memuat XFA dan TPdf.XfaRuntimeAvailable memberi tahu DLL termuat benar-benar bisa mengeksekusinya. Kalau Anda juga perlu membedakan form statis dan dynamic, TPdf.FormType mengembalikan ftXfaFull atau ftXfaForeground; artikel tentang deteksi form XFA dan ekstraksi XFA packet di Delphi membahas probing itu secara rinci
Mengapa field XFA tersimpan mendapat line feed ekstra?
Field XFA tersimpan mendapat line feed karena kedua writer XFA native, writer elemen XML generik dan form packet serializer, melakukan pretty-print pada keluarannya dengan newline setelah start tag. Di kebanyakan XML, whitespace itu kosmetik. Di data XFA bukan: ketika datasets packet di-parse lagi, teks di antara <Comments> dan </Comments> adalah nilai field, newline termasuk. Field kosong karenanya terbuka ulang memuat satu LF, dan setiap siklus save-dan-buka-ulang berikutnya bisa menambah satu lagi
Perbaikan yang kelihatan, memotong nilai saat memuat, justru salah. User mengetik spasi di awal, spasi di akhir, dan teks multi-baris yang disengaja ke field XFA, dan blok alamat atau kode lebar-tetap harus selamat byte demi byte. Perbaikan v3.125.2 karena itu hanya menghapus whitespace yang disintesis serializer itu sendiri di sekitar tag. Nilai user, text node yang ada, dan section CDATA lewat tanpa disentuh, sehingga " indented" tetap ter-indentasi dan field yang sengaja kosong tetap kosong
Mengapa emoji kembali sebagai karakter yang berbeda?
Emoji kembali salah karena wchar_t Windows lebarnya 16 bit, dan dua jalur dekode menyimpan nilai scalar Unicode penuh dalam satu wchar_t. Decoder stream UTF-8 dan parser numeric character reference seperti 🙂 sama-sama melakukannya. U+1F642, wajah tersenyum tipis, tak muat dalam 16 bit, sehingga bit tingginya terlepas dan U+F642 muncul sebagai gantinya: code point di Private Use Area yang kebanyakan font render sebagai kotak atau tak sama sekali
Form serializer punya masalah yang berlawanan. Ia menyaring karakter satu wchar_t sekaligus, melihat dua code unit surrogate yang tak valid bila berdiri sendiri, dan membuang keduanya, sehingga emoji lenyap total dari form packet. Di v3.125.2, decoder mengonsumsi setiap nilai scalar secara lengkap dan memancarkan surrogate pair yang benar. Ketika tersisa hanya satu slot keluaran, ia menahan low surrogate dan tak melaporkan end-of-stream selama unit itu masih dibuffer. Urutan UTF-8 yang terpotong melintasi blok baca dibawa ke pembacaan berikutnya alih-alih dibuang. Form exporter kini menjaga surrogate pair yang valid tetap bersama, dan numeric character reference juga menghasilkan pasangan yang benar
Data uji Latin-1 tak pernah memperlihatkan salah satu dari ini, sehingga setiap test round-trip XFA butuh minimal satu karakter supplementary-plane
XFA single-stream dan kegagalan save yang tak dilihat siapa pun
Dokumen XFA single-stream kehilangan editnya karena helper save native menolak layout penyimpanan itu dan callernya mengabaikan kegagalannya. ISO 32000-1 §12.7.8 mengizinkan entri /XFA dari dictionary form interaktif berupa array nama packet dan stream atau satu stream tunggal yang memuat seluruh dokumen XDP. Array packet adalah kasus umum, tapi stream tunggal sepenuhnya legal, dan save PDF selesai seolah tak terjadi apa-apa sementara data form tetap pada nilai lamanya
Sejak v3.125.2, runtime V8 menangani subset single-stream yang didukung. Ia lebih dulu mengekspor kedua packet live, datasets dan form, ke staging area dan memvalidasinya, dan baru kemudian mengganti packet yang cocok di XDP asli. Packet lain dan deklarasi namespace root dipertahankan. Kalau staging gagal, stream XFA persisten tak pernah tersentuh dan dokumen mempertahankan tanda modifikasinya
Comment XML dan processing instruction butuh perhatian ekstra karena XML DOM internal membuangnya. Di v3.125.2, kehadiran mereka membuat save gagal total alih-alih kehilangan konten diam-diam. v3.126.0 mempertahankannya: sebelum parse, tiap comment atau processing instruction ditukar dengan marker yang dibangun dari prefix yang tak muncul di mana pun dalam teks asli. Setelah packet live diganti, tiap marker harus muncul tepat sekali sebelum token asli dipulihkan dan stream ditulis. Token di luar packet yang diganti karena itu mempertahankan teks dan urutannya, termasuk token di prolog, template, dan packet lain
Beberapa input tetap ditolak dengan sengaja, dan tiap penolakan adalah kegagalan save yang eksplisit:
- Comment atau processing instruction di dalam packet live
datasetsatauform, karena posisi aslinya tak bisa dipetakan ke konten hasil ekspor baru - Deklarasi DTD dan signature XMLDSig, karena menulis ulang XDP tak bisa menjaga signature XML tetap valid
- Encoding UTF-8 atau UTF-16 tak valid, tag tak lengkap, character reference tak valid, entity tak dikenal, dan processing instruction malformed, yang ditolak alih-alih diam-diam diperbaiki
Keluaran single-stream adalah UTF-8 dan mempertahankan model konten XML, bukan layout byte asli maupun deklarasi encoding
Cacat terakhir duduk di bawah XFA. Writer file native mem-buffer keluaran dalam blok 32 KB dan meng-flush blok parsial terakhir hanya di destruktor, setelah writer dokumen sudah melaporkan sukses. Disk penuh atau error I/O pada blok terakhir itu tak terlihat oleh caller. Sejak v3.125.2 di runtime V8 dan v3.125.3 di runtime biasa, flush terakhir itu bagian dari hasil save, dan tanda modifikasi XFA dibersihkan hanya setelah sukses sungguhan. Di sisi Delphi, TPdf.SaveAs(const FileName: string; Option: TSaveOption = saNone; PdfVersion: TPdfVersion = pvUnknown): Boolean menulis ke file sementara di samping target dan memindahkannya ke tempatnya hanya ketika save mengembalikan True, sehingga save yang gagal membiarkan file sebelumnya utuh
Mengapa form XFA dynamic terbuka ulang dengan halaman lebih sedikit?
Form XFA dynamic terbuka ulang dengan halaman lebih sedikit ketika subform root-nya tak mendeklarasikan restoreState="auto", dan itu keputusan penulisan form alih-alih cacat PDFium Component. Di XFA 3.3, restoreState pada subform root default-nya manual. Di bawah manual, XFA processor hanya memulihkan state terbatas dari form packet tersimpan dan menyerahkan sisanya ke script penulisnya. Nilai field tersimpan dan instance count subform berulang tetap kembali, tapi properti geometri yang diset saat run time tidak
Kasus yang membongkar ini adalah form tiga halaman yang script-nya menggendong sebuah subform menjadi h="450pt". Form packet tersimpan memuat tinggi baru, nilai-nilai, dan instance count-nya. Tapi saat buka ulang, layout dibangun ulang dari tinggi template dan form mengalir kembali menjadi dua halaman. Runtime-nya benar: template tak pernah meminta pemulihan otomatis. Mendeklarasikannya pada subform root memperbaiki buka ulangnya:
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb" restoreState="auto">
<pageSet>
<pageArea name="Page1">
<contentArea x="0.25in" y="0.25in" w="8in" h="10.5in"/>
<medium stock="letter"/>
</pageArea>
</pageSet>
<subform name="Details" layout="tb" w="7.5in">
<!-- field; script boleh mengubah h atau menambah instance saat run time -->
</subform>
</subform>
</template>
Kalau template itu bukan milik Anda, jangan menambalnya di viewer: form yang mengandalkan mode manual mengharapkan script-nya sendiri membangun ulang state. Repaginasi live saat user mengetik adalah topik terpisah, dibahas di cara PDFium Component melacak page count dynamic XFA dan field yang berpindah
Bagaimana Anda memverifikasi save XFA di Delphi?
Satu-satunya pemeriksaan save XFA yang andal adalah membuka ulang file tersimpan di instance TPdf baru dan membaca data tersimpannya kembali. TPdf.GetXfaDatasets mengembalikan datasets packet apa adanya saat tersimpan di dokumen, bukan model data XFA live, sehingga memanggilnya sebelum save menampilkan nilai lama. Setelah buka ulang, ia menampilkan persis apa yang ditulis. Dokumen single-stream tak punya packet bernama terpisah: PDFium melaporkan seluruh XDP sebagai satu packet dengan nama kosong, sehingga GetXfaPacketByName('datasets') dan GetXfaDatasets mengembalikan tak ada apa pun, dan fallback membaca stream lengkap lewat GetXfaFormPackets
uses
System.SysUtils, PDFium, FPdfXfa;
function ReadSavedXfaData(const FileName: string): string;
var
Pdf: TPdf;
Packets: TXfaPacketList;
Bytes: TBytes;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True;
Bytes := Pdf.GetXfaDatasets; // layout packet-array
if Length(Bytes) = 0 then
begin
Packets := Pdf.GetXfaFormPackets; // single stream: satu packet tanpa nama
if Length(Packets) = 1 then
begin
SetLength(Bytes, Length(Packets[0].Content));
if Length(Bytes) > 0 then
Move(Packets[0].Content[0], Bytes[0], Length(Bytes));
end;
end;
Result := TEncoding.UTF8.GetString(Bytes); // keluaran XDP tersimpan adalah UTF-8
finally
Pdf.Free;
end;
end;
Routine save lalu meng-commit edit yang tertunda, mengecek hasil SaveAs, dan membandingkan nilai hasil buka ulang. TPdf.ClearFormFieldFocus mematikan focus form, momen ketika PDFium meng-commit edit buffer field yang difokuskan. TPdf.SetFocusedFormFieldText(const Value: WString): Boolean mengisi field terfokus secara programatik, tapi ia bergantung pada focus yang dilacak wrapper lewat FocusFormField, yang menelusuri widget annotation. Halaman XFA dynamic biasanya tak punya itu, sehingga di sana teks biasanya datang lewat input keyboard di TPdfView, dan fungsi mengembalikan False ketika tak ada field terlacak yang terfokus
function XmlText(const S: string): string;
begin
Result := StringReplace(S, '&', '&', [rfReplaceAll]);
Result := StringReplace(Result, '<', '<', [rfReplaceAll]);
end;
procedure SaveXfaAndVerify(Pdf: TPdf; const FileName, FieldTag,
Expected: string);
var
Saved: string;
begin
// Isi ter-skrip opsional; False berarti tak ada field terlacak yang terfokus
if (Pdf.FocusedFormFieldIndex >= 0) and
not Pdf.SetFocusedFormFieldText(Expected) then
raise EPdfError.Create('Could not write the focused field');
Pdf.ClearFormFieldFocus; // commit edit buffer
if not Pdf.SaveAs(FileName) then // termasuk flush terakhir (v3.125.2+)
raise EPdfError.CreateFmt('Saving %s failed', [FileName]);
Saved := ReadSavedXfaData(FileName);
if Pos('<' + FieldTag + '>' + XmlText(Expected) + '</' + FieldTag + '>',
Saved) = 0 then
raise EPdfError.CreateFmt('%s did not survive the round trip', [FieldTag]);
end;
Perlakukan tes substring sebagai smoke test. Elemen kosong bisa terserialisasi sebagai <Tag/>, atribut bisa muncul pada elemen data, dan escaping di luar & dan < adalah pilihan serializer. Untuk pemeriksaan production, muat XML hasil buka ulang dengan XML parser sungguhan dan bandingkan text node elemen data yang terikat. Jalankan pemeriksaan itu dua kali berturut-turut juga, karena cacat newline hanya memperlihatkan bentuk penuhnya pada generasi kedua
Referensi cepat: checklist fidelitas save XFA
- Deploy
pdfium.v8.dlldari v3.125.2 atau lebih baru untuk form XFA, dan v3.125.3 atau lebih baru untukpdfium.dllbiasa, agar perbaikan tulis-akhir ada di keduanya - Arahkan
LibraryNameke path lengkap dan setelEnableV8Engineke True; path yang hilang gagal alih-alih memuat salinan lain - Konfirmasi
TPdf.XFAdanTPdf.XfaRuntimeAvailablesetelah membuka dokumen - Panggil
ClearFormFieldFocussebelumSaveAsagar field terfokus ter-commit - Jangan pernah mengabaikan hasil Boolean
SaveAs; hasil False membiarkan file sebelumnya di tempatnya - Verifikasi dengan membuka ulang di
TPdfbaru dan membacaGetXfaDatasets, fallback keGetXfaFormPacketsuntuk XFA single-stream - Tes dengan nilai kosong, spasi di awal, teks multi-baris,
&, dan satu karakter supplementary-plane, lintas dua generasi save - Harapkan kegagalan save eksplisit untuk DTD, XMLDSig, dan comment di dalam packet live milik XFA single-stream
- Kalau form dynamic kehilangan geometri run-time saat buka ulang, periksa subform root untuk
restoreState="auto"sebelum mencurigai library
Untuk struktur callback yang diharapkan runtime XFA dari aplikasi host, lihat FPDF_FORMFILLINFO versi 2 dan ABI XFA di Delphi. Runtime V8, wrapper Delphi dan C++Builder, dan kontrol viewer semuanya bagian dari PDFium Component for Delphi and C++Builder, yang menyertakan kedua runtime Windows untuk Win32 dan Win64