Artikel Teknis

Setel Nilai Field Form di PDF Termuat dengan Delphi

HotPDF Delphi Component mengisi field AcroForm yang sudah ada pada PDF termuat lewat THotPDF.SetFormFieldValue, dialamati entah dengan indeks field berbasis nol atau dengan nama field yang memenuhi syarat penuh. Menulis entri /V yang baru adalah bagian yang mudah; yang membuat panggilan ini andal pada form dunia nyata adalah bahwa method yang sama juga menjaga tiga potong state tetap konsisten yang tak terlihat sampai ada yang salah: identitas field yang terdekode supaya nama non-ASCII bisa ditemukan sama sekali, state appearance /AS pada widget checkbox dan radio, dan array indeks pilihan /I pada choice field. Appearance stream yang terlihat adalah langkah terpisah dan eksplisit lewat EnsureLoadedFieldAppearanceStream

Skenarionya yang biasa: pelanggan mengirimi Anda form mereka sendiri, deklarasi pajak, klaim asuransi, purchase order yang dibuat seseorang di Acrobat bertahun-tahun lalu, dan aplikasi Delphi Anda harus mengisinya dari database lalu mengembalikan file yang terbuka dengan benar di mana pun. Anda tidak mengendalikan bagaimana form itu dibuat. Nama field bisa terenkode UTF-16, nilai ekspor checkbox bisa 2 alih-alih Yes, dan combo box bisa memakai pasangan opsi [export display]. Setiap detail itu punya aturannya di ISO 32000-1, dan setiap aturannya kini ditangani SetFormFieldValue untuk Anda. Artikel ini soal apa yang dilakukannya, kenapa, dan di mana ia berhenti. Untuk masalah saudaranya, yaitu membuat field yang belum ada, lihat menambahkan field AcroForm ke PDF yang dimuat di Delphi

Kenapa SetFormFieldValue gagal menemukan field dengan nama non-ASCII?

Sebelum v2.752.1 jawabannya adalah encoding: field-nya hidup di file dengan nama UTF-16BE heksadesimal, dan cache namanya menyimpan ejaan hex itu alih-alih teksnya. ISO 32000-1 §12.7.3.1 mendefinisikan nama field parsial /T sebagai text string, dan §7.9.2.2 menyatakan text string boleh UTF-16BE dengan byte order mark FE FF di depan. Alat authoring rutin menyerialkan nama semacam itu sebagai string hex menurut §7.3.4.3, jadi field bernama Straße datang sebagai <FEFF005300740072006100DF0065>. Di dalam HotPDF, THPDFStringObject.Value memegang teks heksadesimal mentahnya setiap kali IsHexadecimal aktif, dan itu persis yang Anda inginkan untuk perjalanan bolak-balik dictionary aslinya tanpa kehilangan apa pun dan persis yang tidak Anda inginkan sebagai key lookup. HPDFLoadedFormTextName memisahkan kedua urusan itu. Saat cache relasinya dibangun, setiap nilai /T melewatinya: kalau objek string-nya heksadesimal, HPDFHexToBytes memulihkan urutan byte-nya; kalau byte-nya diawali FE FF dan panjangnya genap, payload-nya didekode sebagai UTF-16BE lalu dienkode ulang sebagai UTF-8; hasilnya lalu digabung ke nama parent-nya dengan titik untuk membentuk nama berkualifikasi penuh yang dijelaskan §12.7.3.1, jadi kid bernama City di bawah parent bernama Address terdaftar sebagai Address.City. Key cache-nya dinormalisasi ke huruf kecil, yang membuat SetFormFieldValue('address.city', ...) juga berhasil; itu kemudahan di luar standarnya, karena spesifikasi memperlakukan nama sebagai case-sensitive. Yang krusial: hanya key cache-nya yang berubah. Objek /T di dictionary field-nya tetap mempertahankan encoding heksadesimalnya, jadi menyimpan dokumennya tidak menulis ulang identitas field yang sekadar Anda isi

Cara HotPDF meresolusi nama AcroForm non-ASCII: HPDFHexToBytes memulihkan payload UTF-16BE di balik string /T heksadesimal, byte order mark FE FF didekode lalu dienkode ulang sebagai UTF-8, dan nama berkualifikasinya digabung ke parent-nya sehingga Applicant.FullName dan field bernama Straße sama-sama mendarat di cache lookup
Hanya key cache-nya yang berubah: dictionary field-nya tetap mempertahankan encoding heksadesimalnya, lookup dinormalisasi ke huruf kecil sebagai kemudahan di luar standarnya, dan menyimpan dokumennya tidak pernah menulis ulang identitas field yang sekadar Anda isi
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;

    // Nama berkualifikasi didekode dari string /T UTF-16BE lalu
    // digabung dengan titik, jadi nama bersarang dan non-ASCII teresolusi
    Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
    Pdf.SetFormFieldValue('Applicant.Straße', 'Hauptstraße 12');

    // Nilai yang bukan Latin-1 berjalan sebagai hex UTF-16BE berawalan FEFF
    // lalu ditulis sebagai string heksadesimal PDF
    Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');

    Pdf.SaveLoadedDocument('claim-form-filled.pdf');
  finally
    Pdf.Free;
  end;
