Artikel Teknis

Dynamic XFA PDFium Component: Page Count Adalah Delta

Ketika form dynamic XFA di viewer Delphi menambah atau menghapus halaman, PDFium Component melaporkan total baru lewat TPdf.PageCount dan TPdf.OnXfaPageCountChanged sejak v3.126.1, karena page event native membawa delta tambah/hapus alih-alih total. Library Windows V8 di v3.126.1 juga memindahkan hit area input mengikuti field yang berpindah, dan v3.126.2 memuat ulang page handle basi setelah layout callback kembali. Laporan bug yang memicu ini adalah form klaim biaya: klik Add Row dua kali, form tumbuh menjadi dua halaman, dan indikator halaman dengan bangganya membaca 1 of 1. Ketik ke field yang pindah ke halaman 2 dan tombolnya mendarat di tempat tak terlihat. Tak satu pun muncul dengan form sampel panjang-tetap yang pertama kali dites semua orang, dan alasannya layak diketahui kalau Anda menanam form viewer

Apa yang terjadi ketika form dynamic XFA merepaginasi?

Form dynamic XFA tak punya daftar halaman tetap, sehingga page count-nya adalah keluaran layout dan bisa berubah setiap kali user mengedit data. XFA 3.3 mendeskripsikan form sebagai tree subform; subform berulang dikendalikan oleh instanceManager, dan script seperti _Row.addInstance() meng-clone satu baris lagi. Layout processor lalu mengalirkan konten ke page area lagi, yang bisa menambah halaman, menghapus halaman, atau mendorong field yang ada ke halaman lain. ISO 32000-1 §12.7.8 hanya mendefinisikan cara paket XFA menumpang di dalam PDF; semua yang terjadi setelahnya milik XFA engine, yang di PDFium Component adalah layout XFA milik PDFium sendiri yang berjalan di proses host. Viewer Delphi karena itu berurusan dengan dokumen yang page count, ukuran halaman, dan posisi widget-nya semuanya state hidup. Tiga hal jadi salah ketika host menganggap sebaliknya:

  • Page count yang di-cache host untuk navigasi, rentang scroll, dan page spinner jadi basi, atau lebih buruk, ter-update dengan angka yang salah
  • Field yang berpindah menampilkan border-nya di posisi baru sementara editor dan hit area mouse tinggal di koordinat lama
  • Viewer menyimpan page handle yang sudah digantikan layout, sehingga klik dan paint menuju halaman yang tak ada lagi di form itu

Mempertahankan edit baris lintas save dan buka ulang adalah masalah terpisah dengan aturannya sendiri; artikel ini tetap pada yang terjadi saat runtime di dalam viewer

Runtime PDFium mana yang dibutuhkan dynamic XFA?

Dynamic XFA di PDFium Component membutuhkan build V8/XFA dari library native, dipilih lewat variabel global EnableV8Engine di unit PDFium sebelum dokumen pertama dimuat. Proses berkomitmen pada satu DLL pada saat pertama TPdf mana pun memuat library, dan build PDFium polos tak bisa menjalankan XFA engine sama sekali. Saat dokumen dibuka, TPdf memang mengintip file mencari marker XFA dan beralih ke build V8 secara otomatis, tapi hanya jika belum ada library polos yang termuat di proses itu. Ketika komitmen sudah terlanjur ke arah yang salah, TPdf.OnXfaRuntimeMissing menyala sekali agar host bisa menyuruh user me-restart. Menyetel flag secara eksplisit saat startup menghilangkan tebak-tebakan. Struktur callback FPDF_FORMFILLINFO yang membawa event XFA juga harus cocok dengan DLL; latarnya ada di FPDF_FORMFILLINFO versi 2 dan ABI callback XFA, dan deteksi form XFA dan pembacaan packet-nya membahas membedakan jenis form sebelum Anda membuka viewer

uses
  PDFium;

procedure TClaimForm.FormCreate(Sender: TObject);
begin
  // Putuskan sebelum TPdf pertama memuat library native:
  // proses tak bisa beralih dari pdfium.dll ke pdfium.v8.dll belakangan
  EnableV8Engine := True;

  FPdf := TPdf.Create(nil);
  FPdf.OnXfaRuntimeMissing := PdfXfaRuntimeMissing;
  FPdf.OnXfaPageCountChanged := PdfXfaPageCountChanged;
  FPdf.FileName := 'C:\Forms\expense-claim.pdf';
  FPdf.Active := True;

  PdfView1.Pdf := FPdf;
  PdfView1.OnPageChange := PdfViewPageChange;
  PdfView1.Active := True;

  UpdatePageRange(FPdf.PageCount);
end;

