HotPDF Delphi Component memperlakukan /FT, /Ff, /V, dan /DV pada field AcroForm termuat sebagai atribut yang bisa diwarisi, diresolusi dengan menelusuri rantai /Parent. Sejak v2.754.3 dan v2.754.4, anak bernama yang tipenya datang dari parent-nya tetap bisa dialamati satu per satu, RemoveFormField membiarkan saudara-saudaranya sendirian, dan ResetLoadedFormField menyalin default yang diwarisi dengan tipe objek PDF aslinya. Sebelum itu, jumlah form biasa yang salah baca mengejutkan
Form yang membongkar semua ini bukan form eksotis. Tool authoring membangun node grup group yang membawa /FT /Ch, flag field, dan daftar opsinya sekali saja, lalu menggantungkan dua anak bernama a dan b di bawahnya, masing-masing dictionary gabungan field-plus-widget yang hanya berisi /T, /Parent, /Rect, dan /V-nya sendiri. Itu cara berbagi atribut yang sepenuhnya legal, dan persis itulah kasus yang ditandai bagian Limits di menyetel nilai field form di PDF termuat dengan Delphi sebagai belum tertangani: rekonsiliasi tombol hanya melirik /FT lokalnya. Artikel ini mengambil alih dari tempat artikel itu berhenti, membahas bagaimana tree field diklasifikasi, bagaimana nilai yang diwarisi dibaca, dan apa yang boleh ditulis sebuah reset satu field
Entri AcroForm mana yang bisa diwarisi field dari parent-nya?
ISO 32000-1 §12.7.3.1, Tabel 220, menandai /FT, /Ff, /V, dan /DV sebagai bisa diwarisi, dan Tabel 229 di §12.7.4.3 melakukan hal yang sama untuk /MaxLen field teks, jadi reader mana pun yang hanya melirik dictionary lokalnya akan melaporkan tipe yang salah, flag yang salah, dan nilai kosong untuk anak yang sepenuhnya valid. HotPDF menyalurkan semua pembacaan itu lewat satu resolver internal, HPDFLoadedInheritedFieldObject, yang memeriksa key di dictionary, meresolusi referensi tak langsung kalau menemukannya, dan kalau tidak mengikuti /Parent paling jauh 128 level, karena file yang malformed bisa membangun siklus /Parent yang tak ada sangkut pautnya dengan /Kids. Getter publik berdiri di atasnya: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue, serta helper opsi GetLoadedFormFieldOptionCount dan GetLoadedFormFieldOptions, yang juga mengangkat array /Opt yang tersimpan di parent. Satu aturan di resolver itu mudah salah paham: penelusurannya berhenti di dictionary pertama yang memuat key-nya, meski value di sana string kosong. /V () lokal adalah override yang disengaja untuk menutupi parent, bukan celah yang harus diisi dari lebih atas di tree
var
Pdf: THotPDF;
Field: THPDFLoadedFormField;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
// 'group' membawa /FT /Ch, /Ff 131078 dan /Opt; anaknya
// 'group.b' hanya membawa /T, /Parent, /Rect dan /V miliknya
Field := Pdf.GetFormField('group.b');
try
if Pdf.GetFormFieldType(Field.Index) = lfftChoice then
begin
// 131078 = Combo (bit 18) + NoExport (bit 3) + Required (bit 2)
Writeln(Pdf.GetLoadedFormFieldFlags(Field.Index));
Writeln(Pdf.IsFormFieldRequired(Field.Index)); // TRUE
Writeln(Pdf.GetLoadedFormFieldOptionCount(Field.Index));
Writeln(Pdf.GetFormFieldValue(Field.Index)); // /V lokalnya
end;
finally
Field.Free;
end;
finally
Pdf.Free;
end;
end;
Kenapa /FT lokal adalah tes yang salah untuk field terminal?
Karena parent bisa menyediakan tipe dan tetap memiliki field anak bernama, jadi keberadaan /FT tak berkata apa-apa soal di mana tree field berakhir. Traversal lama menyatakan sebuah node terminal setiap kali ia punya /FT sendiri atau tak punya /Kids. Di form di atas, group punya /FT /Ch sekaligus /Kids, jadi ia terdaftar sebagai satu field bernama group dengan dua widget, dan nama-nama berkualifikasi penuh group.a dan group.b sekadar lenyap. GetFormFieldCount mengembalikan 1, lookup berdasarkan nama anak gagal, dan SetFormFieldValue hanya bisa menulis parent bersamanya. Tes penggantinya, HPDFLoadedFieldHasChildFields, melirik anak-anaknya alih-alih parent-nya: sebuah kid adalah field anak kalau ia punya /T sendiri, punya /Kids sendiri, atau sama sekali bukan dictionary /Subtype /Widget. Hanya ketika tak ada kid yang memenuhi syarat node itu terminal, dengan kid-kidnya diperlakukan sebagai anotasi widget miliknya
Dua kasus tepi yang membentuk aturan itu sama-sama datang dari dictionary gabungan, yang diizinkan §12.7.3.1 ketika sebuah field punya satu widget. Dictionary gabungan bernama membawa /Subtype /Widget dan tetap merupakan field anak, jadi subtype-nya sendiri tak bisa mengirimnya ke daftar widget anonim milik parent; /T-nya yang menang. Kebalikannya juga terjadi: sebagian produser mengulang /FT milik parent pada setiap widget anonim, jadi /FT tak bisa dipakai sebagai bukti bahwa sebuah widget memulai field baru. Klasifikasi ini dipakai bersama oleh cache relasi, FormFieldExists, dan RemoveFormField, dan setiap penelusurannya kini mencatat dictionary yang sudah dikunjunginya dan berhenti melewati 128 level. File regresi yang grupnya mendaftarkan dirinya dua kali, /Kids [5 0 R 5 0 R 6 0 R 7 0 R], tetap melaporkan tepat dua field alih-alih merekursi selamanya atau menghitung node yang sama dua kali
Bagaimana RemoveFormField menghindari menghapus field saudara?
RemoveFormField kini menghapus hanya anak yang Anda sebut, karena discovery dan penghapusan akhirnya sepakat soal apa itu field terminal. Kesepakatan itu lebih berarti daripada kelihatannya. Overload berbasis nama meresolusi sebuah indeks lewat cache relasi lalu menghitung field terminal dalam penelusuran kedua atas /AcroForm /Fields. Begitu cache diperbaiki agar melihat group.a dan group.b, penelusuran penghapusan yang belum diperbaiki masih akan memperlakukan group sebagai satu field terminal, dan indeks 0 akan menghapus parent beserta setiap saudara dan semua widget mereka. Penelusuran penghapusannya kini memakai tes HPDFLoadedFieldHasChildFields yang sama dan set visited yang sama, mengumpulkan anotasi widget milik anak yang dihapus saja, mencabutnya dari /Annots setiap halaman, dan menghapus parent hanya ketika array /Kids-nya berujung kosong. Regresinya memeriksa ketiga tempat di mana kesalahan akan tampak: /Kids milik parent, /Annots halaman, dan value serta appearance milik saudara yang selamat, baik setelah penulisan ulang penuh maupun setelah update inkremental
// Hapus satu anak bernama; saudaranya dan parent bersamanya selamat
Pdf.RemoveFormField('group.a');
Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// Tipe, flag, dan opsi tetap diresolusi lewat parent
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');
Apa yang ditulis ResetLoadedFormField saat defaultnya diwarisi?
ResetLoadedFormField menulis /V lokal yang merupakan salinan segar dari /DV yang diwarisi dengan tipe objek PDF yang sama, dan ia memvalidasi seluruh defaultnya sebelum menyentuh field. Tipe objeknya penting karena getter skalar meratakan segalanya menjadi teks. Default checkbox adalah name seperti /Yes, default list box multi-pilihan adalah array berisi string, dan default teks bisa berupa string UTF-16 heksadesimal; menyalin salah satunya lewat GetLoadedFormFieldDefaultValue akan mengubah name menjadi string, array menjadi string kosong, dan string hex menjadi digit literalnya. Resetnya karena itu bercabang menurut tipe yang diwarisi: field teks dan choice mendapat objek string baru yang mempertahankan flag IsHexadecimal, field choice dengan default berupa array mendapat array baru berisi string baru, dan tombol non-pushbutton mendapat objek name baru. Menyalin, alih-alih menunjuk objek milik parent, memang disengaja: /V yang berbagi array /DV milik parent atau nomor objeknya akan mengubah defaultnya saat berikutnya ada yang menyunting nilainya. Default dengan tipe yang salah, atau array choice yang memuat apa pun selain string, melempar exception dan membiarkan /V serta /I persis seperti semula. Pushbutton, yang tak punya nilai (Tabel 226, bit 17), dan field signature mundur ke jalur lama yang hanya mengenal string
Ketika tak ada /DV sama sekali di sepanjang rantai ke atas, method ini tetap menepati kontrak pembersihannya dengan menulis string kosong lokal, atau /Off untuk field checkbox atau radio. Menghapus /V lokalnya memang tampak lebih rapi dan itu keliru: parent bisa memegang nilai terkini, dan mencabut override milik anak akan mengembalikan nilai itu diam-diam. Ini juga alasan reset satu field bukanlah action ResetForm dari §12.7.5.3, yang dijalankan viewer atas satu set field ketika pengguna mengklik tombol, sebagaimana dijelaskan di membangun field dan action AcroForm dengan HotPDF. ResetLoadedFormField adalah operasi penyuntingan pada satu field termuat, dengan aturannya sendiri untuk kasus tanpa default, dan ia mencatat field itu lewat NoteLoadedFormFieldDirty agar rekalkulasi inkremental melihat perubahannya
var
Field: THPDFLoadedFormField;
begin
Field := Pdf.GetFormField('group.a');
try
// Parent memegang /DV [(b) (r)] pada list box MultiSelect: group.a dapat
// /V [(b) (r)] dan /I [0 2] segarnya sendiri; parent tak tersentuh
Pdf.ResetLoadedFormField(Field.Index);
// Getter skalar tak bisa merepresentasikan default berupa array
Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // kosong
finally
Field.Free;
end;
Pdf.SaveLoadedDocument('survey-reset.pdf');
end;
Menjaga /V, /I dan /AS tetap selaras
Reset hanya benar jika indeks pilihan dan state appearance mengikuti nilainya, jadi ResetLoadedFormField diakhiri dengan dua reconciler yang sama dengan SetFormFieldValue. HPDFReconcileChoiceSelection kini menerima value berupa array: ia menghapus /I lokal tanpa memutasi itu, mencocokkan setiap nilai terhadap belahan ekspor setiap entri /Opt, dan menulis satu /I terurut yang baru, jadi reset ke [(b) (r)] terhadap opsi b, g, r menghasilkan /I [0 2]. ReconcileLoadedButtonAppearanceStates kini meminta tipe yang diwarisi, jadi checkbox anak yang /FT /Btn-nya berada di parent akhirnya kebagian /AS yang disetel. Di sisi penulisan, SetFormFieldValue dan SetLoadedFormFieldDefaultValue menyimpan objek name untuk tombol non-pushbutton yang diwarisi bahkan ketika anaknya tak punya entri lokal untuk menyalin tipenya. Dan ketika EnsureLoadedFieldAppearanceStream membangun ulang appearance tombol, ia menulis /AS /Off kecuali nilainya cocok dengan state on, dan memberi setiap state stream /Type /XObject, /Subtype /Form, dan /BBox yang benar; sebelum v2.754.4, regenerasi appearance setelah reset bisa mencentang kotaknya lagi sebelum file tersimpan
Batas yang layak diketahui sebelum membangun di atasnya
Getter skalar tetap skalar. GetFormFieldValue dan GetLoadedFormFieldDefaultValue mengembalikan string kosong untuk value berupa array, meng-string-kan angka dan boolean sebagai 42 atau true, dan melaporkan string terenkode hex dalam ejaan heksadesimalnya. Siklus /Parent mengakhiri penelusuran tanpa exception, jadi field yang tipenya hilang dalam siklus melaporkan lfftUnknown dan flag 0 alih-alih gagal. SetFormFieldValue dan ResetLoadedFormField selalu menulis anak yang Anda alamati dan tak pernah mempromosikan nilai ke parent bersama, yang tepat untuk anak-anak independen tapi berarti grup radio sebaiknya dialamati lewat field yang memiliki pilihannya. Dan setiap panggilan meng-commit satu field secara sendiri-sendiri; tak ada di sini yang membuat satu batch reset menjadi transaksional
Resolusi atribut terwarisi, klasifikasi tree field yang terpadu, dan reset bertipe yang dijelaskan di sini adalah bagian dari API form termuat di HotPDF Delphi Component untuk Delphi dan C++Builder, bersama pembuatan field yang dibahas di menambahkan field AcroForm ke PDF termuat di Delphi