end;

Apa yang sebenarnya ditulis SetFormFieldValue?

Kedua overload-nya menjalankan lima langkah yang sama: temukan dictionary field-nya, tulis /V lewat HPDFSetDictFormValue, rekonsiliasi indeks pilihan choice, tandai dictionary-nya kotor, rekonsiliasi state appearance tombolnya, dan terakhir catat indeks field-nya lewat NoteLoadedFormFieldDirty. Langkah terakhir itu penting kalau form-nya membawa script kalkulasi, karena himpunan kotor itulah yang dikonsumsi overload RecalculateLoadedFormFieldsIncremental tanpa parameter untuk menjalankan ulang hanya kalkulasi yang secara transitif membaca field yang berubah. HPDFSetDictFormValue sendiri berhati-hati soal tipe objek yang digantikannya. Kalau /V yang ada adalah objek name, yang dipakai field checkbox dan radio sebagai nilai ekspornya, nilai barunya ditulis sebagai name, tidak pernah sebagai string, karena name PDF menurut konstruksinya hanya ASCII. Kalau tidak, ia menulis objek string lalu memeriksa nilai yang Anda kirim: string yang diawali FEFF, panjangnya genap, dan hanya terdiri dari digit hex diperlakukan sebagai bentuk wire UTF-16BE dari §7.9.2.2 dan disimpan dengan IsHexadecimal aktif, jadi ia terserialisasi sebagai <FEFF...> alih-alih sebagai literal (FEFF...). Itulah mekanisme yang diandalkan baris City di atas; string lain disimpan sebagai string literal dengan byte yang Anda berikan, jadi untuk teks Latin biasa Anda cukup mengirim teks biasa

Kenapa checkbox tetap menyimpan centang lamanya setelah nilainya berubah?

Karena untuk field tombol, nilainya saja tidak menentukan apa yang digambar. ISO 32000-1 §12.7.4.2.3 menetapkan bahwa widget checkbox membawa state appearance /AS yang menyebut stream mana di /AP /N yang sedang ditampilkan, dan viewer melukis dari /AS, bukan dari /V. Kalau Anda mengubah /V jadi Yes tapi membiarkan /AS di Off, file-nya bertentangan secara internal, dan flattening akan dengan senang hati memanggang appearance tak-tercentang yang basi ke halamannya sementara data form-nya menyatakan tercentang. ReconcileLoadedButtonAppearanceStates ada untuk menutup celah itu: untuk field yang /FT-nya Btn, ia mengunjungi dictionary field-nya sendiri dan setiap entri di array /Kids-nya, membaca nama state-on dari /AP /N, lalu menulis ulang /AS ke nama itu ketika cocok dengan nilai field-nya atau ke Off ketika tidak cocok

Kenapa checkbox HotPDF tetap menyimpan centang lamanya ketika hanya /V yang berubah: viewer melukis dari state appearance /AS ke /AP /N, jadi ReconcileLoadedButtonAppearanceStates mengunjungi field-nya beserta setiap kid-nya, membaca nama state-on sebagai key pertama selain Off, lalu menulis ulang /AS saat cocok atau ke Off bila tidak
Grup radio membandingkan setiap kid terhadap nilai parent yang dipulihkan InheritedButtonValue dengan menelusuri rantai /Parent, jadi menyetel grupnya ke satu nilai ekspor menyalakan tepat widget itu dan mematikan setiap saudaranya