procedure TClaimForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
  StatusBar1.SimpleText :=
    'This XFA form needs the V8 runtime; restart the application to enable it';
end;

Mengapa PageCount melaporkan 1 untuk form dua halaman?

Sebelum v3.126.1, PDFium Component menyimpan argumen page_count dari page event native sebagai total dokumen, padahal argumen itu sebenarnya selisih absolut antara page count baru dan lama. PDFium memicu FFI_PageEvent setelah satu pass layout selesai dengan tipe event halaman ditambah atau halaman dihapus; secara internal ia meng-update page count tersimpannya lebih dulu lalu meneruskan abs(new - old). Pada layout awal, count lama nol, jadi delta sama dengan total, dan sampel statis tiga halaman melaporkan tiga halaman seperti harapan. Persis itulah kenapa form uji panjang-tetap tak pernah membongkar bug ini. Saat pertama kali form dynamic tumbuh dari satu halaman ke dua, delta-nya 1, dan wrapper menyetel TPdf.PageCount maupun parameter NewCount milik OnXfaPageCountChanged menjadi 1. Menghapus satu baris dari form tiga halaman menghasilkan omong kosong serupa ke arah sebaliknya

Mengakumulasi delta ke nilai sebelumnya juga bukan perbaikan yang aman. Urutan callback inisialisasi dan layout berarti wrapper tak selalu bisa memercayai count sebelumnya sebagai baseline, sehingga jumlah berjalan bisa menyimpang. Sejak v3.126.1, callback mengabaikan argumen sebagai count dan memanggil FPDF_GetPageCount pada dokumen, yang membaca total dari layout yang baru saja selesai. Ia lalu membersihkan page scene ter-cache, menyimpan total itu sebagai override XFA page-count di balik TPdf.PageCount, dan baru setelah itu memicu OnXfaPageCountChanged. Pada saat handler Anda berjalan, NewCount dan FPdf.PageCount sudah sepakat

Diagram dynamic XFA PDFium Component ketika menambah satu baris merepaginasi form satu halaman menjadi dua halaman dan FFI_PageEvent meneruskan abs(new minus old) sebagai delta, sehingga wrapper lama melaporkan TPdf.PageCount 1 sementara v3.126.1 membaca FPDF_GetPageCount dan melaporkan total yang benar
Page event native melaporkan delta tambah-atau-hapus, bukan total, sehingga v3.126.1 mengabaikan argumennya dan membaca layout yang selesai sebelum memicu OnXfaPageCountChanged
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
  // v3.126.1+: NewCount adalah total layout yang selesai, bukan delta.
  // Ini berjalan di dalam layout callback PDFium: hanya update state UI host,
  // jangan tutup dokumen atau muat ulang halaman dari sini
  UpdatePageRange(NewCount);
end;

procedure TClaimForm.PdfViewPageChange(Sender: TObject);
begin
  // Menyala setelah setiap muat ulang halaman, termasuk deferred XFA refresh
  PageSpin.Value := PdfView1.PageNumber;
end;

procedure TClaimForm.UpdatePageRange(Count: Integer);
begin
  PageSpin.MinValue := 1;
  PageSpin.MaxValue := Count;
  PageLabel.Caption := Format('of %d', [Count]);
end;

Event hanya menyala untuk form Full XFA yang layoutnya berubah saat runtime. Dokumen Static XFA dan AcroForm tak pernah memicunya, jadi viewer yang menangani keduanya bisa membiarkan handler yang sama ter-assign. Membiarkannya tanpa assign juga aman; override di balik TPdf.PageCount diterapkan apa pun yang terjadi, dan event itu ada supaya host bisa menyegarkan apa pun yang di-cachenya

Mengapa kotak input tetap di halaman lama ketika sebuah field berpindah?

Border berpindah dan editor tidak karena notifier XFA native membandingkan rectangle dengan dirinya sendiri. Ketika layout mengubah geometri widget yang sudah termuat, PDFium seharusnya menyadari rectangle baru dan memanggil PerformLayout pada widget itu, yang memposisikan ulang text editor dan hit area-nya. Pemeriksaannya membandingkan GetWidgetRect() dengan RecacheWidgetRect(). Kedua fungsi mengembalikan const reference ke member yang sama, dan recache menimpa member itu di tempat, sehingga perbandingan selalu melihat dua nilai identik dan widget termuat melewatkan relayout-nya

