Satu halaman A4 yang dirender pada zoom membaca yang nyaman memerlukan memori sekitar beberapa megabyte bitmap 32-bit. Kalikan itu dengan kontrak 400 halaman dan aritmatika tersebut tidak lagi abstrak: render setiap halaman di muka dan Anda meminta Windows untuk lebih dari satu gigabyte bitmap yang hanya akan dilihat pengguna satu layar penuh dalam satu waktu. Aplikasi akan kehabisan ruang alamat pada build 32-bit atau menghabiskan beberapa detik pertama dalam keadaan macet sementara GPU dan parser halaman bekerja keras memproses halaman yang belum di-scroll oleh siapa pun. Pembaca continuous-scroll harus terasa seperti satu pita halaman yang panjang, tetapi tidak dapat menahan semuanya di dalam memori secara bersamaan
Ketegangan tersebut adalah inti masalah di sini. PDFium Component menyelesaikannya di dalam TPdfView, sehingga sebagian besar pekerjaannya adalah memilih mode tampilan yang tepat dan memahami apa yang dilakukan komponen atas nama Anda. Bagian-bagian yang tidak dilakukannya untuk Anda, seperti menyesuaikan ukuran halaman untuk alur membaca dan menjaga agar scrolling cepat tetap responsif, adalah bagian di mana sedikit kode akan sangat berguna. Jika Anda masih merakit chrome di sekitarnya (bilah alat, miniatur, kotak pencarian), panduan viewer kaya fitur membahas hal itu; di sini subjeknya adalah gulir (scroll) itu sendiri
Tata letak adalah mode tampilan, bukan panel bitmap
Naluri dari pekerjaan formulir VCL adalah mencari kotak gulir (scroll box) dan menumpuk kontrol gambar di dalamnya, satu per halaman. Hindari itu. Desain tersebut memaksa Anda untuk menangani pemosisian halaman, matematika gulir, dan masalah memori sekaligus, dan Anda akan menciptakan kembali setiap hal tersebut dengan buruk. TPdfView sudah memodelkan dokumen sebagai rangkaian halaman yang berkelanjutan dan mengekspos tata letak melalui properti DisplayMode-nya
Pdf := TPdf.Create(Self);
PdfView := TPdfView.Create(Self);
PdfView.Parent := Self;
PdfView.Align := alClient;
PdfView.Pdf := Pdf;
PdfView.DisplayMode := dmSingleContinuous; // lebar satu halaman, gulir vertikal
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
if not Pdf.Active then
ShowMessage('Could not open the document');
Itulah seluruh pengaturan continuous-scroll. dmSingleContinuous menata halaman-halaman dalam satu kolom vertikal dengan celah di antara mereka ditangani secara internal, dan tampilan bergulir melalui kolom tersebut sebagai satu permukaan. Tidak ada kontrol per halaman yang perlu dihubungkan dan tidak ada handler gulir yang perlu ditulis untuk navigasi biasa. Perhatikan pemeriksaan pada Pdf.Active setelah penetapan: membuka dokumen tidak pernah memicu pengecualian, sehingga berkas yang rusak atau dilindungi kata sandi membiarkan Active bernilai False tanpa ada pengecualian yang ditangkap, dan viewer yang melewatkan pemeriksaan ini akan merender panel kosong dan menyalahkan dirinya sendiri
Properti yang sama membawa mode penyebaran (spread modes). dmTwoPageContinuous menempatkan halaman berdampingan, dua per baris, untuk pembacaan gaya buku yang diinginkan beberapa dokumen; dmTwoPageContinuousWithCover melakukan hal yang sama tetapi membiarkan halaman satu berdiri sendiri sebagai sampul sehingga penyebaran yang tersisa jatuh pada batas genap-ganjil yang alami. Ketiganya bergulir secara berkelanjutan. Beralih di antara mereka adalah penetapan tunggal, yang membuat kotak kombo mode tampilan menjadi sepele untuk ditambahkan nanti
Hanya halaman yang terlihat yang dirasterisasi
Alasan mengapa hal ini dapat diskalakan ke berkas 400 halaman adalah karena kolom tersebut virtual. TPdfView mengetahui tinggi setiap halaman dari pohon halaman dokumen, sehingga dapat menghitung total rentang gulir dan posisi setiap halaman tanpa merasterisasi apa pun. Rasterisasi, langkah mahal yang mengubah aliran konten halaman menjadi piksel, hanya terjadi untuk halaman yang saat ini berpotongan dengan viewport, ditambah sedikit margin agar halaman siap pada saat di-scroll ke dalam tampilan. Saat Anda men-scroll ke bawah, halaman yang memasuki viewport dirender dan halaman yang meninggalkannya bitmap-nya dilepaskan. Memori tetap proporsional dengan apa yang muat di layar, bukan dengan panjang dokumen
Hal ini patut diinternalisasi karena mengubah cara Anda memikirkan tentang biaya kinerja. Membuka dokumen 400 halaman itu murah: ia mengurai strukturnya, bukan kontennya. Biaya tersebut dibayarkan per halaman secara malas (lazily), pada saat halaman di-scroll mendekat. Viewer yang terasa instan saat dibuka dan lancar saat di-scroll tidak melakukan lebih sedikit pekerjaan secara keseluruhan, ia menyebarkan pekerjaan di sepanjang jalur pembacaan aktual pengguna dan membuang apa yang tertinggal di belakang. Konsekuensi praktisnya adalah Anda hampir tidak pernah ingin memaksa merender halaman mendahului pengguna. Biaran tampilan yang menentukan apa yang terlihat
Sesuaikan ukuran halaman dengan lebar, lalu biarkan zoom
Kolom membaca menginginkan halaman disesuaikan dengan lebar panel, bukan disematkan pada zoom absolut. Properti FitMode melakukan ini dan terus melakukannya saat ukuran jendela berubah
PdfView.FitMode := pfmFitWidth; // setiap halaman mengisi lebar kolom; tinggi mengikuti
Dengan pfmFitWidth komponen menghitung ulang zoom setiap kali ukuran tampilan berubah, sehingga kolom selalu mengisi lebar yang tersedia dan tinggi halaman, dan oleh karena itu rentang gulir, mengikuti dari itu. Ada satu jebakan yang sering dialami orang: menetapkan Zoom secara langsung akan menyetel ulang FitMode kembali ke pfmNone. Hal itu disengaja, karena zoom manual dan penyesuaian otomatis adalah niat yang bertentangan, tetapi itu berarti adanya kode PdfView.Zoom := 1.0 yang tersesat di suatu tempat dalam kode Anda secara diam-diam mematikan penyesuaian lebar dan perubahan ukuran berikutnya berhenti mengalir ulang. Jika Anda menawarkan kontrol zoom dan tombol penyesuaian, perlakukan keduanya sebagai sakelar mode: menyetel salah satu akan menghapus yang lain, dan Anda memutuskan mana yang menang
Untuk kontrol zoom absolut yang dibaca secara alami, tampilan mengekspos zoom penyesuaian sebagai nilai yang dapat Anda terapkan atau tampilkan: PageWidthZoom[PageNumber] mengembalikan zoom yang akan menyesuaikan halaman tersebut dengan lebarnya, dan PageZoom yang cocok akan menyesuaikan seluruh halaman. Membaca nilai tersebut adalah cara Anda mengisi menu "Fit Width" / "Fit Page" tanpa mengodekan persentase ajaib secara keras (hard-coding) yang dapat salah pada halaman lanskap atau halaman berukuran besar
Jaga agar gulir cepat tetap responsif dengan rendering progresif
Jalur render default menggambar halaman hingga selesai sebelum ia mengembalikan nilai. Untuk satu halaman, hal itu tidak masalah. Namun selama gulir cepat (flick-scroll) melalui dokumen yang padat, hal itu tidak baik: setiap halaman yang lewat dengan cepat memicu rasterisasi penuh, dan jika pengguna men-scroll lebih cepat daripada halaman dapat dirender, render tersebut akan menumpuk dan panel akan tersendat karena pekerjaan dilakukan untuk halaman yang sudah berada di luar layar pada saat selesai. Perbaikannya adalah membuat render dapat dibatalkan dan meninggalkannya saat pengguna berpindah
RenderPageProgressive merender dalam potongan-potongan (chunks) dan memeriksa token pembatalan pada setiap batas potongan, sehingga render halaman yang sedang berjalan yang baru saja di-scroll dapat dibuang alih-alih dijalankan sampai akhir
type
TFormMain = class(TForm)
// ...
private
FRenderCancel: IPdfCancellationTokenSource;
procedure RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
end;
procedure TFormMain.RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
var
Status: TPdfProgressiveStatus;
begin
// Batalkan apa pun yang sedang dirender; token lama sekarang diberi sinyal.
if Assigned(FRenderCancel) then
FRenderCancel.Cancel;
FRenderCancel := TPdfCancellationTokenSource.New;
Pdf.PageNumber := PageNo;
Status := Pdf.RenderPageProgressive(Bmp, 0, 0, Bmp.Width, Bmp.Height,
FRenderCancel.Token);
case Status of
prsDone: ; // bitmap selesai, lukis
prsCancelled: Exit; // digantikan, buang hasil ini
prsFailed: ShowMessage('Render failed for page ' + IntToStr(PageNo));
end;
end;
Pembatalan dipantau pada batas potongan alih-alih secara preemptif, jadi harapkan latensi puluhan milidetik antara memanggil Cancel dan render benar-benar berhenti. Itu masih jauh lebih murah daripada membiarkan render halaman penuh yang usang memblokir antrean. Meneruskan nilai nil sebagai token akan merender langsung hingga selesai, yang merupakan pilihan tepat untuk render satu kali seperti pratinjau cetak di mana tidak ada yang perlu dibatalkan
Ketika Anda memanggil bentuk fungsi dari RenderPage, yang mengembalikan TBitmap baru, ingatlah bahwa pemanggil memilikinya dan harus memanggil Free pada objek tersebut. Dalam loop gulir yang mengalokasikan bitmap per halaman, melupakan hal ini adalah kebocoran memori yang tumbuh seiring setiap halaman yang dilewati pengguna, yang persis merupakan kegagalan memori tak terbatas yang seharusnya dihindari oleh desain continuous. Render-lah ke dalam bitmap yang digunakan kembali jika Anda bisa
Apa yang tersisa untuk Anda
Pembaca continuous-scroll sebagian besar diserahkan kepada komponen untuk menyediakannya. Anda memilih dmSingleContinuous untuk tata letak, menyetel pfmFitWidth agar kolom mengalir ulang dengan jendela, dan memeriksa Pdf.Active agar berkas yang buruk gagal dengan keras. Satu-satunya bagian yang layak Anda tulis sendiri adalah rendering yang dapat dibatalkan, karena pembaca dinilai dari bagaimana ia berperilaku ketika seseorang menyeret bilah gulir (scrollbar) ke bagian bawah dokumen yang panjang dan panelnya dapat mengikuti atau tidak. Segala sesuatu setelah itu, pemilihan teks lintas halaman, penyorotan pencarian, pohon bookmark, adalah pekerjaan antarmuka yang berada di atas permukaan gulir ini, bukan di dalamnya
API TPdfView, DisplayMode, dan RenderPageProgressive yang ditunjukkan di sini adalah bagian dari PDFium Component untuk Delphi dan Lazarus