Kebanyakan halaman PDF dirasterisasi dalam beberapa milidetik dan Anda tak pernah memikirkannya. Lalu seorang pengguna membuka gambar teknik A1, halaman yang padat puluhan ribu goresan vektor, atau poster yang sesak grup transparansi dan soft mask, dan satu panggilan yang mengecatnya memakan waktu dua-tiga detik. Jika panggilan itu berjalan di UI thread, jendela berhenti mengecat ulang, title bar mengabu, dan sistem operasi menawarkan untuk mematikan aplikasi. Pekerjaannya sah. Halaman itu memang butuh waktu selama itu. Cacatnya, render tersebut adalah satu panggilan pemblokir yang tak terpisahkan, tanpa cara untuk mengambil napas dan tanpa cara untuk berhenti
Artikel ini membahas tepat satu dari dua masalah itu: membatalkan render satu halaman yang panjang tanpa membekukan UI. Pengguna mengklik halaman berikutnya, atau men-zoom, atau menutup dokumen, dan render yang sedang berjalan kini menjadi pekerjaan sia-sia yang harus berakhir pada kesempatan pertama alih-alih dijalankan sampai tuntas. Menghaluskan gulir dan zoom dengan men-cache yang sudah dirasterisasi adalah persoalan tersendiri dengan desainnya sendiri, dibahas di artikel pendamping yang ditautkan di akhir. Di sini satu-satunya pertanyaannya adalah bagaimana membuat satu render progresif menjawab permintaan pembatalan dengan cepat dan bersih
API render progresif yang sudah dibawa PDFium
PDFium telah mengantisipasi separuh masalah pembekuan itu. Di samping FPDF_RenderPageBitmap yang sekali jalan, ia menyediakan varian progresif yang memecah halaman menjadi potongan-potongan pekerjaan. Anda memanggil FPDF_RenderPageBitmap_Start sekali untuk menyiapkan render ke bitmap tujuan, lalu memanggil FPDF_RenderPage_Continue berulang kali. Setiap Continue merasterisasi irisan yang dibatasi dan mengembalikan sebuah status. FPDF_RENDER_TOBECONTINUED berarti masih ada yang harus dikerjakan, FPDF_RENDER_DONE berarti halaman selesai, dan FPDF_RENDER_FAILED berarti ia berhenti karena galat. Saat perulangan berakhir Anda memanggil FPDF_RenderPage_Close untuk melepaskan status progresif per halaman. Karena kendali kembali ke kode Anda di antara irisan, Anda bisa memompa pesan, memperbarui indikator progres, atau memeriksa apakah pekerjaan itu masih diinginkan
Mekanisme yang disediakan PDFium untuk memutuskan kapan harus mengalah adalah struct callback bernama IFSDK_PAUSE. Anda menyerahkannya ke Start dan ke setiap Continue. Setelah setiap chunk PDFium memanggil pointer fungsi NeedToPauseNow-nya, dan bila itu mengembalikan nilai bukan nol, Continue saat ini berhenti lebih awal dan menyerahkan kendali kembali dengan FPDF_RENDER_TOBECONTINUED. Struct itu juga membawa field version yang harus diisi 1, serta pointer user bebas bentuk yang tak pernah disentuh PDFium dan diteruskan apa adanya. Pointer yang tak tersentuh itulah seluruh poros dari desain yang mengikuti
Mengubah fungsi jeda menjadi batal
Maksud awal NeedToPauseNow adalah pemenggalan waktu. Kembalikan nilai bukan nol saat jatah frame Anda habis, kembalikan nol untuk terus merender, dan PDFium berjeda agar Anda bisa mengerjakan hal lain sebelum melanjutkan render yang sama. PDFium Component memakai ulang sinyal yang sama untuk kata kerja yang berbeda. Alih-alih menjawab "haruskah saya berjeda agar Anda bisa melanjutkan", callback menjawab "apakah pekerjaan ini sudah dibatalkan". Keduanya saling petakan dengan rapi berkat apa yang dilakukan perulangan saat melihat tanda itu. Jeda sungguhan mengharapkan Continue di kemudian hari; pembatalan tidak. Begitu perulangan pemanggil melihat token-nya dibatalkan, ia menutup konteks render dan tak pernah lagi memanggil Continue, sehingga nilai bukan nol yang sama yang dibaca PDFium sebagai "hentikan chunk ini" menjadi, efektifnya, "berhenti untuk selamanya"
Pembatalan diekspresikan lewat sebuah antarmuka, IPdfCancellationToken, yang properti IsCancelled-nya berubah dari false ke true ketika bagian lain dari program meminta render berhenti. Jembatan antara antarmuka Pascal itu dan callback C milik PDFium adalah satu pointer. Referensi antarmuka token ditulis ke IFSDK_PAUSE.user, dan callback cdecl statis membacanya kembali lalu menanyakannya. Inilah masalah klasik membiarkan pustaka C memanggil balik ke Pascal: callback harus berupa fungsi polos dengan calling convention C, bukan metode, karena PDFium menyimpan dan memanggil pointer fungsi telanjang yang tak tahu apa-apa tentang objek Pascal atau Self
type
TPdfProgressivePause = record
Pause: IFSDK_PAUSE; // PDFium membaca ini; .user menampung token
Token: IPdfCancellationToken; // referensi kuat menjaga token tetap hidup
end;
function ProgressivePauseCallback(pThis: PIFSDK_PAUSE): FPDF_BOOL; cdecl;
var
Token: IPdfCancellationToken;
begin
Result := 0;
if (pThis = nil) or (pThis^.user = nil) then
Exit;
Token := IPdfCancellationToken(pThis^.user);
if Token.IsCancelled then
Result := 1; // bukan nol: PDFium menghentikan chunk ini
end;
Callback memulihkan token dengan meng-cast pThis^.user kembali ke tipe antarmukanya lalu membaca IsCancelled. Tak ada bagian darinya yang mengalokasikan, mengunci, atau memblokir, dan ini penting karena PDFium memanggilnya di thread rendering setelah setiap chunk, dan pekerjaan apa pun yang dilakukan di sini menambah biaya render itu sendiri. Pelindung terhadap struct nil maupun field user nil membuat fungsi yang sama aman dipasang bahkan pada render yang tak pernah diberi token sungguhan
Menjaga token tetap hidup sepanjang perulangan
Meng-cast pointer antarmuka lewat Pointer mentah dan mengembalikannya adalah tempat bug umur hidup dilahirkan. IInterface di Delphi dihitung referensinya, dan hitungan itu hanya bergerak ketika kompiler bisa melihat variabel bertipe antarmuka sedang diisi. Menyimpan token hanya sebagai pointer telanjang di dalam IFSDK_PAUSE.user akan menyembunyikannya sepenuhnya dari penghitung referensi. Jika satu-satunya referensi lain ke token itu keluar dari cakupan selagi perulangan Continue masih berjalan, objeknya akan dibebaskan di bawah callback, dan chunk berikutnya akan mendereferensi pointer menggantung
Itulah sebabnya deskriptornya adalah record yang menampung dua hal, bukan satu. Field Pause adalah struct yang dibaca PDFium. Field Token adalah referensi bertipe antarmuka sungguhan yang dihitung kompiler, dan keberadaannya tak lain untuk menyematkan token di memori selama record itu hidup. Record itu adalah variabel lokal di stack rutin render, sehingga tetap valid sepanjang durasi perulangan dan baru dibongkar saat rutin keluar. Pointer telanjang di user dan referensi terhitung di Token menunjuk objek yang sama; yang satu adalah yang bisa dibaca PDFium, yang lain adalah yang menjaga objek itu agar tidak dikumpulkan
var
Pause: TPdfProgressivePause;
EffectiveToken: IPdfCancellationToken;
begin
// ... pilih EffectiveToken ...
// Referensi kuat lebih dulu, lalu publikasikan objek yang sama ke PDFium lewat .user.
Pause.Token := EffectiveToken;
Pause.Pause.version := 1;
Pause.Pause.NeedToPauseNow := ProgressivePauseCallback;
Pause.Pause.user := Pointer(EffectiveToken);
Menutup konteks render apa pun akhir perulangannya
Setiap panggilan FPDF_RenderPageBitmap_Start mengalokasikan status progresif yang dikaitkan PDFium dengan halaman, dan status itu hanya dilepaskan oleh FPDF_RenderPage_Close. Ada tiga jalan keluar dari perulangan penggerak. Halaman selesai dan status terakhir FPDF_RENDER_DONE. Token terpicu dan perulangan keluar lebih awal sambil melaporkan pembatalan. Sesuatu gagal dan statusnya FPDF_RENDER_FAILED. Ketiganya wajib memanggil Close, dan jalur pembatalan paling mudah keliru, karena bentuk alami "lihat batal, keluar" cenderung melewatkan pembersihan dalam perjalanan ke pintu keluar. Membiarkan Close tak terpanggil membocorkan status per halaman, dan viewer yang mengizinkan pengguna membatalkan render demi render akan menumpuk kebocoran itu di setiap halaman yang digugurkan
Bentuk yang tangguh menempatkan perulangan dan klasifikasi hasil di dalam try serta FPDF_RenderPage_Close di finally yang berpasangan. Bitmap tujuan dihancurkan di blok yang sama. Pembatalan bisa meninggalkan perulangan lewat Exit dini dan finally tetap berjalan, sehingga tepat ada satu tempat yang membebaskan status progresif dan itu tak bisa dilewati
Status := FPDF_RenderPageBitmap_Start(PdfBmp, FPage, Left, Top,
Width, Height, Ord(Rotation), EncodeRenderOptions(Options), Pause.Pause);
try
while Status = FPDF_RENDER_TOBECONTINUED do
begin
if EffectiveToken.IsCancelled then
begin
Result := prsCancelled;
Exit;
end;
Status := FPDF_RenderPage_Continue(FPage, Pause.Pause);
end;
if EffectiveToken.IsCancelled then
Result := prsCancelled
else if Status = FPDF_RENDER_DONE then
Result := prsDone
else
Result := prsFailed;
finally
// Membebaskan status progresif yang dialokasikan Start; wajib di setiap jalur.
FPDF_RenderPage_Close(FPage);
FPDFBitmap_Destroy(PdfBmp);
end;
Perulangan memeriksa token sebelum setiap Continue selain mengandalkan callback di dalamnya. Callback memperpendek chunk saat ini; pemeriksaan perulangan mencegah chunk berikutnya dimulai. Bersama-sama keduanya membatasi lamanya waktu hingga pembatalan berlaku menjadi kira-kira durasi satu chunk
Tiga hasil, dan apa isi bitmap setelah pembatalan
Titik masuk publiknya adalah TPdf.RenderPageProgressive, dan ia mengembalikan TPdfProgressiveStatus yang bernilai salah satu dari prsDone, prsCancelled, atau prsFailed. Nilai-nilai itu mengikuti konstanta FPDF_RENDER_* milik PDFium dalam idiom Pascal, tetapi melipat kasus pembatalan sebagai hasil kelas satu alih-alih galat
Hal yang menjerat orang adalah isi bitmap tujuan setelah prsCancelled. Ia tidak kosong. PDFium merender secara progresif ke bitmap yang sama chunk demi chunk, jadi saat pembatalan menghentikan perulangan, bitmap menampung semua yang sudah dicat hingga saat itu, yakni gambar parsial: sebagian band selesai, sisanya masih menampilkan warna isi. Apakah hasil parsial itu berguna tergantung pemanggilnya. Viewer yang akan membuang bitmap karena pengguna bernavigasi ke tempat lain cukup mengabaikannya. Viewer yang ingin menampilkan pratinjau murah bisa menyimpannya. Yang tak boleh Anda lakukan adalah menganggap prsCancelled berarti bitmap kosong atau tak terdefinisi; ia berarti potongan jujur dari render yang belum selesai
var
Bmp: TBitmap;
Token: IPdfCancellationToken;
Status: TPdfProgressiveStatus;
begin
Bmp := TBitmap.Create;
try
// Token dimulai belum dibatalkan; ubah Token.IsCancelled dari tempat lain
// (aksi UI, event navigasi) untuk menggugurkan render yang sedang berjalan.
Status := Pdf.RenderPageProgressive(Bmp, 0, 0, PageW, PageH, Token);
case Status of
prsDone: Image1.Picture.Assign(Bmp); // sepenuhnya terender
prsCancelled: ; // bitmap parsial, biasanya dibuang
prsFailed: ShowMessage('Render failed');
end;
finally
Bmp.Free;
end;
end;
Token nil dan jalur callback tanpa percabangan
Pembatalan bersifat opsional. Pemanggil yang hanya ingin render progresif demi manfaat pemompaan pesan, tanpa niat menggugurkan, harusnya bisa mengoper nil untuk token. Cara naif mendukungnya adalah menebar pemeriksaan "jika token disediakan" ke seluruh callback dan perulangan, yang berarti percabangan di setiap chunk dan callback yang harus menangani token sungguhan sekaligus ketiadaannya
Implementasinya menghindari hal itu dengan mensubstitusi sebuah singleton saat pemanggil tak mengoper apa pun. Token nil ditukar dengan PdfNoCancellationToken, antarmuka yang IsCancelled-nya selalu false. Dari titik itu callback dan perulangan selalu punya token untuk ditanyai, sehingga keduanya tak butuh pemeriksaan nil dan tak butuh jalur khusus. Token yang tak pernah batal selalu menjawab false, callback selalu mengembalikan nol, dan render berjalan sampai tuntas persis seperti render yang tak bisa dibatalkan. Perilaku opsional dimodelkan sebagai token yang tak pernah terpicu alih-alih ketiadaan token, dan itu menjaga jalur panas tetap seragam
// nil -> singleton tak-pernah-batal, sehingga jalur callback identik
// terlepas dari apakah pemanggil memilih pembatalan.
if AToken <> nil then
EffectiveToken := AToken
else
EffectiveToken := PdfNoCancellationToken;
Bentuk yang muncul ini kecil dan layak dinyatakan ulang, karena di situlah bagian yang dapat dipakai ulang. Pustaka C yang mendukung callback memberi Anda tepat satu saluran untuk meneruskan state ke dalam callback itu, yaitu pointer user yang opak. Letakkan referensi antarmuka Pascal terhitung di balik pointer itu, jaga referensi sungguhan kedua tetap hidup di samping struct agar objek tak bisa dikumpulkan di tengah panggilan, dan baca antarmukanya kembali di dalam fungsi cdecl statis. Bungkus seluruh perulangan penggerak dalam try dan bebaskan konteks natif di finally. Templat yang sama terbawa ke operasi PDFium progresif maupun callback-driven mana pun di mana kode Pascal harus tetap mengendalikan umur hidup sementara C memegang pointernya
Pembatalan hanyalah separuh dari viewer yang responsif. Separuh lainnya adalah tidak merender ulang halaman yang sudah Anda gambar, dan menjaga zoom serta gulir tetap mulus dengan menyajikan bitmap ter-cache, yang dibahas dalam artikel kami tentang cache render dan kinerja zoom. Untuk melihat bagaimana render yang dapat dibatalkan itu berpadu menjadi viewer lengkap di samping navigasi, seleksi, dan pencarian, lihat membangun viewer PDF kaya fitur dengan PDFium Component. Render progresif yang dijelaskan di sini dikirimkan sebagai bagian dari PDFium Component untuk Delphi dan Lazarus, bersama API pemuatan, render, dan form yang dibahas di bagian lain blog ini