HotPDF memutar balik nilai list box multi-select lewat FDF dan XFDF dengan menjaga nilai field sebagai array dari ujung ke ujung. Sejak versi 2.755.0, ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF, dan ExportLoadedFormToXFDF menulis setiap opsi terpilih sebagai string FDF atau elemen <value> XFDF miliknya sendiri, dan method impor yang bersesuaian memeriksa setiap nilai terhadap opsi field serta membangun ulang indeks pilihan /I sebelum mengubah apa pun. Tak ada yang direkat menjadi satu string di sepanjang jalannya
Kegagalan yang diperbaiki ini mudah direproduksi. Ambil satu order form dengan list box multi-select berisi opsi produk, biarkan pengguna memilih dua di antaranya, ekspor data form-nya untuk sistem back-office, lalu impor file yang sudah disunting kembali ke PDF. Sebelum perubahan ini, list box-nya kembali kosong atau salah. Sebabnya, salah satu nilai ekspornya memuat line break, dan jalur lama telah meratakan pilihan-pilihannya menjadi satu string berpemisah baris. Mengeluarkan kembali beberapa pilihan dari string itu tak pernah andal, dan dengan nilai ekspor yang sendirinya memuat line break, itu sama sekali tak mungkin bekerja
Kenapa merekatkan nilai multi-select dengan line break merusak round trip-nya?
Merekatkan pilihan menjadi satu string membuang batas antar nilai, dan sebuah nilai bisa memuat pemisahnya, jadi tak ada importer yang bisa membelah string itu kembali dengan benar. ISO 32000-1 §12.7.4.4 mengizinkan entri /V milik choice field berupa satu text string tunggal atau array text string, dan list box dengan flag MultiSelect (bit 22 dari /Ff) memakai bentuk array begitu lebih dari satu opsi dipilih. Seksi yang sama mendefinisikan /I sebagai array indeks opsi berbasis nol dalam urutan naik, yang dipakai viewer untuk membedakan dua opsi yang kebetulan berbagi nilai ekspor. Di HotPDF, getter skalar GetFormFieldValue hanya membaca bentuk string, jadi menyodorkan array lewat itu menurunkan ekspornya menjadi string kosong, dan impor XFDF yang lama merekatkan elemen <value> yang berulang dengan LF. Bayangkan satu opsi terekspor sebagai Deep, line feed, Blue: setelah direkatkan, Deep\nBlue\nRed bisa berarti dua pilihan atau tiga, dan filenya tak memberi cara mengetahui yang mana. Perbaikannya adalah berhenti memakai skalar di tengah round trip sama sekali
Apa isi file FDF dan XFDF hasil ekspor itu?
HotPDF menulis nilai multi-select sebagai array bertipe di FDF dan sebagai satu elemen <value> per pilihan di XFDF, jadi batas-batasnya tetap terlihat di disk. Di FDF setiap item mempertahankan ejaan yang dimilikinya di PDF sumber: string heksadesimal keluar sebagai hex, dan string literal di-escape oleh satu helper yang mengubah CR dan LF menjadi \r dan \n. Di XFDF, root-nya membawa xml:space="preserve" sebagaimana disyaratkan ISO 19444-1, yang berarti whitespace apa pun di dalam elemen teks dihitung sebagai data. Karena itu HotPDF menulis tag pembuka, teks yang di-escape, dan tag penutup setiap <value> dalam satu keping, membiarkan indentasi di luar elemen, dan mengenkode CR, LF, serta TAB sebagai character reference supaya parser XML yang menerapkan normalisasi akhir baris tak bisa mengubah byte aslinya
<!-- FDF: satu array bertipe per field -->
<< /T (options) /V [(Deep\nBlue) (Red)] >>
<< /T (region) /V [<45553132>] >>
<!-- XFDF: satu <value> per pilihan -->
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
<fields>
<field name="options">
<value>Deep
Blue</value>
<value>Red</value>
</field>
</fields>
</xfdf>
Dua kasus tepi ekspor layak diketahui sebelum Anda menulis kode pemanggilnya. Pertama, ExportLoadedFormToFDF membangun seluruh body FDF di memori sebelum membuat file targetnya (diperbaiki di 2.755.1), jadi nilai yang tak bisa diekspor, seperti array yang memuat sesuatu selain string, melempar exception tanpa memotong file yang sudah ada. Kedua, pilihan kosong pada list box yang juga menawarkan nilai ekspor string kosong itu ambigu di XFDF, karena <value/> bisa berarti tak ada yang dipilih atau opsi kosongnya yang dipilih. ExportLoadedFormToXFDF melempar exception dalam kasus itu alih-alih menebak, dan ia melemparnya sebelum file target dibuka. FDF tak punya ambiguitas semacam itu, karena /V [] dan /V [()] berbeda. Kedua eksporter FDF juga melewati terminal yang hanya widget tanpa nama /T, menyamai eksporter XFDF, karena tak ada importer yang bisa mencocokkan entri itu kembali ke sebuah field
var
Pdf: THotPDF;
Written: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
begin
// List box multi-select ditulis sebagai /V [(...) (...)]
Written := Pdf.ExportLoadedFormToFDF('order-form.fdf');
try
Pdf.ExportLoadedFormToXFDF('order-form.xfdf');
except
on E: Exception do
// Pilihan kosong plus opsi ekspor kosong: XFDF tak bisa
// membedakannya, dan file .xfdf yang ada dibiarkan tak tersentuh
ShowMessage('XFDF export refused: ' + E.Message);
end;
end;
finally
Pdf.Free;
end;
end;
Bagaimana HotPDF memvalidasi nilai multi-select saat impor?
HotPDF menerima array hasil impor hanya ketika targetnya adalah choice field dengan flag MultiSelect terpasang dan setiap nilai di array itu cocok dengan nilai ekspor di array /Opt milik field. Setiap slot opsi hanya bisa dipakai sekali, jadi list dengan dua opsi yang berbagi nilai ekspor b menerima [<62> <62>] sebagai dua pilihan berbeda dan menolak b ketiga. /I yang dibangun ulang mengikuti urutan /Opt dan bukan urutan nilai yang masuk, sebab §12.7.4.4 mensyaratkan indeks menaik. HotPDF membangun /V dan /I yang baru sebagai objek lepas dan meng-assign keduanya hanya setelah setiap nilai lolos validasi, jadi nilai yang ditolak tak pernah menyisakan setengah array atau indeks basi. Salinannya ditulis ke field yang sedang diimpor alih-alih ke array leluhur bersama, ejaan hex yang datang dari FDF tetap hex sampai penyimpanan, dan field yang kalkulasinya bergantung pada list box ditandai untuk rekalkulasi. Kalau Anda hanya perlu menyetel satu nilai, menyetel satu nilai field form di PDF termuat lewat jalur skalar, yang memang oleh desainnya tak menangani pilihan ganda
Sebagian tool lain menulis nilai ekspor ASCII polos sebagai hex string tanpa byte order mark, misalnya <416272>, lalu mengekspor XFDF dengan menuliskan digit-digit hex itu sebagai teks. Perbandingan literal yang ketat di jalan pulangnya gagal, dan impornya batal. Versi 2.755.1 menambah satu retry: ketika sebuah nilai tak cocok dengan opsi mana pun, HPDFHexSpellingText mendekode teksnya sebagai payload hex dan membandingkan hasilnya lagi. Retry hanya berlaku untuk input yang tanpa itu akan melempar exception, jadi ia tak pernah mengubah nilai yang sudah cocok. Rilis yang sama juga membuat jalur skalar dan array memakai decoder Unicode yang sama, yang memahami PDFDocEncoding, UTF-16 dengan byte order mark mana pun, dan UTF-8. Sebelum itu, satu nilai logis bisa cocok di satu jalur dan gagal di jalur lain pada dokumen yang mencampur encoding
Kenapa file FDF yang valid tetap bisa kehilangan field saat parsing?
Scanner FDF yang tak melacak string heksadesimal bisa membelah sebuah dictionary field menjadi dua ketika nilai hex-nya berakhir tepat di sebelah terminator dictionary. Di << /T (region) /V <416273>>>, > pertama menutup string hex-nya, tapi scanner yang naif membacanya bersama > berikutnya sebagai akhir dictionary dan diam-diam membuang field-nya. Importer FDF level file sudah melacak apakah ia sedang berada di dalam string hex, dan di 2.755.1 scanner array dan dictionary di balik ImportLoadedInterchangeFromFDF melakukan hal yang sama. Isu kedua menyangkut referensi tak langsung. File FDF adalah dokumen sintaks PDF kecil dengan penomoran objeknya sendiri (ISO 32000-1 §12.7.7), jadi nilai seperti /V [11 0 R] merujuk objek 11 milik file FDF, bukan objek 11 milik PDF yang sedang Anda isi. Parser FDF sederhana di HotPDF tidak meresolusi referensi di dalam file itu, jadi ia menolak array semacam itu alih-alih membaca objek 11 yang kebetulan ada di dokumen target
Impor file, stream, dan XFDF melaporkan error dengan cara berbeda
Ketiga jalur impor memvalidasi dengan cara yang sama tapi melaporkan kegagalan dengan cara berbeda, dan memilih satu secara sengaja itu layak. ImportLoadedFormFromFDF melewati setiap field yang gagal validasi dan mengembalikan jumlah field yang benar-benar diterapkannya, jadi angka yang lebih rendah dari harapan adalah satu-satunya tanda masalah. ImportLoadedInterchangeFromFDF dan ImportLoadedFormFromXFDF melempar exception pada field pertama yang ditolak. Setiap field di-commit secara sendiri-sendiri, jadi field yang terproses sebelum exception tetap memegang nilai barunya. Perlakukan tak satu pun dari ini sebagai transaksi atas seluruh file pertukaran: kalau Anda butuh perilaku semua-atau-tidak sama sekali, buang dokumen termuatnya ketika exception terjadi alih-alih menyimpannya
var
Pdf: THotPDF;
Source: TMemoryStream;
Status: AnsiString;
Info: THPDFFDFInterchangeInfo;
begin
Pdf := THotPDF.Create(nil);
Source := TMemoryStream.Create;
try
Source.LoadFromFile('order-form-reviewed.fdf');
if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
try
// Hanya field; nilai di luar /Opt atau target non-multi-select melempar exception
if Pdf.ImportLoadedInterchangeFromFDF(Source, True, False, Status, Info) then
Pdf.SaveLoadedDocument('order-form-filled.pdf');
except
on E: Exception do
ShowMessage('Import rejected, nothing saved: ' + E.Message);
end;
finally
Source.Free;
Pdf.Free;
end;
end;
Memperluas callback XFDF tanpa merusak pemanggil yang sudah ada
Dukungan array di unit XFDF tingkat lebih rendah tinggal di record terpisah, THPDFXFDFArrayAccess, dan di overload baru HPDFXFDFExportFields serta HPDFXFDFImportFields, bukan di field tambahan yang ditambahkan ke ujung record THPDFXFDFAccess yang sudah ada. Alasannya adalah kompatibilitas biner. Kode yang mengisi THPDFXFDFAccess sebagai variabel lokal sering hanya menyetel slot yang ia kenal dan tak pernah membersihkan sisanya, jadi pointer fungsi baru yang ditambahkan ke record itu akan berisi sampah stack, dan library akan menganggapnya callback sungguhan. Dengan record terpisah, pemanggil lama mempertahankan layout lama dan overload lama, dan overload itu meneruskan record array all-nil secara internal. Overload impor skalar yang asli tetap merekatkan nilai berulang dengan LF demi kompatibilitas, dan hanya overload yang sadar array yang memisahkannya. Ketika Anda mengikat data store milik sendiri, mulailah dari Default(THPDFXFDFArrayAccess). Kembalikan True dari GetFormFieldValueArray untuk setiap field bernilai list, termasuk yang tak memilih apa pun, dan False untuk mundur ke callback skalar
uses HPDFXFDF;
// Pointer fungsi polos, bukan "of object": Context membawa store Anda sendiri
function StoreGetSelections(Context: Pointer; FieldIndex: Integer;
out Values: THPDFXFDFValueArray): Boolean;
begin
Result := TFormStore(Context).IsListField(FieldIndex);
if Result then
Values := TFormStore(Context).Selections(FieldIndex);
end;
procedure ExportStore(Store: TFormStore; out Bytes: TBytes);
var
Access: THPDFXFDFAccess;
ArrayAccess: THPDFXFDFArrayAccess;
begin
Access := MakeStoreAccess(Store); // binding skalar yang sudah ada milik Anda
ArrayAccess := Default(THPDFXFDFArrayAccess); // setiap slot tak terpakai bernilai nil
ArrayAccess.GetFormFieldValueArray := StoreGetSelections;
HPDFXFDFExportFields(Access, ArrayAccess, Bytes);
end;
Pertukaran multi-select bekerja pada list box yang sudah ada dan menyetel bit MultiSelect di /Ff. Untuk bagaimana choice field dan bit flag-nya dibuat sejak awal, lihat menambahkan ListBox dan field AcroForm lain ke PDF termuat. Untuk markup komentar yang lewat tree <annots> milik XFDF, lihat impor dan ekspor anotasi XFDF di HotPDF. Referensi API lengkap dan unduhan trial ada di halaman HotPDF Delphi PDF component