Sebuah rectangle yang digambar di sekeliling paragraf saat review tidak harus menjadi tanda di dalam PDF. THPDFViewerModel milik HotPDF mengekspos AddHighlightRegion, sebuah metode yang menyimpan setiap highlight sebagai record di memori alih-alih perubahan pada dokumen yang dimuat, sehingga seorang reviewer bisa menandai puluhan halaman sementara file di disk tetap byte-demi-byte sama seperti sebelumnya. Zoom ke 6400%, putar halaman 90 derajat, beralih dari Fit Width ke Fit Page, dan rectangle yang sama tetap mendarat pada paragraf yang sama, karena perhitungan koordinatnya berjalan lewat geometri render aktual pada saat tanda tersebut digambar
Tooling review yang dibangun di sekitar sebuah PDF viewer terus-menerus menghadapi masalah ini. Layar redlining, langkah QA atas invoice yang dihasilkan, workflow persetujuan internal: semuanya membutuhkan cara membiarkan seseorang menarik perhatian ke sebuah region halaman tanpa setiap tanda draf berubah menjadi perubahan permanen pada file, dan tanpa harus menggunakan sistem anotasi lengkap hanya untuk menampilkan kotak berwarna sementara seseorang masih memutuskan apakah tanda itu relevan. HotPDF menjawabnya dengan sebuah highlight layer khusus yang sepenuhnya berada di sisi Model dari pemisahan yang dijelaskan di membangun PDF viewer kustom dengan arsitektur MVC di Delphi, yang juga menjadi alasan mengapa daftar highlight yang sama bisa dijalankan dari sebuah unit test tanpa satu pun window handle terlihat
Apa sebenarnya yang disimpan AddHighlightRegion milik HotPDF?
AddHighlightRegion menyimpan tepat tiga hal per tanda: sebuah indeks halaman berbasis nol, sebuah THPDFRectangle dalam koordinat user-space PDF, dan sebuah TColor, semuanya dikemas sebagai record THPDFViewerHighlight di dalam THPDFViewerModel. Memanggil Viewer.HighlightRegion(PageIndex, PageRect, clYellow), atau padanannya Model.AddHighlightRegion, menambahkan salah satu record ini ke sebuah array privat dan mengembalikan indeksnya, dan indeks itulah satu-satunya handle yang didapat pemanggil: tidak ada objek terpisah, tidak ada interface reference-counted, tidak ada apa pun yang perlu dibebaskan. Setiap kemampuan lain dalam artikel ini, menggambar tanda, memetakan ulang setelah perubahan zoom, menghapusnya, dibangun di atas satu record kecil itu
Setiap rectangle dinormalisasi dan dipotong sebelum diterima. AddHighlightRegion menukar tepi kiri dan kanan jika reviewer menyeret dari kanan ke kiri, menukar atas dan bawah untuk drag ke atas, lalu memotong hasilnya terhadap MediaBox halaman yang diambil lewat GetLoadedPageBox. Sebuah rectangle yang berakhir dengan lebar nol, tinggi nol, atau sepenuhnya di luar halaman langsung ditolak: metode ini mengembalikan -1 dan tidak ada yang ditambahkan ke daftar. Nilai kembali itu bukan sekadar formalitas: sebuah batch highlight yang dibangun ulang dari file review eksternal, atau dari koordinat basi setelah sebuah halaman diganti, bisa diam-diam kehilangan entri jika pemanggil tidak memeriksanya
Bagaimana sebuah highlight tetap sejajar setelah zoom atau rotasi?
Sebuah highlight tetap sejajar karena HotPDF menyimpannya dalam ruang halaman PDF dan memproyeksikannya ulang ke ruang layar pada setiap penggambaran ulang, alih-alih menyimpan rectangle layar yang akan basi begitu level zoom berubah. THPDFViewerModel.PagePointToView dan kebalikannya, ViewPointToPage, melakukan proyeksi itu dalam dua tahap: pertama entry /Rotate milik halaman itu sendiri, lalu ViewRotation independen milik Viewer, yang tidak pernah ditulis kembali ke PDF dan hanya memengaruhi apa yang ditampilkan Viewer. Membatalkan transformasi saat mouse dilepaskan menjalankan kedua tahap yang sama secara terbalik, dan itulah yang memungkinkan sebuah highlight yang digambar pada zoom tinggi di halaman yang diputar 270 derajat mendarat tepat di posisi yang benar setelah reviewer mengatur ulang tampilan kembali ke Fit Page
DPI yang digunakan untuk proyeksi itu sama pentingnya dengan rotasi. Viewer milik HotPDF menangkap DPI persis dari bitmap yang sedang tampil di layar dalam FRenderedDPI tepat setelah setiap render, dan ImageMouseUp meneruskan nilai yang sama itu ke ViewPointToPage sehingga koordinat mouse selalu dikonversi menggunakan resolusi yang benar-benar digunakan saat menggambar, bukan resolusi yang dihitung ulang dari properti zoom saat ini. CreatePageSnapshot dan kerabatnya membatasi DPI ke rentang 12 hingga 2400, tetapi jalur render interaktif tidak membawa batas atas semacam itu: tangga zoom standar mentok di 6400%, yang setara jauh di atas 2400 DPI pada baseline default 96 DPI, sehingga menggunakan kembali batas gaya snapshot untuk pemetaan koordinat akan menggeser setiap highlight beberapa piksel di puncak rentang zoom. Dua default lebih kecil melengkapi interaksi ini: sebuah drag yang lebih pendek dari dua piksel pada sumbu mana pun diperlakukan sebagai klik dan tidak menghasilkan highlight, dan highlighting tidak bisa dimulai sampai setidaknya satu halaman benar-benar dirender, karena FRenderedDPI dimulai dari nol
Menghubungkan highlighting interaktif ke layar review
Mengaktifkan highlighting interaktif adalah pekerjaan tiga properti pada kontrol THPDFViewer itu sendiri: atur InteractionMode ke vimHighlight alih-alih default vimBrowse, pilih sebuah HighlightColor, yang defaultnya clYellow, dan tangani OnMarqueeSelect untuk mengetahui apa yang baru saja digambar reviewer. Semua hal lainnya, menangkap mouse, menggambar rectangle seleksi putus-putus saat reviewer menyeret, mengonversi titik pelepasan kembali ke ruang halaman, memanggil AddHighlightRegion, terjadi di dalam kontrol sebelum event itu dipicu
type
TReviewForm = class(TForm)
Viewer: THPDFViewer;
ReviewLog: TMemo;
procedure FormCreate(Sender: TObject);
private
procedure ViewerMarqueeSelect(Sender: TObject; Shift: TShiftState;
PageIndex: Integer; const PageRect: THPDFRectangle;
HighlightIndex: Integer);
end;
// PdfDoc is a THotPDF already loaded elsewhere on the form
procedure TReviewForm.FormCreate(Sender: TObject);
begin
Viewer.PDFDocument := PdfDoc;
Viewer.InteractionMode := vimHighlight;
Viewer.HighlightColor := clLime;
Viewer.OnMarqueeSelect := ViewerMarqueeSelect;
end;
procedure TReviewForm.ViewerMarqueeSelect(Sender: TObject; Shift: TShiftState;
PageIndex: Integer; const PageRect: THPDFRectangle; HighlightIndex: Integer);
begin
ReviewLog.Lines.Add(Format('page %d, mark #%d at (%.1f, %.1f)-(%.1f, %.1f)',
[PageIndex + 1, HighlightIndex, PageRect.Left, PageRect.Bottom,
PageRect.Right, PageRect.Top]));
end;
OnMarqueeSelect hanya terpicu untuk drag yang benar-benar menghasilkan sebuah highlight: klik yang terlalu kecil untuk dihitung sebagai drag langsung menghapus overlay seleksi, dan drag yang mendarat sepenuhnya di luar halaman tetap sampai ke AddHighlightRegion tetapi ditolak di sana dengan cara yang sama seperti pemanggilan programatik, sehingga event ini tetap diam dalam kedua kasus. Satu detail implementasi yang layak diketahui jika highlighting pernah tampak berhenti merespons di tepi kontrol: penangkapan mouse dimiliki oleh THPDFViewer itu sendiri, sebuah turunan TScrollBox, bukan oleh TImage internal yang menampilkan bitmap halaman, dan itulah yang memungkinkan seorang reviewer menyeret melewati tepi halaman yang dirender dan tetap mendapatkan pelepasan yang bersih
Menambah, menghapus, dan membaca ulang highlight dari kode
Highlight tidak harus selalu berasal dari drag mouse. Viewer.HighlightRegion(PageIndex, PageRect, Color), yang menyalurkan ke Model.AddHighlightRegion yang sama yang dipanggil drag interaktif secara internal, bersifat publik secara khusus agar sebuah layar review bisa membangun ulang highlight dari data yang sudah dimilikinya: komentar yang dimuat dari database, hasil dari pencarian teks, atau tanda yang dipulihkan dari sesi sebelumnya. Karena koordinatnya adalah angka user-space PDF biasa, tidak ada apa pun dalam jalur ini yang bergantung pada sebuah halaman yang sudah dirender lebih dulu, berbeda dengan drag interaktif, yang membutuhkan FRenderedDPI yang sudah memegang nilai nyata
var
I: Integer;
Item: TPriorComment; // your own record: PageIndex + PageRect
NewIndex: Integer;
begin
for I := 0 to PriorComments.Count - 1 do
begin
Item := TPriorComment(PriorComments[I]);
NewIndex := Viewer.HighlightRegion(Item.PageIndex, Item.PageRect, clAqua);
if NewIndex < 0 then
LogWarning('comment %d fell outside the page and was dropped', [I]);
end;
end;
Menghapus satu highlight adalah tempat penyimpanan berbasis array ini terlihat jelas. RemoveHighlightRegion menghapus satu record dan menggeser setiap record setelahnya turun satu posisi untuk menutup celah, yang berarti indeks mana pun yang ditangkap sebelumnya, dari sebuah event OnMarqueeSelect atau dari enumerasi sebelumnya, tidak lagi bisa diandalkan begitu sesuatu yang lebih awal dalam daftar dihapus. OnHighlightChange terpicu pada setiap penambahan, penghapusan, dan pemanggilan ClearHighlightRegions, tetapi tidak membawa informasi apa pun tentang apa yang berubah, sehingga pola yang aman adalah memperlakukannya sebagai sinyal untuk membangun ulang daftar apa pun yang ditampilkan panel review dari HighlightCount dan TryGetHighlightRegion, alih-alih menambal indeks yang sudah di-cache di tempat
procedure TReviewForm.ViewerHighlightChange(Sender: TObject);
var
I: Integer;
Mark: THPDFViewerHighlight;
begin
MarkList.Items.Clear;
for I := 0 to Viewer.Model.HighlightCount - 1 do
if Viewer.Model.TryGetHighlightRegion(I, Mark) then
MarkList.Items.AddObject(Format('page %d', [Mark.PageIndex + 1]),
TObject(I));
end;
Kapan sebuah tanda seharusnya menjadi anotasi Highlight sungguhan?
Sebuah region highlight seharusnya menjadi anotasi sungguhan begitu ia perlu bertahan di luar satu instance THPDFViewer tersebut. HotPDF juga mengekspos AddHighlightAnnotation untuk halaman baru dan AddLoadedHighlightAnnotation untuk dokumen yang sudah dimuat, dan meski namanya hampir identik, ini adalah mekanisme yang sepenuhnya berbeda: keduanya menulis anotasi text-markup ISO 32000-1 §12.5.6.10 sesungguhnya, PDF /Subtype /Highlight, ke dalam array /Annots milik halaman, dengan /QuadPoints menandai deretan glyph yang tepat, dan viewer PDF apa pun yang sesuai standar akan merendernya begitu file disimpan, bukan hanya milik HotPDF sendiri. Batas mekanisme yang sama ini menentukan apakah sebuah tanda bisa round-trip lewat XFDF: sebuah anotasi yang dibuat dengan AddLoadedHighlightAnnotation adalah objek PDF normal yang diambil ExportLoadedAnnotationsToXFDF dan diserahkan ke Acrobat atau tool review lain sebagai markup ISO 19444-1, dibahas di mengimpor dan mengekspor anotasi PDF sebagai XFDF di Delphi, sementara sebuah region yang ditambahkan lewat AddHighlightRegion tidak terlihat oleh ekspor itu karena memang tidak pernah ditulis ke object graph sama sekali: ia hanya ada selama THPDFViewerModel yang membuatnya masih ada. Keluarga lengkap tipe anotasi markup dan geometrik yang tersedia pada sebuah halaman, dan bagaimana sebuah rectangle menempatkan masing-masing, dibahas di artikel tentang anotasi PDF di Delphi dengan HotPDF, dan aturan praktisnya sederhana: biarkan sebuah tanda sekali-pakai selama sebuah dokumen masih dibahas, dan komitkan menjadi anotasi begitu sebuah keputusan sudah final
Di mana highlight layer berhenti
Highlight layer, di sisinya, tidak mencoba terlihat seperti pena highlighter tembus pandang: RefreshDocument menggambar setiap region sebagai rectangle outline dua-piksel dengan warnanya sendiri di atas bitmap halaman yang di-cache, dengan cara yang sama seperti menggambar hasil pencarian, alih-alih mencampurkan fill berwarna di atas teks di bawahnya, sehingga tampilan sapuan kuning klasik harus dilukis dalam kode aplikasi atau ditunda hingga menjadi appearance stream milik sebuah anotasi yang dipromosikan. Satu kemampuan yang layak digunakan kembali begitu sebuah region sudah ada adalah CreateCurrentPageRegionSnapshot, yang mengambil THPDFRectangle yang sama yang sudah dibawa sebuah highlight dan merender hanya area itu ke sebuah bitmap, berguna untuk melampirkan gambar pratinjau kecil ke sebuah komentar review tanpa mengekspor seluruh halaman. Sebuah pembangunan review tidak harus memilih di antara kedua mekanisme di awal: jadikan default setiap tanda baru sebagai region THPDFViewerHighlight sekali-pakai selama sebuah thread komentar masih terbuka, dan hanya panggil AddLoadedHighlightAnnotation begitu seorang reviewer menyelesaikannya, yang menjaga PDF yang dimuat tetap tidak tersentuh selama bolak-balik yang menghasilkan perubahan paling banyak. Kontrol viewer yang dijelaskan di sini adalah bagian dari HotPDF Component standar untuk Delphi dan C++Builder, berdampingan dengan sisa API anotasi dan form yang dirujuk di atas