Anotasi PDF adalah dictionary yang dilekatkan pada suatu halaman, bukan tanda yang digambar di atasnya. ISO 32000-1 §12.5 mendefinisikan kurang lebih dua lusin subtipe, dan masing-masing membawa /Subtype, sebuah rectangle dalam koordinat halaman, sekumpulan flag, dan biasanya sebuah appearance stream yang menentukan apa yang sebenarnya digambar oleh viewer. Subtipe-subtipe ini tidak semuanya berarti sama bagi orang yang meninjau dokumen. Highlight dan Ink stroke adalah komentar; Link adalah navigasi; Popup adalah jendela kecil yang terbuka saat Anda mengklik catatan tempel, disimpan sebagai objeknya sendiri dan ditunjuk oleh induknya. Replies adalah anotasi Text penuh yang merujuk pada komentar yang mereka jawab melalui entri in-reply-to. Jadi array anotasi pada level halaman bukanlah daftar komentar milik peninjau. Ia adalah kantong datar yang berisi komentar, mekanisme penghubung antar-komentar, dan beberapa hal yang sama sekali tidak akan disebut komentar oleh peninjau mana pun. Panel yang memperlakukan array ini sebagai daftar komentar akan menghasilkan angka yang berbeda dari setiap viewer lain yang dijalankan pelanggan
Membangun alur kerja peninjauan anotasi di atas PDFium Component, komponen VCL/LCL berbasis PDFium untuk Delphi, C++Builder, dan Lazarus, berarti memusatkan perhatian pada titik-titik tempat kesenjangan antara array mentah dan tampilan yang dipahami manusia menimbulkan masalah: menghitung, mengindeks, mengubah warna tanda yang appearance-nya sudah dibekukan oleh engine, menghapus tanpa meninggalkan jejak hantu, dan menambahkan tanda buatan sendiri
Mengapa jumlah hitungan Anda tidak pernah cocok dengan panel komentar Acrobat
Buka sebuah kontrak yang telah diberi markup di viewer Anda dan di Acrobat secara berdampingan, dan totalnya jarang sekali cocok. Acrobat menampilkan tampilan yang telah dikurasi: markup dikelompokkan ke dalam thread balasan, popup dilipat ke dalam catatan tempat mereka berada, sementara link dan widget form tidak diikutsertakan. Array mentah menyimpan semuanya tanpa dibedakan, sehingga penghitungan yang naif menjadi terlalu tinggi pada satu sisi dan terlalu rendah pada sisi lain secara bersamaan
Popup menggelembungkan total, karena setiap catatan tempel disertai objek Popup terpisah, dan menghitung keduanya berarti menghitung ganda catatan yang sama. Replies justru mengempiskan total jika Anda menyaring berdasarkan tanda yang terlihat, karena sebuah reply adalah anotasi Text yang tidak menggambarkan apa pun sampai seseorang membuka thread-nya, dan membuangnya berarti kehilangan diskusi tersebut. Flag Hidden dan NoView menyembunyikan sebuah anotasi dari layar tanpa mengeluarkannya dari array, sehingga penghitungan yang mengabaikan flag akan turut menyertakan tanda yang tidak dapat dilihat pengguna. Anotasi Link berada dalam array yang sama dengan komentar, padahal ia tidak termasuk dalam hitungan maupun daftar. Tentukan aturan penghitungan sebelum Anda menulis loop-nya, dan catat keputusan itu, karena "why does your panel show a different number than Acrobat" adalah tiket pertama yang akan diterima oleh fitur peninjauan ini
Indeks semuanya sekali saja, lalu jangan pernah mengurai ulang halaman
Satu aturan desain mengendalikan semua yang mengikutinya: menyaring berdasarkan penulis, tipe, atau halaman tidak boleh pernah mengurai ulang objek halaman. Pada dokumen 300 halaman dengan markup yang padat, mengurai ulang setiap kali dropdown berubah membuat panel tersendat selama beberapa detik setiap kalinya. Komponen ini mengekspos AnnotationCount dan properti Annotation[] yang terindeks, keduanya berlingkup pada halaman yang sedang dimuat, dan record TPdfAnnotation yang dikembalikannya membawa apa yang dibutuhkan sebuah tampilan daftar: Subtype, Flags, Color, Rectangle, ContentsText, AuthorText. Langkah yang tepat adalah menyapu setiap halaman satu kali saat dokumen dibuka dan menyimpan indeks datar milik Anda sendiri:
procedure TReviewPanel.BuildIndex;
var
PageNo, i: Integer;
A: TPdfAnnotation;
begin
FItems.Clear;
for PageNo := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := PageNo;
for i := 0 to Pdf.AnnotationCount - 1 do
begin
A := Pdf.Annotation[i];
// Simpan hanya subtipe yang relevan bagi peninjau; catat pasangan
// halaman dan indeks karena semua edit berikutnya dialamatkan melaluinya
if A.Subtype in [anText, anHighlight, anInk] then
FItems.Add(TReviewItem.Create(PageNo, i,
A.AuthorText, A.ContentsText, A.Rectangle, A.Color));
end;
end;
end;
Pasangan yang layak digarisbawahi adalah (PageNo, i). Setiap mutasi berikutnya, baik itu mengubah warna atau menghapus, dialamatkan melalui nomor halaman ditambah indeks anotasi, dan indeks ini rapuh: menghapus sebuah anotasi akan menomori ulang semua yang ada setelahnya pada halaman itu. Karena itu, rencanakan untuk membangun ulang entri halaman yang terdampak setelah setiap penghapusan, alih-alih menambal nomor indeks di tempat. Membangun ulang hanya memakan waktu semilidetik. Indeks yang basi, sebaliknya, akan menghapus komentar milik peninjau yang salah, yang merupakan jenis bug yang mengikis kepercayaan terhadap seluruh fitur ini
Threading layak mendapat tempat dalam indeks bahkan jika rilis pertama Anda hanya menghitung reply alih-alih menampilkannya. Kelompokkan item berdasarkan referensi induknya selagi halaman masih terbuka, sehingga panel nantinya dapat melipat sebuah thread seperti yang dilakukan Acrobat. Merekonstruksi pengelompokan itu secara lazy saat scrolling meniadakan seluruh maksud dari mengindeks sekali saja, karena itu berarti membuka ulang halaman yang sudah Anda bayar biayanya untuk diurai. Geometri menuntut disiplin yang sama. Rectangle pada setiap record berada dalam page-space, dan mengonversinya ke koordinat tampilan harus dilakukan dalam satu helper bersama, bukan tersebar di seluruh kode. Panel akan menumbuhkan bug koordinat ketika seleksi, hit-testing, dan penggambaran masing-masing menciptakan perhitungan zoom dan rotasi sendiri-sendiri; alirkan ketiganya melalui satu konversi tunggal agar sebuah highlight, barisnya dalam daftar, dan target kliknya tetap tertaut pada tinta yang sama
Mengubah warna markup dan hak veto appearance stream
Mengubah sebuah highlight dari kuning menjadi amber kedengarannya seperti perubahan satu baris kode, dan kadang memang begitu. Ganjalannya ada di ISO 32000-1 §12.5.5. Ketika sebuah anotasi membawa appearance stream /AP, viewer yang sesuai standar akan menggambar stream yang sudah dibangun sebelumnya itu dan memperlakukan entri warna dalam dictionary sebagai metadata mati. Acrobat menulis appearance stream untuk hampir semua hal yang dibuatnya, sehingga sebagian besar anotasi yang datang dari pelanggan sudah berada dalam kondisi ini, dan warna yang Anda tetapkan dengan penuh percaya diri itu tidak pernah sampai ke layar. Mengubah warna adalah operasi read-modify-write melalui properti Annotation[], dan komponen ini jujur soal konflik tersebut: ketika engine menolak membiarkan warna dictionary menimpa appearance yang sudah dipanggang di dalamnya, penulisan tersebut memicu EPdfError
A := Pdf.Annotation[Item.Index];
A.HasColor := True;
A.Color := $0000B0FF; // amber
A.ColorAlpha := 160;
try
Pdf.Annotation[Item.Index] := A;
except
on EPdfError do
begin
// Anotasi ini memiliki stream /AP yang sudah dirender sebelumnya; warna
// dictionary saja tidak dapat mengubah apa yang digambar oleh viewer
Item.AppearanceLocked := True;
StatusBar.SimpleText := 'Color is fixed by the annotation appearance';
end;
end;
Tangkap exception tersebut setiap saat, dan perlakukan itu sebagai informasi, bukan kegagalan. Lewati pengaman ini dan panel Anda akan dengan riang menampilkan amber dalam daftarnya sendiri sementara halaman tetap menggambar warna kuning; pengguna melaporkannya berminggu-minggu kemudian sebagai "your viewer ignores my edits," dan Anda menghabiskan waktu satu sore gagal mereproduksinya pada sebuah file yang kebetulan tidak memiliki appearance stream. Begitu Anda tahu appearance-nya terkunci, Anda memiliki dua respons yang jujur: ubah warna overlay seleksi Anda sendiri, bukan anotasinya, sehingga peninjau setidaknya melihat highlight yang mereka pilih, atau tandai baris tersebut sebagai appearance-locked agar tidak ada yang mengharapkan perubahan itu akan bertahan
Menghapus anotasi tanpa meninggalkan jejak hantu
DeleteAnnotation menghapus objek dari annotation tree halaman yang aktif, tetapi ia membiarkan raster halaman yang di-cache tetap apa adanya. Gambar ulang segera setelah pemanggilan itu, dan highlight yang sudah dihapus masih tampak di layar, berdiam dalam sebuah bitmap yang sudah tidak lagi cocok dengan model dokumen di baliknya. Perbaikannya adalah memperlakukan render ulang sebagai bagian dari penghapusan itu sendiri, bukan sebagai langkah yang mungkin terlupa oleh si pemanggil:
Pdf.PageNumber := Item.PageNo;
Pdf.DeleteAnnotation(Item.Index); // memicu EPdfError jika gagal
Bmp := Pdf.RenderPage(0, 0, ViewWidth, ViewHeight, ro0, [reAnnotations]);
try
PaintPageBitmap(Bmp);
finally
Bmp.Free; // RenderPage menyerahkan kepemilikan bitmap kepada si pemanggil
end;
RebuildPageEntries(Item.PageNo); // indeks setelah Item.Index bergeser
Dua detail dalam blok itu mudah sekali salah. Opsi reAnnotations harus ada, kalau tidak raster baru akan membuang semua anotasi yang tersisa dan halaman akan terlihat seolah-olah Anda menghapus seluruh set komentar, bukan hanya satu tanda. Dan Bmp.Free bukanlah sesuatu yang opsional: overload RenderPage bergaya fungsi menyerahkan kepemilikan bitmap kepada si pemanggil, sehingga free yang terlewat akan membocorkan raster satu halaman penuh pada setiap penghapusan, yang bagi seorang peninjau yang mengerjakan dokumen panjang akan berubah menjadi tekanan memori nyata hanya dalam hitungan menit
Menambahkan tanda peninjau dari UI Anda sendiri
Membuat anotasi dilakukan melalui CreateAnnotation, yang menerima record TPdfAnnotation yang sudah diisi (subtype, rectangle, color, contents, author) dan melampirkannya ke halaman yang sedang aktif. Sebuah catatan tempel, dengan subtype anText, adalah kasus yang mudah: atur posisi, isi, dan penulisnya, lalu selesai. Anotasi Ink adalah tempat orang biasanya terjebak. Rectangle pada record hanya membatasi area gambar; goresan-goresannya sendiri berupa array titik yang harus dilampirkan secara terpisah melalui panggilan ink-stroke milik engine, FPDFAnnot_AddInkStroke, yang diberi data FS_POINTF yang diambil dari input mouse atau pen satu goresan pada satu waktu. Bangun sebuah anotasi Ink hanya dari rectangle tanpa apa pun yang lain, dan Anda akan mendapatkan coretan kosong yang dirender sebagai ruang hampa, yang terlihat seperti bug pada engine padahal sebenarnya adalah anotasi yang belum selesai dibuat
Tetapkan kebijakan kepenulisan pada saat yang sama. Setiap tanda yang dibuat UI Anda harus membawa AuthorText yang konsisten, karena filter peninjau yang Anda bangun bulan depan hanya akan sebaik nama-nama yang Anda cap pada komentar hari ini. String penulis yang kosong atau tidak konsisten tidak dapat diperbaiki secara retroaktif tanpa membuka ulang setiap file
Mengeluarkan hasil peninjauan dari viewer
Data peninjauan baru terasa berharga begitu ia dapat keluar dari viewer, entah sebagai ringkasan yang dibaca pemimpin proyek tanpa membuka file, atau sebagai CSV yang memasok lembar pelacakan. Ekspor dari indeks yang sudah Anda bangun, jangan pernah dari penguraian baru, dan pilih cara yang stabil untuk merujuk kembali ke setiap tanda. Nomor halaman yang dipasangkan dengan rectangle anotasi bertahan melalui round-trip yang tidak dapat dilalui oleh indeks array, karena penghapusan berikutnya diam-diam menomori ulang indeks dan CSV Anda mulai menunjuk ke komentar yang salah
Sebuah baris yang layak disimpan membawa halaman, subtype, penulis, timestamp pembuatan ketika file mencatatnya, teks isi, dan kolom status yang Anda miliki sendiri, bukan yang disediakan oleh PDF. Proses pengindeksan yang sama juga berguna lebih awal, saat intake, ketika sebuah dokumen datang dari luar tim dan Anda ingin tahu apa isinya sebelum ada yang meninjaunya. Artikel PDF intake workbench membahas triase tersebut secara mendalam, dan navigasi form-field membahas masalah bayangan cerminnya: meninjau dokumen yang dibangun untuk mengumpulkan data, bukan komentar
Satu kasus yang tidak akan diperlihatkan oleh array
Satu mode kegagalan layak diberi tanda karena ia terlihat seperti cacat pada kode Anda, padahal bukan. Seorang pelanggan melaporkan highlight yang terlihat memenuhi sebuah halaman, tetapi panel Anda tidak mendaftarkan apa pun, dan AnnotationCount kembali dengan nilai nol. Penjelasan yang biasa terjadi adalah bahwa tanda-tanda tersebut telah di-flatten di suatu tempat hulu prosesnya. Flattening memanggang appearance anotasi ke dalam konten halaman biasa, sehingga highlight tersebut menjadi bagian dari grafik halaman dan sepenuhnya berhenti eksis sebagai objek anotasi. Tidak ada lagi yang tersisa bagi sebuah annotation API untuk dienumerasi, diubah warnanya, atau dihapus. Ketika Anda melihat markup yang tergambar dengan hitungan nol, berhentilah mencari bug pada loop enumerasi Anda dan tanyakan bagaimana file itu dihasilkan
Permukaan anotasi yang digunakan di sini, mulai dari enumerasi dan pembuatan hingga pengubahan warna, penghapusan, dan opsi render yang menjaga tampilan tetap jujur, disertakan dalam PDFium Component untuk Delphi, C++Builder, dan Lazarus/FPC