Kelas THPDFBackgroundRenderer milik HotPDF adalah turunan TThread yang merender halaman PDF yang dimuat menjadi bitmap pada worker thread, sehingga viewer Delphi tetap bisa scroll dan menggambar ulang selagi sebuah halaman masih dirasterisasi di latar belakang. THPDFBackgroundRenderer.RequestPage mengantrekan sebuah indeks halaman untuk worker thread itu, CancelAll membuang apa pun yang masih menunggu, dan GetCachedBitmap menyerahkan kembali sebuah bitmap yang sudah selesai yang dimiliki pemanggil dan harus dibebaskan. Scroll sebuah kontrak hasil scan dua ratus halaman pada resolusi cetak hanya di UI thread saja dan setiap pergantian halaman akan menjeda window sampai GDI selesai menggambarnya, dan jeda itulah persis yang ingin dihilangkan THPDFBackgroundRenderer
Mengapa perlu merender halaman PDF pada thread latar belakang sama sekali?
Thread latar belakang layak dengan kompleksitasnya karena page renderer milik HotPDF adalah interpreter content-stream sungguhan, bukan sekadar penyalinan bitmap murah yang kembali sebelum ada yang menyadarinya: ia menelusuri operator PDF, memegang tumpukan graphics-state, dan merasterisasi path, gambar, dan glyph lewat GDI, mesin yang sama yang dibahas di merender halaman PDF yang dimuat ke TBitmap. Jalankan pekerjaan itu secara sinkron di dalam handler scroll atau paint dan message loop berhenti memompa sampai pemanggilan itu kembali, dan itulah sebenarnya yang terjadi pada window yang membeku. Menyisipkan Application.ProcessMessages di dalam pemanggilan render tidak memperbaiki ini: itu memang membuat antrean pesan terkuras, tetapi render itu sendiri tetap memegang thread pemanggil, sehingga window menggambar ulang konten basi lebih cepat sementara pekerjaan sesungguhnya sama sekali belum bergerak. Satu-satunya cara menjaga viewer tetap responsif selama render yang benar-benar lambat adalah menjalankan render itu di tempat lain, dan itulah sebabnya THPDFBackgroundRenderer ada sebagai subclass TThread alih-alih sebuah callback atau timer
Menyiapkan antrean permintaan untuk viewer yang bisa di-scroll
THPDFBackgroundRenderer.Create menerima instance THotPDF yang sudah dimuat dan sebuah DPI yang tetap konstan sepanjang umur renderer itu, sehingga setiap halaman yang diantrekan lewat satu instance dirender pada satu resolusi; sebuah viewer yang mendukung zoom membutuhkan renderer baru, bukan properti DPI baru, setiap kali level zoom berubah. RequestPage menambahkan sebuah indeks halaman ke antrean internal dan langsung kembali: metode ini sendiri tidak melakukan rendering apa pun dan tidak pernah menyentuh UI thread. Execute, titik masuk TThread yang diwarisi dan dijalankan HotPDF begitu Anda memanggil Start, mengambil satu indeks dari depan antrean itu satu per satu, merendernya lewat cache halaman milik dokumen, dan menyimpan salinannya diindeks berdasarkan halaman sehingga GetCachedBitmap bisa menyerahkannya kembali belakangan
type
TViewerForm = class(TForm)
RenderPollTimer: TTimer;
procedure RenderPollTimerTimer(Sender: TObject);
private
FDoc: THotPDF;
FRenderer: THPDFBackgroundRenderer;
FPendingPage: Integer;
procedure RequestPageWindow(CenterPage: Integer);
end;
procedure TViewerForm.RequestPageWindow(CenterPage: Integer);
var
I: Integer;
begin
if FRenderer <> nil then
begin
FRenderer.CancelAll;
FRenderer.Free;
end;
FRenderer := THPDFBackgroundRenderer.Create(FDoc, 150);
for I := CenterPage - 1 to CenterPage + 1 do
if (I >= 0) and (I < FDoc.LoadedPageCount) then
FRenderer.RequestPage(I);
FPendingPage := CenterPage;
FRenderer.Start;
end;
procedure TViewerForm.RenderPollTimerTimer(Sender: TObject);
var
Bmp: TBitmap;
begin
if FRenderer = nil then Exit;
Bmp := FRenderer.GetCachedBitmap(FPendingPage);
if Bmp <> nil then
begin
PageImage.Picture.Bitmap.Assign(Bmp);
Bmp.Free;
end;
end;
GetCachedBitmap mengembalikan nil sampai salinan halaman itu siap, sehingga pola poll-lewat-timer seperti di atas sudah cukup; tidak ada event ready terpisah yang perlu dihubungkan, HotPDF menyelesaikan ini dengan pemeriksaan nil biasa alih-alih API notifikasi yang lebih besar. Bagian berikutnya membahas apa yang sebenarnya dilakukan CancelAll dan pemanggilan Free itu, karena keduanya penting begitu halaman mulai dirender di luar urutan atau scroll terjadi lebih cepat dari yang bisa dikuras antrean
Jalan pintas satu-pemanggilan untuk satu halaman
THotPDF.RenderLoadedPageToBitmapAsync ada untuk kasus umum menembakkan tepat satu halaman tanpa menyentuh THPDFBackgroundRenderer secara langsung: metode ini membangun renderer secara internal, memanggil RequestPage sekali, memulai thread, dan mengembalikan referensi TThread ke pemanggil, yang memilikinya dan bertanggung jawab membebaskannya. Mengambil hasilnya melalui THotPDF.GetLoadedCachedRenderedBitmap alih-alih GetCachedBitmap milik renderer sendiri, karena GetLoadedCachedRenderedBitmap membaca cache bersama milik dokumen yang dikunci berdasarkan indeks halaman dan DPI, cache yang sama yang sudah diisi RenderLoadedPageToBitmapCached dan prefetcher bawaan — sebuah halaman yang sudah pernah dirender bagian lain viewer pada DPI itu bisa langsung kembali, bahkan sebelum thread latar belakang yang baru saja dimulai sempat dijadwalkan OS
// A simpler alternative to the queue above, for one page at a time.
procedure TViewerForm.RequestSinglePage(PageIndex: Integer);
begin
if FAsyncWorker <> nil then
FAsyncWorker.Free; // waits if a prior page is still rendering
FAsyncWorker := Pdf.RenderLoadedPageToBitmapAsync(PageIndex, 150);
FPendingPage := PageIndex;
end;
procedure TViewerForm.AsyncPollTimerTimer(Sender: TObject);
var
Bmp: TBitmap;
begin
Bmp := Pdf.GetLoadedCachedRenderedBitmap(FPendingPage, 150);
if Bmp <> nil then
begin
PageImage.Picture.Bitmap.Assign(Bmp);
Bmp.Free;
end;
end;
Bisakah sebuah halaman yang sudah diantrekan dibatalkan?
CancelAll hanya menghapus job yang masih duduk di antrean; sebuah halaman yang sudah diambil HotPDF dari depan dan diserahkan ke pemanggilan render-nya tetap berjalan hingga selesai, karena THPDFBackgroundRenderer tidak memiliki mekanisme untuk menginterupsi pekerjaan yang sudah berlangsung. Itu adalah trade-off yang wajar dalam praktiknya — sebuah render satu halaman jarang cukup panjang untuk membuat preemption sepadan dengan tambahan kompleksitasnya — tetapi sebuah scroll cepat yang memicu CancelAll pada setiap event scroll tetap membayar biaya untuk halaman mana pun yang sedang mid-render pada saat setiap pembatalan. Referensi resmi terus terang tentang hal ini: rendering yang sudah berjalan mungkin selesai sebelum thread berhenti
Execute memiliki perilaku kedua yang mudah terlewat: loop-nya keluar segera setelah menemukan antrean kosong, ia tidak diam menunggu pekerjaan baru datang. Sebuah instance THPDFBackgroundRenderer karenanya adalah worker batch sekali-pakai, bukan layanan latar belakang yang persisten — antrekan beberapa halaman, panggil Start, dan begitu halaman terakhir yang diantrekan selesai dirender, OS thread di baliknya berakhir dengan sendirinya. Memanggil RequestPage lagi pada instance yang sama setelah Execute sudah menguras antrean tidak akan me-restart-nya, dan itulah persis alasan RequestPageWindow di atas mengganti instance renderer pada setiap pemanggilan alih-alih mencoba terus memberi makan satu objek berumur panjang
Apakah aman menyentuh TBitmap dari thread latar belakang di Delphi?
Menyentuh sebuah TBitmap dari thread latar belakang aman dalam desain HotPDF selama hanya satu thread yang beroperasi pada sebuah instance bitmap tertentu pada satu waktu, dan THPDFBackgroundRenderer menegakkan batas itu alih-alih menyerahkannya ke pemanggil. Execute merender setiap halaman di dalam render lock milik dokumen itu sendiri, critical section yang sama yang sudah dibagi setiap pemanggilan RenderLoadedPageToBitmapCached dan prefetcher bawaan PrefetchLoadedPages, sehingga penggambaran GDI sesungguhnya untuk sebuah halaman tertentu terjadi pada tepat satu thread pada satu waktu dan tidak pernah tumpang tindih dengan render lain dari dokumen tersebut. Bitmap hasilnya adalah objek milik worker-thread yang tidak pernah dipublikasikan THPDFBackgroundRenderer secara langsung ke pemanggil
GetCachedBitmap sebaliknya mengalokasikan sebuah TBitmap baru sama sekali dan memanggil Assign padanya di bawah lock terpisah milik renderer itu sendiri, sehingga penyalinan selalu terjadi selagi Execute terhalang mengganti slot cache itu di baliknya — thread pemanggil mendapatkan data piksel, tidak pernah handle aslinya. Pemisahan itu juga menjadi alasan untuk menghindari membuat thread rendering kustom sendiri yang memanggil fungsi rendering HotPDF secara langsung tanpa melalui THPDFBackgroundRenderer atau PrefetchLoadedPages: dua render yang berlomba terhadap cache bersama dan object graph milik dokumen termuat yang sama itulah persis skenario yang ingin dicegah locking internal HotPDF, dan kelas background renderer memberikan locking itu secara gratis alih-alih Anda perlu mengimplementasikannya ulang
Apa bedanya ini dengan prefetch halaman bawaan HotPDF?
PrefetchLoadedPages dan THPDFBackgroundRenderer menyelesaikan masalah yang berkaitan tetapi berbeda: PrefetchLoadedPages, diberikan sebuah rentang halaman, secara otomatis merender seluruh tetangga itu ke dalam cache dokumen bersama pada worker thread-nya sendiri, tanpa objek antrean yang perlu dibuat atau dikelola pemanggil. THPDFBackgroundRenderer menukar otomasi itu dengan kontrol — pemanggil memutuskan persis indeks halaman mana yang penting dan dalam urutan apa, dan bisa membatalkan yang masih diantrekan tanpa menyentuh rentang apa pun yang sedang dipanaskan prefetcher bawaan di tempat lain. Keduanya menyalur lewat render lock yang sama, sehingga sebuah viewer bisa menjalankan PrefetchLoadedPages untuk kasus beberapa-halaman-berikutnya yang biasa dan hanya menggunakan THPDFBackgroundRenderer ketika sesuatu di luar pola itu muncul, seperti sebuah strip thumbnail yang langsung melompat ke halaman yang baru saja diklik pengguna
begin
// PrefetchLoadedPages takes a 1-based "start-end" range string, while
// RequestPage below stays 0-based like every other loaded-page index.
Pdf.PrefetchLoadedPages(Format('%d-%d', [CenterPage + 1, CenterPage + 5]), 150);
// Reach for THPDFBackgroundRenderer only for a page outside that
// window, such as a thumbnail the user just clicked.
FRenderer := THPDFBackgroundRenderer.Create(Pdf, 150);
FRenderer.RequestPage(ClickedThumbnailPage);
FRenderer.Start;
end;
Dua detail lifecycle layak dibawa ke kode produksi. Cache seluruh-dokumen di balik RenderLoadedPageToBitmapCached dibatasi oleh RenderCacheCapacity, delapan halaman secara default, dan mengusir entri yang paling lama tidak dipakai begitu penuh, tetapi daftar hasil milik sebuah instance THPDFBackgroundRenderer sendiri tidak memiliki batas semacam itu — ia menyimpan satu bitmap per indeks halaman berbeda yang pernah diminta lewat instance itu sampai instance itu sendiri dibebaskan, sehingga sebuah renderer yang dipertahankan hidup sepanjang sesi scroll penuh pada DPI tinggi akan dengan senang hati mengumpulkan satu bitmap resolusi-penuh per halaman yang pernah dilewati. HotPDF juga tidak membatalkan renderer buatan pemanggil secara otomatis seperti cara ia membatalkan prefetcher-nya sendiri sebelum sebuah dokumen dimuat atau menghancurkan dirinya, karena sebuah instance THPDFBackgroundRenderer tidak pernah didaftarkan pada objek THotPDF yang ditunjuknya — sehingga kode pemanggil harus membatalkan dan membebaskan setiap renderer yang dibangun terhadap sebuah dokumen sebelum memuat ulang atau membebaskan dokumen itu, disiplin urutan yang sama yang diterapkan HotPDF secara internal pada PrefetchLoadedPages
THPDFBackgroundRenderer adalah satu bagian dari facade dokumen-termuat di balik arsitektur viewer MVC milik HotPDF, dan berpadu secara alami dengan workflow tingkat-file di Direct File API untuk PDF besar ketika dokumen yang sedang di-scroll sendiri terlalu besar untuk dimuat begitu saja sejak awal. Background rendering, antrean permintaan, dan render cache yang dijelaskan di sini semuanya merupakan bagian dari HotPDF Component standar untuk Delphi dan C++Builder