Artikel Teknis

Peninjauan Anotasi PDF Delphi dengan Komponen PDFium

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

Diagram yang menunjukkan bagaimana panel tinjauan PDFium Delphi memfilter larik anotasi halaman mentah berisi komentar, popup, balasan, dan tautan menjadi daftar komentar terkurasi yang dilihat peninjau
Array annotasi halaman mencampur komentar dengan popup, balasan, link, dan tanda tersembunyi, sehingga panel tinjauan butuh aturan penghitungan sebelum memperlihatkan total

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

Diagram jalur pewarnaan ulang baca-ubah-tulis dalam komponen PDFium Delphi di mana appearance stream yang dibakar memveto warna kamus dan memunculkan EPdfError
Ketika annotasi membawa stream /AP prakira jadi, engine menolak warna dictionary dan melempar EPdfError, sehingga panel mewarnai ulang overlay-nya sendiri atau menandai baris sebagai appearance-locked
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:

Diagram siklus hapus PDFium Delphi tiga langkah yang menghapus anotasi, merender ulang halaman dengan reAnnotations, dan membangun ulang indeks halaman
Penghapusan hanya menyentuh pohon annotasi, sehingga panel harus merender ulang dengan reAnnotations dan membangun ulang entri halaman sebelum tampilan dan indeks jujur kembali
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