Dua detail dari form nyata membentuk perbaikan v2.752.3. Pertama, dictionary appearance normal boleh hanya berisi state on; §12.7.4.2.3 menamai appearance off sebagai Off tapi alat authoring sering menghilangkan stream-nya dan membiarkan viewer menggambar apa pun. Kode sebelumnya menyerah ketika dictionary-nya berisi kurang dari dua entri, jadi checkbox berstate tunggal itu diam-diam menyimpan centang lamanya. Pemeriksaannya sekarang sederhana, yaitu dictionary-nya tidak kosong, dan nama state-on-nya diambil sebagai key pertama yang bukan Off. Kedua, nama state-on-nya adalah apa pun yang dipilih authornya. Form nyata memakai 2, Yes, On, atau kata yang dilokalkan, jadi perbandingannya terhadap key yang sebenarnya, tanpa peduli huruf besar-kecil, tidak pernah terhadap Yes yang di-hard-code. Tombol radio menambah satu kerutan lagi, yang dijelaskan di §12.7.4.2.4: pilihannya berada di /V milik field parent-nya, sedangkan kid-nya masing-masing memiliki widget-nya dan biasanya tidak punya /V sendiri. Helper bertingkat InheritedButtonValue karena itu menelusuri rantai /Parent, sampai 64 tingkat, hingga menemukan nilai yang tidak kosong, jadi setiap kid dibandingkan terhadap nilai grup tempatnya bernaung. Menyetel parent-nya ke nilai ekspor salah satu kid menyalakan tepat kid itu dan mematikan setiap saudaranya

// Checkbox: nilai ekspornya harus cocok dengan key state-on di /AP /N
// (sering 'Yes', tapi form nyata memakai '2', 'On', atau apa pun)
Pdf.SetFormFieldValue('Consent', 'Yes');

// Grup radio: /V ditulis di parent-nya; setiap widget kid mendapat
// /AS berisi nama ekspornya sendiri atau Off
Pdf.SetFormFieldValue('PaymentMethod', 'Card');

// Mengosongkan checkbox: nilai apa pun yang tidak cocok state on menghasilkan /AS Off
Pdf.SetFormFieldValue('Newsletter', 'Off');

Choice field: menjaga /I tetap selaras dengan /V

Untuk combo box atau list box, /V bukan satu-satunya tempat pilihan dicatat. Tabel 231 di §12.7.4.4 mendefinisikan /I sebagai array indeks berbasis nol ke /Opt yang mengidentifikasi item yang dipilih, dan viewer yang menemukan /I menunjuk opsi 0 sementara /V menyebut opsi 3 bisa menyorot baris yang salah. Sejak v2.754.1, HPDFReconcileChoiceSelection berjalan di dalam setiap panggilan SetFormFieldValue dan, bila /FT yang diwarisinya Ch, membangun ulang /I dari nilai barunya. Urutan operasinya disengaja. Entri /I lokalnya dihapus lebih dulu, tanpa menyentuh isinya: kalau array lamanya adalah objek tak langsung yang dipakai bersama field lain, memutasinya di tempat akan merusak pilihan field lain itu, jadi rutinnya membuang referensinya lalu membuat array langsung yang baru. Ia lalu meresolusi /Opt lewat rantai /Parent, karena opsi choice bisa diwariskan, dan memindai entri-entrinya. Opsi berupa string polos dibandingkan langsung; pasangan [export display] dibandingkan pada elemen ekspornya, dan pasangan dengan kurang dari dua elemen dilewati. Kedua sisinya melewati HPDFLoadedFormTextName, jadi opsi hex UTF-16 cocok dengan nilai hex UTF-16 tanpa Anda harus mengejanya identik. Pada kecocokan pertama, /I satu elemen ditulis dan pemindaiannya berhenti; nilai skalar selalu menggantikan multi-pilihan sebelumnya, terlepas dari flag MultiSelect

Cara HotPDF menjaga choice field tetap konsisten: HPDFReconcileChoiceSelection menghapus array /I lokalnya sebelum menyentuhnya, meresolusi /Opt lewat rantai /Parent, membandingkan belahan ekspor setiap opsi lewat HPDFLoadedFormTextName, menulis /I satu elemen pada kecocokan pertama, dan tidak menulis apa pun ketika nilai combo yang editable tidak punya indeks
Opsi berupa string polos dibandingkan langsung dan pasangan export display dibandingkan pada elemen ekspornya, sementara nilai di luar /Opt dengan tepat meninggalkan indeks kosong — /I yang basi dan menunjuk baris yang salah lebih buruk daripada tidak ada sama sekali

Ketika tidak ada yang cocok, tidak ada /I yang ditulis sama sekali. Itu hasil yang benar untuk combo box yang editable, di mana §12.7.4.4 mengizinkan pengguna mengetik nilai di luar daftar opsinya; nilai semacam itu tidak punya indeks, dan indeks yang basi lebih buruk daripada tidak ada. Itu juga yang Anda dapat kalau Anda mengirim label tampilan alih-alih nilai ekspor ke daftar opsi berpasangan, jadi ketika sebuah combo box menolak menampilkan pilihan Anda, periksa belahan mana dari pasangannya yang Anda kirim

