Dalam PDFium Component, komponen VCL/LCL berbasis PDFium untuk Delphi, C++Builder, dan Lazarus, sebuah indeks form field bukanlah indeks annotation. Sebuah halaman membawa annotation Link, Text, dan Ink di samping widget-nya, sehingga enumerasi field harus memfilter berdasarkan FPDFAnnot_GetSubtype dan mengekspos sebuah indeks logis berbasis-nol, yang dipetakan kembali ke posisi annotation sesungguhnya hanya pada pemanggilan native
Bug yang mengungkap ini tidak bisa disalahartikan begitu Anda pernah melihatnya. Seorang tester menekan Tab dalam sebuah invoice form yang sudah terisi dan caret-nya lenyap, karena focus pindah ke sebuah hyperlink di footer. Atau lebih buruk lagi, tidak terjadi apa-apa sama sekali: kode Anda mencatat field 3 sebagai focused, panel UI-nya ter-update, dan FORM_SetFocusedAnnot diam-diam mengembalikan false selama itu. Kedua gejala berasal dari kesalahan desain yang sama, dan salah satunya punya akar masalah kedua yang bersembunyi di baliknya
Dua ruang indeks yang diberikan PDFium kepada Anda
PDFium mengekspos dua skema penomoran atas halaman yang sama, dan keduanya hanya bertepatan pada dokumen yang kebetulan tidak mengandung apa pun selain widget form. Yang pertama adalah indeks annotation: sebuah posisi dalam array /Annots halaman, yaitu yang dihitung FPDFPage_GetAnnotCount dan yang diambil FPDFPage_GetAnnot (ISO 32000-1 §12.5.2). Yang kedua adalah indeks field logis yang seharusnya ditawarkan sebuah API level-aplikasi, berjalan dari nol atas field interaktif yang sungguh-sungguh bisa dijangkau pengguna. ISO 32000-1 §12.5.6.19 mendefinisikan widget annotation sebagai representasi visual dari field form interaktif, dan §12.7 mendefinisikan form itu sendiri. Segala sesuatu yang lain pada halaman adalah subtype berbeda dengan semantik berbeda: sebuah Link annotation punya sebuah destination, sebuah Ink annotation punya sebuah daftar stroke, sebuah Text annotation adalah sebuah sticky note. Tidak satu pun dari mereka termasuk dalam hitungan field, dan tidak satu pun dari mereka bisa menerima focus form. Namun dalam array /Annots mereka duduk berselang-seling dengan widget dalam urutan apa pun yang ditulis aplikasi pembuatnya, yang seringkali bukan urutan yang disarankan hal lain apa pun soal dokumen tersebut
Kenapa Tab mendarat pada sebuah hyperlink alih-alih field berikutnya?
Karena hitungan field-nya sebenarnya adalah hitungan annotation. Implementasi aslinya mengembalikan FPDFPage_GetAnnotCount langsung dari FormFieldCount, sementara accessor informasi field, helper tab order, dan helper focus semuanya memperlakukan integer yang sama itu sebagai posisi widget. Pada sebuah halaman AcroForm yang bersih dengan enam widget dan tidak ada yang lain, enam sama dengan enam dan setiap test lolos. Tambahkan sebuah hyperlink di footer dan sebuah komentar reviewer di margin, dan hitungannya melaporkan delapan field, indeks 6 dan 7 meresolusi ke objek non-form, dan Tab berjalan langsung ke sana
Perbaikan di sisi enumerasi adalah menghitung subtype, bukan annotation. Buka setiap annotation, tanyakan subtype-nya, pertahankan widget-nya, dan tutup handle-nya dalam sebuah blok finally, karena FPDFPage_GetAnnot mengembalikan sebuah handle yang dimiliki dan harus dikembalikan lewat FPDFPage_CloseAnnot
function WidgetCountForPage(Page: FPDF_PAGE): Integer;
var
Count, I: Integer;
Annot: FPDF_ANNOTATION;
begin
Result := 0;
if Page = nil then
Exit;
Count := FPDFPage_GetAnnotCount(Page); // every annotation, not just fields
for I := 0 to Count - 1 do
begin
Annot := FPDFPage_GetAnnot(Page, I);
if Annot = nil then
Continue;
try
if FPDFAnnot_GetSubtype(Annot) = FPDF_ANNOT_WIDGET then
Inc(Result);
finally
FPDFPage_CloseAnnot(Annot);
end;
end;
end;
Perhatikan apa yang sengaja tidak dilakukan ini. Ia tidak bertanya apa pun ke form-fill environment, dan ia tidak membutuhkan form handle, karena subtype-nya berada dalam annotation dictionary dan bisa dibaca dari halaman saja. Itu penting untuk pengurutan: hitungannya sudah tersedia sebelum Anda memutuskan apakah dokumen ini bahkan layak mendapat form-fill environment, yang dibahas artikel tentang JavaScript AcroForm dan host event sebagai sebuah keputusan keamanan, bukan sekadar kenyamanan
Memetakan kembali indeks logis pada batas native
Aturan yang mencegah kedua ruang itu saling bocor sederhana: indeks logis adalah satu-satunya angka yang melintasi API publik Anda, dan ia dikonversi menjadi indeks annotation dalam fungsi terakhir sebelum pemanggilan native. Satu helper pemetaan, yang dipakai oleh info field, focus, setter flag, dan tab order secara sama, adalah yang membuat aturan itu bisa ditegakkan
function AnnotationIndexForField(Page: FPDF_PAGE;
FieldIndex: Integer): Integer;
var
Count, I, Current: Integer;
Annot: FPDF_ANNOTATION;
begin
Result := -1;
if (Page = nil) or (FieldIndex < 0) then
Exit;
Count := FPDFPage_GetAnnotCount(Page);
Current := 0;
for I := 0 to Count - 1 do
begin
Annot := FPDFPage_GetAnnot(Page, I);
if Annot = nil then
Continue;
try
if FPDFAnnot_GetSubtype(Annot) = FPDF_ANNOT_WIDGET then
begin
if Current = FieldIndex then
Exit(I); // real /Annots position: native calls only
Inc(Current);
end;
finally
FPDFPage_CloseAnnot(Annot);
end;
end;
end;
Ada dua properti dari helper ini yang layak dinyatakan dengan jelas. Ia adalah sebuah linear scan, sehingga sebuah loop naif atas setiap field memakan biaya sejumlah kuadratik pembukaan annotation pada sebuah halaman dengan ratusan widget; bila Anda mengenumerasi seluruh halaman, telusuri annotation-nya sekali dan kumpulkan handle widget-nya sambil berjalan, alih-alih memanggil mapper per field. Dan ia mengembalikan -1 alih-alih memunculkan exception, yang membiarkan caller memutuskan apakah sebuah indeks basi adalah sebuah error pemrograman yang layak mendapat exception atau sebuah race yang layak diabaikan, misalnya setelah sebuah edit menghapus sebuah annotation yang masih dirujuk sebuah daftar UI yang di-cache
Kenapa FORM_SetFocusedAnnot gagal pada halaman headless?
Karena PDFium menolak untuk mem-focus sebuah widget yang page view-nya tidak pernah ditandai valid. FORM_SetFocusedAnnot meresolusi annotation-nya ke sebuah page view di dalam form-fill environment, dan bila page view itu tidak ada, ia mengembalikan false tanpa diagnostik apa pun. Memperbaiki pemetaan indeks saja karena itu memperbaiki Tab yang mendarat pada sebuah hyperlink tapi meninggalkan gejala kedua tidak tersentuh: record focus logis Anda menyatakan field 3, widget focused native-nya masih tidak ada, dan setiap accessor yang dibangun di atas native focus, focused text, focused value, choice selection state, tetap mengembalikan kosong. Page view-nya dibuat oleh FORM_OnAfterLoadPage dan dihancurkan oleh FORM_OnBeforeClosePage. Dalam sebuah viewer yang dibangun di sekeliling sebuah visual control, pemanggilan itu terjadi sebagai bagian dari menampilkan sebuah halaman, dan itulah sebabnya kegagalannya begitu sering terlihat seperti bug khusus-headless: kode yang sama yang bekerja di demo GUI gagal di tool batch. Siklus hidupnya milik objek dokumen, bukan viewer-nya, sehingga PDFium Component kini menerbitkan kedua pemanggilan itu setiap kali sebuah halaman dimuat atau dibongkar dengan sebuah form handle hadir. Signature C-nya mengambil halaman lebih dulu dan form handle kedua, yang mudah terbalik ketika menulis binding-nya secara manual
procedure ReportFirstField(const FileName: string);
var
Pdf: TPdf;
Idx: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FormFill := True; // form-fill environment, before Active
Pdf.FileName := FileName;
Pdf.Active := True;
Pdf.PageNumber := 1; // page load also runs FORM_OnAfterLoadPage
Idx := Pdf.FocusNextFormField; // logical index, 0-based over widgets
if Idx < 0 then
Exit; // page holds no widget annotations
Writeln(string(Pdf.FormFieldInfo[Idx].Name), ' = ',
string(Pdf.FocusedFormFieldValue)); // reads the native focused widget
finally
Pdf.Free; // page unload runs FORM_OnBeforeClosePage
end;
end;
Pemeriksaan yang membuktikan perbaikan ini adalah yang membandingkan kedua sisinya. Panggil FocusFormField dengan sebuah indeks logis, lalu baca sebuah nilai lewat sebuah accessor yang melalui widget focused native, bukan lewat record Anda sendiri, seperti FocusedFormFieldValue atau FocusedFormOptionSelected. Jika indeks logisnya round-trip tapi accessor native-nya kembali kosong, yang hilang adalah page view-nya, bukan pemetaannya
Apa yang tidak dijanjikan indeks field logis
Sebuah indeks field berbasis-nol adalah sebuah kenyamanan, bukan sebuah identitas semantik, dan empat batasan mengikuti dari itu. Ia bersifat per halaman, bukan per dokumen, sehingga indeks 0 pada halaman 2 adalah widget yang berbeda dari indeks 0 pada halaman 1 dan membandingkan keduanya tidak bermakna. Ia bersifat posisional, sehingga menyisipkan atau menghapus sebuah annotation membatalkan setiap indeks yang di-cache di atas perubahan itu; perlakukan sebuah indeks yang tersimpan sebagai valid hanya selama halaman itu tetap dimuat dan tidak diedit
Batasan ketiga adalah yang mengejutkan orang yang meninjau sebuah daftar field. Indeksnya mengenumerasi widget, bukan field. Sebuah radio group adalah satu field dengan beberapa widget kid, sehingga sebuah grup tiga-tombol menyumbangkan tiga indeks berurutan yang semuanya melaporkan Name yang sama. Record TPdfFormFieldInfo membawa GroupCount dan GroupIndex persis untuk kasus ini, dan sebuah UI daftar yang mengabaikan keduanya menampilkan field yang sama tiga kali. Batasan keempat menyangkut urutan traversal: tab order yang diekspos di sini adalah urutan enumerasi widget, yang mengikuti array /Annots, bukan entri /Tabs halaman (ISO 32000-1 §7.7.3.3) dan bukan field tree AcroForm. Untuk kebanyakan produsen, keduanya sepakat; untuk sebuah form yang di-layout dalam dua kolom oleh sebuah generator yang menerbitkan kolom kanan lebih dulu, keduanya tidak sepakat, dan jalur keyboard yang dijelaskan di artikel navigasi form field akan terasa salah meski setiap indeksnya benar. Ketika sebuah berkas pelanggan berperilaku aneh, dump kedua ruang indeks itu berdampingan sebelum berteori: tampilan annotation dan tampilan field dari halaman yang sama, dicetak bersama, biasanya membuat penyebabnya jelas dalam sekali lihat
procedure DumpIndexSpaces(Pdf: TPdf);
var
I: Integer;
Info: TPdfFormFieldInfo;
begin
for I := 0 to Pdf.AnnotationCount - 1 do
Writeln('annot ', I, ': subtype ', Ord(Pdf.Annotation[I].Subtype));
for I := 0 to Pdf.FormFieldCount - 1 do
begin
Info := Pdf.FormFieldInfo[I];
Writeln('field ', I, ': ', string(Info.Name),
' widget ', Info.GroupIndex, ' of ', Info.GroupCount);
end;
end;
Sebuah hitungan annotation yang jauh di atas hitungan field berarti halamannya mencampur subtype, yang normal dalam dokumen yang sudah direview dan persis merupakan situasi tempat pemetaan ini ada; artikel workflow review annotation melihat halaman yang sama dari sisi markup. Hitungan yang sama pada setiap berkas test, di sisi lain, berarti fixture Anda sama sekali tidak bisa mendeteksi kelas bug ini, dan respons yang jujur adalah menambahkan sebuah fixture form yang membawa sebuah link dan sebuah sticky note
API enumerasi field, focus, dan annotation yang dijelaskan di sini hadir dengan PDFium Component untuk Delphi, C++Builder, dan Lazarus, yang halaman produknya memuat referensi form-field lengkap termasuk record informasi field dan accessor focus