Artikel Teknis

Save XFA PDFium Component: Newline, Emoji, dan restoreState

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 ulangPenyebabDiperbaiki di
Field kosong memuat line feed; nilai tumbuh satu newline per saveKedua writer XFA menyisipkan newline layout setelah start tagv3.125.2, pdfium.v8.dll
U+1F642 kembali sebagai U+F642, atau emoji lenyap dari form packetTruncasi wchar_t 16-bit saat dekode; penyaringan surrogate di form serializerv3.125.2, pdfium.v8.dll
Edit di dokumen XFA single-stream sekadar lenyapSave native menolak layout stream, tapi nilai kembalian diabaikanv3.125.2; comment dan processing instruction dipertahankan sejak v3.126.0
File terpotong padahal save melaporkan suksesTulis buffered terakhir gagal setelah writer sudah mengembalikan suksesRuntime V8 v3.125.2; pdfium.dll biasa v3.125.3
Form dynamic tiga halaman terbuka ulang sebagai dua halamanSubform 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

Diagram siklus save XFA PDFium Component ketika writer menambahkan newline setelah start tag, parser yang membuka ulang membaca LF di antara tag Comments sebagai nilai field, dan tiap save berikutnya menambahkan line feed lagi sampai v3.125.2 menghapus hanya whitespace hasil sintesis serializer
Satu siklus save-buka-ulang menanam line feed pertama dan setiap ronde berikutnya menambah satu lagi, itulah kenapa gesernya memperlihatkan bentuk penuhnya baru pada generasi kedua

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 &#x1F642; 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

Diagram penanganan surrogate PDFium Component ketika U+1F642 tiba sebagai pasangan UTF-16 D83D DE42 dan dua jalur cacat merusaknya: decoder wchar_t 16-bit memotong scalar menjadi U+F642 di private use area, sementara form serializer menyaring surrogate tunggal dan membuang emoji sepenuhnya
wchar_t Windows lebarnya 16 bit, sehingga scalar yang butuh surrogate pair kehilangan setengah tingginya atau lenyap dari packet sampai kedua jalur belajar menjaga pasangan tetap bersama

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 datasets atau form, 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
Pipeline save XFA single-stream PDFium Component ketika packet live datasets dan form diekspor ke staging, divalidasi, lalu diganti di dalam XDP asli dengan comment dipertahankan lewat marker, sementara kegagalan staging dan input seperti DTD atau XMLDSig menolak save secara eksplisit
Ekspor bertahap divalidasi sebelum apa pun diganti, sehingga save yang gagal membiarkan stream XFA persisten tak tersentuh dan dokumen mempertahankan tanda modifikasinya

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, '&', '&amp;', [rfReplaceAll]);
  Result := StringReplace(Result, '<', '&lt;', [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.dll dari v3.125.2 atau lebih baru untuk form XFA, dan v3.125.3 atau lebih baru untuk pdfium.dll biasa, agar perbaikan tulis-akhir ada di keduanya
  • Arahkan LibraryName ke path lengkap dan setel EnableV8Engine ke True; path yang hilang gagal alih-alih memuat salinan lain
  • Konfirmasi TPdf.XFA dan TPdf.XfaRuntimeAvailable setelah membuka dokumen
  • Panggil ClearFormFieldFocus sebelum SaveAs agar field terfokus ter-commit
  • Jangan pernah mengabaikan hasil Boolean SaveAs; hasil False membiarkan file sebelumnya di tempatnya
  • Verifikasi dengan membuka ulang di TPdf baru dan membaca GetXfaDatasets, fallback ke GetXfaFormPackets untuk 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