// /Opt adalah [[US United States] [CA Canada] [MX Mexico]]:
// cocokkan pada nilai ekspornya, dan /I menjadi [1]
Pdf.SetFormFieldValue('Country', 'CA');

// Combo editable dengan nilai di luar /Opt: /V tetap ditulis,
// /I dihapus, dan tidak ada indeks yang dikarang
Pdf.SetFormFieldValue('Title', 'Principal Engineer');

Nilai dan appearance adalah dua operasi terpisah

SetFormFieldValue tidak pernah menyentuh appearance stream field teks atau choice. Setelah panggilannya, /V memegang teks barunya sementara /AP /N masih melukis yang lama, dan mana dari keduanya yang ditampilkan viewer bergantung pada apakah dictionary AcroForm-nya membawa /NeedAppearances true menurut §12.7.3.3 dan apakah viewer-nya menuruti itu. Kalau Anda butuh file-nya merender nilai baru di setiap reader, termasuk flattener dan generator thumbnail yang mengabaikan flag itu, panggil EnsureLoadedFieldAppearanceStream dengan indeks field-nya. Ia membangun Form XObject dari string /DA yang diwarisi, quadding /Q, tata letak comb /MaxLen, dan nilainya, lalu meresolusi font bernama lewat resource /DR milik AcroForm supaya font Type0 mempertahankan descendant font-nya sendiri alih-alih merosot jadi Helvetica, dan mengembalikan True ketika setidaknya satu widget menerima stream. Overload SetFormFieldValue yang berbasis nama tidak memberi Anda indeksnya kembali, jadi ambil satu lewat GetFormField, yang mengembalikan THPDFLoadedFormField milik Anda dan harus Anda free. Suite regresi untuk perubahan v2.752.1 eksplisit soal pemisahan ini: ia menyetel nilai, memanggil EnsureLoadedFieldAppearanceStream, lalu merender halamannya dan memeriksa bahwa piksel di dalam persegi widget-nya berubah sementara piksel di luarnya tidak. Memverifikasi bahwa /V berubah tidak membuktikan apa pun soal apa yang akan dilihat pengguna

var
  Field: THPDFLoadedFormField;
begin
  Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
  Field := Pdf.GetFormField('Applicant.FullName');
  try
    // Lukis nilai barunya ke /AP supaya viewer yang mengabaikan
    // /NeedAppearances tetap menampilkannya
    if not Pdf.EnsureLoadedFieldAppearanceStream(Field.Index) then
      raise Exception.Create('No widget rectangle to paint into');
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('claim-form-filled.pdf');
end;

Batas yang layak diketahui sebelum Anda membangun di atas ini

ReconcileLoadedButtonAppearanceStates menguji /FT lokal dari dictionary yang Anda alamati, jadi ia bekerja pada parent radio atau pada checkbox yang membawa /FT-nya sendiri; widget kid yang dialamati sendiri, dengan /FT hanya di parent-nya, tidak direkonsiliasi lewat jalur itu. HPDFReconcileChoiceSelection menangani satu nilai skalar dan menulis paling banyak satu indeks; list box multi-pilihan dengan beberapa entri terpilih berada di luar apa yang dimodelkan SetFormFieldValue. Kedua rutinnya juga tidak memvalidasi nilai yang Anda kirim terhadap /Opt atau terhadap key state-on-nya, jadi salah ketik menghasilkan checkbox Off atau combo tanpa indeks alih-alih exception. Dan GetFormFieldValue mengembalikan teks /V yang tersimpan seperti adanya di dictionary-nya, yang untuk nilai terenkode hex berarti ejaan heksadesimalnya, bukan teks hasil dekodenya

Setelah nilainya masuk dan appearance-nya terlukis, dua langkah lanjutan yang wajar berada di kedua sisi operasi ini. Bertukar data field dalam jumlah besar dengan sistem eksternal, alih-alih satu panggilan SetFormFieldValue sekali waktu, adalah cakupan impor dan ekspor XFDF di Delphi. Dan ketika form yang terisi sudah final dan tidak boleh lagi bisa disunting, flattening field AcroForm dan XFA di Delphi memanggang persis state /AS dan appearance stream yang dijelaskan di sini ke konten halaman statis, itulah kenapa membuat semuanya konsisten sebelum flattening bukan pilihan opsional

API penyuntingan form termuat di artikel ini, termasuk SetFormFieldValue, EnsureLoadedFieldAppearanceStream, dan graph kalkulasi inkrementalnya, dikirim sebagai bagian dari HotPDF Delphi Component untuk Delphi dan C++Builder