Gejalanya muncul ketika sebuah tes mengubah tinggi subform sehingga field yang ada melintasi ke halaman berikutnya. Di kedua arsitektur V8, border field digambar di posisi barunya sementara teks hasil ketikan dan hit area mouse tinggal di koordinat Y sebelumnya. Relayout eksplisit tak memperbaikinya, demikian pula memuat ulang halaman, karena widget masih percaya geometrinya mutakhir. Library Windows V8 yang dikirim bersama v3.126.1 menyalin rectangle lama by value sebelum recache dan membandingkan salinan itu, sehingga widget yang berpindah melakukan relayout dan nilai hasil edit muncul tepat di tempat border berada. Ini perbaikan native: ia bepergian bersama DLL, jadi meng-update unit Pascal sambil mempertahankan pdfium.v8.dll lama membiarkan hit area yang salah tempat tetap di sana. Regression check yang mendorongnya mengedit baris yang bertahan ke nilai nondefault lebih dulu lalu menuntut nilai itu di lokasi baru field, karena baris yang dibangun ulang dengan nilai default akan terlihat seperti lulus

Diagram relayout widget PDFium Component yang mengontraskan perbandingan-diri lama ketika GetWidgetRect dan RecacheWidgetRect mengembalikan satu member bersama sehingga widget yang berpindah melewatkan PerformLayout, dengan pemeriksaan copy-by-value Windows V8 v3.126.1 yang memposisikan ulang editor dan hit area mouse ke border hasil gambar ulang
Membandingkan rectangle dengan dirinya sendiri tak pernah gagal, sehingga border berpindah sementara teks ketikan dan klik tertinggal sampai pemeriksaannya menyimpan salinan by value lebih dulu

Bagaimana TPdfView memuat ulang halaman tanpa menarik handle dari bawah PDFium?

Sejak v3.126.2, TPdfView menunda muat ulang halaman yang menyusul perubahan layout XFA sampai call stack native terurai. Page event biasanya menyala saat PDFium masih memproses input: user mengklik tombol Add Row, klik itu menjalankan script, script mengubah instance count, dan layout selesai di dalam panggilan native yang sama. Menutup dan membuka ulang page handle pada saat itu akan mem-free objek yang masih dipakai caller. Sebelum v3.126.2, viewer hanya meng-invalidasi dirinya, sehingga page handle yang ditampilkan bisa terus menunjuk state pra-layout, dan kalau user sedang di halaman terakhir ketika halaman itu lenyap, nomor halaman terpilih keluar dari rentang

Deferred refresh bekerja dalam beberapa langkah kecil, dan langkah-langkah itu menjelaskan perilaku yang Anda lihat dari host:

  1. Callback page-event menandai view sebagai punya deferred XFA layout refresh yang tertunda dan meng-post pesan window privat; event berulang sebelum pesan tiba bergabung menjadi satu refresh
  2. View yang belum punya window handle menyimpan flag tertunda dan meng-post pesan dari CreateWnd, sementara mengganti dokumen, menonaktifkan view, atau menghancurkannya membersihkan flag itu
  3. Ketika pesan tiba, view membersihkan seleksi teks, highlight pencarian, dan index field yang difokuskan, karena ketiganya merujuk layout lama
  4. Halaman terpilih di-clamp ke PageCount baru; nomor halaman yang berubah melewati pergantian halaman normal, jika tidak, halaman saat ini dimuat ulang, dan fit mode diterapkan lagi
  5. Kalau layout meninggalkan nol halaman, view membuang page handle lamanya alih-alih menggambar halaman yang tak ada lagi
Diagram deferred XFA refresh TPdfView PDFium Component ketika page event di dalam call stack layout native hanya menandai refresh tertunda dan meng-post pesan window, yang belakangan membersihkan state seleksi basi, meng-clamp halaman ke PageCount baru, dan memuat ulang atau membuang page handle
Muat ulang menunggu sampai call stack native terurai: pesan yang di-post menggabungkan event berulang, lalu view meng-clamp halaman, memuatnya ulang, dan memicu OnPageChange

Kendala yang sama berlaku untuk kode Anda sendiri. OnXfaPageCountChanged berjalan di dalam layout callback native itu, jadi perlakukan seperti notifikasi: update label, rentang spinner, dan state toolbar di sana, dan antrekan apa pun yang lebih berat, seperti menutup dokumen atau membuka yang lain, dengan pesan yang di-post agar berjalan setelah callback kembali. TPdfView.OnPageChange lalu memberi tahu kapan view benar-benar memuat ulang halaman, dan membaca PdfView1.PageNumber pada saat itu memberi Anda nilai hasil clamp. Traversal tombol Tab dan pemeriksaan FormType yang dijalankan form viewer saat membuka dibahas di navigasi form field PDF dengan PDFium Component

Mengapa mengklik field Full XFA memicu "Cannot open text page"?

