Artikel Teknis

Viewer PDF Continuous Scrolling di Delphi dengan PDFium Component

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