PDFium Component kini menyetel FPDF_FORMFILLINFO.version ke 2 untuk setiap form-fill environment yang ia inisialisasi, karena versi yang diterima build PDFium native adalah properti build itu, bukan properti dokumen yang dibuka. pdfium.v8.dll yang ber-XFA menolak versi 1 mentah-mentah, sehingga PDF AcroForm biasa yang dibuka lewatnya dulu gagal di FPDFDOC_InitFormFillEnvironment padahal tidak ada XFA di mana pun. Perbaikan di v3.116.0 kecil, tapi kesalahan di baliknya bersifat umum dan layak disebut namanya: field versi protokol mendeskripsikan tata letak memori yang diharapkan pihak lain, dan ia tidak boleh diturunkan dari apakah Anda kebetulan membutuhkan fitur yang dibawa tata letak itu
Kenapa FPDFDOC_InitFormFillEnvironment gagal pada PDF biasa dengan pdfium.v8.dll?
Environment-nya gagal karena build PDFium ber-XFA memvalidasi field version-nya sebelum melakukan apa pun, dan logika wrapper yang lama memberinya nilai 1 setiap kali dokumen saat itu bukan form XFA. Gejala di host Delphi adalah EPdfError yang dimunculkan dari TPdf.InitializeFormFill dengan pesan Cannot initialize form fill environment, dilempar saat membuka invoice atau formulir pajak biasa yang isinya tidak lain hanya field teks AcroForm. File yang sama terbuka lancar dengan pdfium.dll biasa. DLL yang sama juga membuka dokumen XFA sungguhan dengan lancar. Hanya kombinasi build V8 dengan dokumen non-XFA yang rusak, dan itulah persis kombinasi yang dialami host setelah ia menyalakan EnableV8Engine demi JavaScript AcroForm, atau setelah pemilihan otomatis di LoadDocument sudah mengikat prosesnya ke pdfium.v8.dll untuk file XFA sebelumnya. Ikatan itu berlaku se-proses: EnableV8Engine dibaca sebelum LoadLibrary pertama, dan begitu build XFA-nya dimuat, setiap PDF biasa setelahnya melewati penyiapan environment yang sama terhadap binary yang sama. Host-nya tidak berbuat salah; wrapper-nya yang mengajukan pertanyaan yang keliru saat mengisi record-nya. Kalau Anda masih menentukan binary mana yang akan dirilis, catatan kami tentang deploying DLL PDFium dan mendiagnosis kegagalan load membahas pemilihan plain versus V8, dan artikel ini mengasumsikan build V8-nya sudah berada di dalam prosesnya
Apa yang sebenarnya dijanjikan field version di FPDF_FORMFILLINFO?
FPDF_FORMFILLINFO.version memberi tahu PDFium field mana dari record itu yang boleh ia baca, dan header publik fpdf_formfill.h mengaitkan nilai yang bisa diterima dengan bagaimana library-nya dikompilasi, bukan dengan dokumennya. Secara garis besar kontraknya punya tiga bagian. Versi 1 mencakup callback stabil dari FFI_Invalidate sampai FFI_DoGoToAction plus pointer m_pJsPlatform. Build tanpa modul XFA menerima 1 maupun 2, dan dengan 2 ia juga akan memanggil callback eksperimental tambahannya. Build dengan modul XFA mewajibkan 2, titik, dan header-nya mengulang syarat itu dua kali seolah ia menduga orang akan melewatkannya. Tidak ada satu pun bagian kontrak itu yang menyebut dokumennya. Versinya adalah pernyataan tentang record yang Anda alokasikan: dengan 2 Anda menjanjikan bahwa memori setelah m_pJsPlatform itu ada dan berisi function pointer yang valid atau NULL
Wilayah versi 2 itulah tempat semua mesin XFA berada. Ia dimulai dengan xfa_disabled, sebuah FPDF_BOOL yang oleh header dijelaskan sebagai diabaikan di bawah versi 2 dan bermakna hanya ketika modul XFA-nya dikompilasi masuk, lalu berlanjut dengan tujuh belas function pointer, FFI_DisplayCaret sampai FFI_DoURIActionWithKeyboardModifier. Masing-masing didokumentasikan sebagai wajib untuk XFA dan selebihnya disetel NULL. Frasa itulah kunci seluruh perbaikannya. NULL bukan state galat untuk slot-slot itu; ia adalah state yang didokumentasikan untuk host yang tidak menjalankan XFA. Record yang sudah dibersihkan dengan FillChar lalu ditandai versi 2 memenuhi kontraknya pada build non-XFA sama baiknya dengan record versi 1, dan ia satu-satunya record yang diterima build XFA
Pemilihan lama mengikat ABI-nya ke dokumen
Cacatnya adalah satu conditional tunggal yang tampak masuk akal kalau dilihat sendiri. TPdf.InitializeFormFill menghitung flag RuntimeReady dari tiga fakta: dokumennya melaporkan tipe form XFA lewat TPdf.XFA, helper string XFA-nya teresolusi lewat XfaFeaturesAvailable, dan ekspor V8-nya teresolusi lewat V8FeaturesAvailable. Sebelum v3.116.0 flag yang sama juga yang memilih versinya
// v3.115.0 dan sebelumnya: versi ABI-nya mengikuti dokumennya
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
if RuntimeReady then
FFormFillInfo.Info.version := 2
else
FFormFillInfo.Info.version := 1;
// ... dan cabang runtime-hilang memakunya sekali lagi
else if XFA then
begin
FFormFillInfo.Info.version := 1;
FFormFillInfo.Info.xfa_disabled := 1;
if Assigned(FOnXfaRuntimeMissing) then
FOnXfaRuntimeMissing(Self);
end;
Bacalah dengan header-nya di tangan dan kegagalannya jadi jelas. RuntimeReady bernilai false untuk setiap dokumen AcroForm biasa, jadi setiap dokumen biasa mengumumkan versi 1. Di pdfium.dll itu tidak masalah. Di pdfium.v8.dll, yang merupakan build ber-XFA, PDFium memeriksa field-nya, mendapatinya di bawah 2 yang diwajibkan, lalu mengembalikan FPDF_FORMHANDLE null, yang oleh CheckPdf diubah menjadi exception di atas. Maksud kode lamanya defensif: pertahankan versi 1 agar build XFA tidak pernah membaca slot versi 2 yang belum diassign. Ia membela diri dari masalah yang sudah dikesampingkan header-nya dan justru menciptakan masalah yang diperingatkan header itu secara eksplisit. Kode yang dibetulkan memutuskan versinya sekali, di muka, dari apa record itu secara fisik
procedure TPdf.InitializeFormFill;
var
RuntimeReady: Boolean;
begin
FXfaRuntimeUsable := False;
FXfaPageCountOverride := -1; // sentinel: pakai static page tree
if not FormFill then
Exit;
FillChar(FFormFillInfo, SizeOf(FFormFillInfo), 0);
FFormFillInfo.Pdf := Self;
// Record versi 2 yang lengkap dialokasikan dan dibersihkan di atas. PDFium
// menerima versi 2 tanpa XFA dan mewajibkannya di setiap build ber-XFA,
// termasuk ketika dokumen ini tidak memuat form XFA.
FFormFillInfo.Info.version := 2;
FFormFillInfo.Info.xfa_disabled := 1;
// RuntimeReady membatasi callback XFA dan xfa_disabled, tidak pernah versinya.
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
...
Di mana RuntimeReady tetap dibutuhkan: callback dan xfa_disabled
RuntimeReady tetap menjalankan tugasnya sebagai gate perilaku XFA; ia cuma tidak lagi menyentuh tata letak record-nya. Callback versi 1, FFI_Invalidate, FFI_SetTimer, FFI_GetPage, FFI_DoURIAction, FFI_DoGoToAction, dan sisa blok itu, dipasang tanpa syarat karena AcroForm maupun XFA sama-sama bergantung padanya. Tujuh belas pointer versi 2-nya diassign hanya di dalam cabang RuntimeReady, bersama xfa_disabled := 0. Ketika dokumennya XFA tapi runtime-nya tidak ada, record-nya tetap di versi 2 dengan xfa_disabled bernilai 1 dan slot versi 2-nya dibiarkan NULL, dan wrapper-nya memunculkan OnXfaRuntimeMissing agar host-nya bisa menyarankan restart dengan pdfium.v8.dll. Setelah environment-nya ada, FPDF_LoadXFA dipanggil hanya ketika RuntimeReady bernilai true, dan hanya return true yang menyetel FXfaRuntimeUsable, yang dilaporkan TPdf.XfaRuntimeAvailable
if RuntimeReady then
begin
FFormFillInfo.Info.xfa_disabled := 0; // 0 = XFA aktif
FFormFillInfo.Info.FFI_DisplayCaret := FormFillDisplayCaret;
FFormFillInfo.Info.FFI_GetCurrentPageIndex := FormFillGetCurrentPageIndex;
FFormFillInfo.Info.FFI_SetCurrentPage := FormFillSetCurrentPage;
FFormFillInfo.Info.FFI_GotoURL := FormFillGotoURL;
FFormFillInfo.Info.FFI_GetPageViewRect := FormFillGetPageViewRect;
FFormFillInfo.Info.FFI_PageEvent := FormFillPageEvent;
FFormFillInfo.Info.FFI_PopupMenu := FormFillPopupMenu;
FFormFillInfo.Info.FFI_OpenFile := FormFillOpenFile;
FFormFillInfo.Info.FFI_EmailTo := FormFillEmailTo;
// ... FFI_UploadTo sampai FFI_DoURIActionWithKeyboardModifier
end
else if XFA then
begin
// Runtime tidak tersedia: pertahankan versi 2, biarkan XFA nonaktif, beri tahu host.
if Assigned(FOnXfaRuntimeMissing) then
FOnXfaRuntimeMissing(Self);
end;
FFormHandle := FPDFDOC_InitFormFillEnvironment(FDocument, FFormFillInfo.Info);
CheckPdf(FFormHandle <> nil, 'Cannot initialize form fill environment');
if RuntimeReady then
FXfaRuntimeUsable := FPDF_LoadXFA(FDocument) <> 0;
Dua detail di blok itu mudah keliru ketika Anda menulis binding sendiri. FXfaPageCountOverride direset ke -1 sebagai sentinel sebelum apa pun terjadi, sehingga PageCount jatuh kembali ke static page tree sampai FFI_PageEvent melaporkan repaginasi; nilai nol di situ akan diam-diam mengklaim dokumen kosong. Dan setiap callback versi 2-nya adalah rutin cdecl statis yang memulihkan TPdf pemiliknya dari record-nya lalu menelan exception Pascal mana pun sebelum kembali ke PDFium, dan itulah disiplin yang dijabarkan catatan kami tentang mengeraskan ABI PDFium di Delphi untuk FFI_OpenFile. Tidak ada bagian dari perubahan versi ini yang melonggarkan kedua aturan itu
Apakah versi 2 aman ketika DLL-nya tidak punya modul XFA?
Ya, dan alasannya ada di record-nya, bukan di janji dari library-nya. Pada build non-XFA, header-nya menyatakan versi 2 juga menyebabkan callback eksperimentalnya dipanggil, jadi pertanyaannya adalah apa yang ditemukan PDFium saat ia melihat. TPdfFormFillInfo adalah packed record yang anggota Info-nya berupa FPDF_FORMFILLINFO lengkap termasuk setiap field versi 2, dan InitializeFormFill membersihkan seluruhnya dengan FillChar sebelum menyentuh satu byte pun. Jadi pada pdfium.dll biasa dengan dokumen biasa, library-nya melihat versi 2, xfa_disabled yang diset, dan NULL di setiap slot eksperimentalnya, yang justru state yang ditetapkan header untuk host yang tidak mengimplementasikan XFA. Tidak ada record terpotong yang harus dibaca lewat batasnya oleh library-nya, karena record-nya memang tidak pernah lebih pendek dari versi 2. Logika lamanya membela diri dari ketidakcocokan tata letak yang sudah dihilangkan oleh deklarasi Pascal-nya sendiri
Batas yang layak dinyatakan dengan jujur adalah batas yang tidak bisa dicakup record-nya. Versi 2 pada dokumen biasa tidak menyalakan JavaScript, scripting XFA, atau event host mana pun di balik callback itu. m_pJsPlatform dipasang hanya ketika V8FeaturesAvailable bernilai true, XFA tetap nonaktif kecuali RuntimeReady bernilai true, dan TPdf.XFA tetap melaporkan tipe form dari FPDF_GetFormType terlepas dari apa yang dinegosiasikan environment-nya. Host yang ingin tahu apakah XFA dinamis benar-benar akan di-render sebaiknya terus membaca XfaRuntimeAvailable setelah Active menjadi true, seperti yang disarankan catatan kami tentang mendeteksi form XFA dan mengekstrak paket XFA, alih-alih menyimpulkan apa pun dari field versinya
procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
// Dipicu dari InitializeFormFill ketika dokumennya XFA tapi pdfium.dll
// yang dimuat tidak bisa menjalankan engine-nya. Environment form-nya tetap
// terbuka, karena versi 2 dikirim bagaimanapun; hanya runtime XFA-nya yang mati.
StatusBar.SimpleText :=
'XFA form detected; restart with pdfium.v8.dll to enable dynamic rendering';
end;
procedure TMainForm.OpenDocument(const FileName: string);
begin
Pdf.Active := False;
Pdf.OnXfaRuntimeMissing := PdfXfaRuntimeMissing;
Pdf.FormFill := True;
Pdf.FileName := FileName;
Pdf.Active := True; // tidak lagi melempar exception pada PDF biasa di bawah pdfium.v8.dll
if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
ShowStaticXfaWarning;
end;
Versi protokol dan ketersediaan fitur adalah dua sumbu berbeda
Aturan umum yang keluar dari perbaikan ini adalah field versi di struktur callback menjawab pertanyaan "seberapa besar record ini dan apa yang boleh Anda baca darinya", sementara deteksi fitur menjawab "slot mana di antaranya yang akan melakukan sesuatu yang berguna". Yang pertama ditetapkan oleh binary native dan oleh deklarasi Pascal yang Anda kompilasi terhadapnya. Yang kedua bervariasi per dokumen, per tabel ekspor DLL, dan per konfigurasi host. Meruntuhkan keduanya menjadi satu boolean itu menggoda karena kasus XFA-nya kebetulan butuh keduanya, tapi begitu ada build yang menegakkan versi minimum, keruntuhan itu patah untuk setiap dokumen yang tidak membutuhkan fiturnya. Form XFA, yang dijelaskan ISO 32000-1 §12.7.8 sebagai payload XML yang hidup berdampingan dengan dictionary AcroForm-nya, adalah fiturnya di sini; tata letak record-nya adalah protokolnya, dan PDFium berhak menuntut tata letaknya sebelum ia melihat filenya sama sekali. Bentuk yang sama muncul di mana pun library C memversikan strukturnya: blok viewer-info, record render-options, tabel callback platform. Pola yang aman adalah yang diikuti InitializeFormFill yang dibetulkan. Deklarasikan tata letak terbaru yang Anda pahami, bersihkan seluruhnya, set versinya agar cocok dengan tata letak itu tanpa syarat, lalu biarkan pemeriksaan kapabilitas yang memutuskan slot mana yang diisi. Kalau header PDFium di masa depan menambahkan versi 3, perubahannya ada di deklarasinya dan di satu assignment itu, bukan di cabang yang bergantung dokumen yang akan salah untuk kombinasi mana pun yang tidak diuji siapa pun
Inisialisasi form-fill yang dibetulkan sudah ada di PDFium Component untuk Delphi, Lazarus, dan C++Builder, dan ia berlaku di Win32 maupun Win64 karena kedua build-nya memakai deklarasi record yang sama. Kalau aplikasi Anda sudah memilih pdfium.v8.dll untuk AcroForm yang digerakkan JavaScript, inilah perubahan yang membuatnya bisa membuka sisa arsip PDF Anda lewat binary yang sama tanpa membuat pengecualian khusus untuk form environment-nya