Halaman Full XFA tak punya text page PDF, dan sebelum v3.126.2 seleksi teks default milik viewer serta deteksi link tetap mencoba memuatnya. Dengan TPdfView.AllowUserTextSelection di default-nya True, hover bertanya ke text layer tentang karakter di bawah mouse, dan klik mouse-up menjalankan probe URL otomatis atas teks halaman. Di halaman Full XFA, text page tak bisa dibuka, sehingga klik biasa ke sebuah field bisa berakhir exception Cannot open text page. Sejak v3.126.2, kedua jalur internal mengembalikan tanpa hasil ketika TPdf.FormType adalah ftXfaFull dan runtime XFA tersedia, jadi setting default bekerja dan input field tetap tersedia

Mematikan AllowUserTextSelection untuk dokumen Full XFA tetap pilihan UI yang masuk akal, karena tak ada teks halaman untuk diseleksi dan gestur drag tak seharusnya memulai mode seleksi. Tapi itu bukan pengganti upgrade: di versi lebih lama, probe URL saat klik tak bergantung pada property itu, sehingga viewer bisa mengenai exception yang sama dengan seleksi dimatikan

procedure TClaimForm.ConfigureViewerForForm;
begin
  // FormType membaca dokumen yang terbuka, jadi panggil ini setelah FPdf.Active := True
  if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
  begin
    // Tak ada text layer PDF di halaman Full XFA; field tetap bisa diedit
    PdfView1.AllowUserTextSelection := False;
    StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
      [FPdf.PageCount]);
  end
  else
    PdfView1.AllowUserTextSelection := True;
end;

Mengetik butuh perbaikannya sendiri di v3.126.2. Text editor XFA native tidak mengganti seleksi ketika menerima karakter: FORM_OnChar menyisipkan di caret, dan Backspace menghapus satu karakter, sehingga menyeleksi sebuah nilai lalu mengetik menimpanya menghasilkan teks lama dan baru berdampingan. PDFium Component kini mengingat bahwa klik mendarat di XFA text field dan mengalirkan karakter ketikan, Backspace, dan Delete melalui FORM_ReplaceSelection kapan pun ada seleksi dan dokumen memberi izin fill-forms atau modify. Apakah field XFA read-only boleh berubah tetap diputuskan oleh editor native, sehingga field yang ditandai read-only di form mempertahankan nilainya bahkan di dokumen yang lainnya mengizinkan pengisian. Menyetel TPdfView.AllowFormEvents ke False juga menghentikan routing keyboard ini, yang menjaga viewer read-only tetap read-only

Referensi cepat: dynamic XFA di viewer Delphi

GejalaPenyebabDiperbaiki di
Page count menampilkan 1 setelah form tumbuh menjadi dua halamanPage event native meneruskan delta tambah/hapus, bukan totalv3.126.1 (wrapper)
Border field berpindah, teks ketikan dan hit area tertinggalWidget termuat melewatkan relayout setelah perbandingan-diriv3.126.1 (library Windows V8)
Viewer menggambar atau mengalirkan input ke state halaman pra-layoutPage handle tak dimuat ulang setelah repaginasiv3.126.2 (deferred refresh)
Klik ke field memicu Cannot open text pageSeleksi teks dan probe URL pada halaman tanpa text layerv3.126.2
Mengetik menimpa nilai terseleksi menambah alih-alih menggantiEditor XFA native menyisipkan di caretv3.126.2
  • Set EnableV8Engine ke True sebelum dokumen mana pun dimuat, dan tangani OnXfaRuntimeMissing untuk kasus library polos termuat lebih dulu
  • Baca total dari TPdf.PageCount atau parameter NewCount milik OnXfaPageCountChanged; jangan pernah menambah atau mengurangi page count sendiri
  • Jaga handler OnXfaPageCountChanged tetap ringan, karena ia berjalan di dalam layout callback native
  • Sinkronkan indikator halaman saat ini di TPdfView.OnPageChange, yang menyala setelah muat ulang tertunda meng-clamp nomor halaman
  • Deploy DLL Windows V8 v3.126.1 atau lebih baru bersama unit-unitnya; perbaikan relayout widget tinggal di kode native
  • Tes dengan form yang benar-benar mengubah page count dan memindahkan field yang diedit melintasi page break, karena sampel panjang-tetap menyembunyikan setiap bug di daftar ini

Dynamic XFA menjadikan page count dan geometri field nilai hidup, dan viewer tetap benar hanya ketika mengambilnya dari layout yang selesai serta memuat ulang halaman di momen yang aman. PDFium Component menangani keduanya di dalam TPdf dan TPdfView, sehingga host tinggal mendengarkan. Detail dan unduhan ada di halaman produk PDFium Component for Delphi