Arsip yang dipindai dapat mencapai ukuran beberapa gigabyte dalam satu PDF. Penampil yang membuka file tersebut biasanya hanya ingin menampilkan satu halaman, mungkin daftar isi, atau halaman yang dituju pengguna dari bookmark. Membaca seluruh file ke memori untuk merender dua halaman sangat boros di setiap sisi: membuang ruang alamat memori, membuat pengguna menunggu lama di awal pembacaan, dan pada proses Delphi 32-bit dapat gagal total sebelum satu halaman pun muncul. PDFium dibangun dengan mempertimbangkan hal ini. Ia dapat memuat dokumen melalui callback yang meminta rentang byte spesifik yang dibutuhkannya, saat ia membutuhkannya, dan tidak pernah meminta seluruh file sekaligus.
Komponen mengekspos jalur tersebut melalui adaptor stream. Anda menyerahkan TStream apa pun, dan PDFium menarik blok dari stream tersebut sesuai permintaan. File dapat berada di disk, dalam kolom blob database, atau di belakang turunan TStream lainnya, dan tidak ada bagian darinya yang disalin ke memori terlebih dahulu.
Bagaimana PDFium meminta byte
C API PDFium memuat dokumen dari objek yang disediakan pemanggil yang dideskripsikan oleh struktur FPDF_FILEACCESS. Struktur tersebut memiliki tiga bagian penting di sini: kolom panjang (length field), callback pembacaan, dan parameter pengguna buram (opaque). Titik masuk yang mengonsumsinya adalah FPDF_LoadCustomDocument. Setelah PDFium memegang struktur tersebut, ia mengurai trailer, menemukan lokasi tabel referensi silang, dan sejak saat itu hanya membaca apa yang dibutuhkan oleh operasi tertentu. Membuka dokumen menyentuh bagian ekor file dan beberapa objek katalog. Merender halaman 400 membaca stream konten dan sumber daya untuk halaman itu saja dan tidak ada yang lain.
Ini adalah perbedaan antara pemuatan ber-buffer (buffered load) dan pemuatan streaming (streaming load). Pemuatan ber-buffer membaca file dari ujung ke ujung sebelum PDFium melihat byte nol. Pemuatan streaming membalikkan hubungan tersebut: PDFium yang mendorong pembacaan, dan byte yang tidak pernah disentuh tidak pernah dibaca. Untuk file multi-gigabyte yang dilihat satu halaman setiap kali, itu adalah perbedaan antara pemuatan yang tidak dapat digunakan dan pemuatan yang instan.
Adaptor stream
Adaptor yang menjembatani Delphi TStream ke FPDF_FILEACCESS adalah TPdfStreamAdapter. Konstruktornya mengambil stream dan flag kepemilikan, mengambil panjang stream sekali, mengisi record FPDF_FILEACCESS, dan menghubungkan callback baca. Ketika PDFium memanggil kembali nanti dengan offset dan ukuran, adaptor mencari (seek) stream ke offset tersebut dan menyalin rentang tersebut dengan tepat ke dalam buffer yang disediakan PDFium.
// Verbatim from the component: the stream-to-FPDF_FILEACCESS bridge
constructor TPdfStreamAdapter.Create(AStream: TStream; AOwnsStream: Boolean);
begin
inherited Create;
if AStream = nil then
raise EPdfError.Create('TPdfStreamAdapter: AStream is nil');
FStream := AStream;
FOwnsStream := AOwnsStream;
// FPDF_FILEACCESS.m_FileLen is a 32-bit unsigned long. Refuse a stream
// that would silently truncate past 4 GiB.
if AStream.Size > High(FPDF_DWORD) then
raise EPdfError.Create('TPdfStreamAdapter: stream exceeds the 4 GiB limit');
FillChar(FFileAccess, SizeOf(FFileAccess), 0);
FFileAccess.m_FileLen := FPDF_DWORD(AStream.Size);
FFileAccess.m_GetBlock := GetBlockCallback;
FFileAccess.m_Param := Self;
end;
Flag kepemilikan memutuskan siapa yang membebaskan stream. Lewatkan False dan pemanggil mempertahankan stream dan harus menjaganya tetap hidup selama masa pakai dokumen. Lewatkan True dan adaptor mengambil alih, membebaskan stream saat dokumen ditutup. Dengan cara apa pun, stream harus hidup lebih lama dari setiap pembacaan yang dilakukan PDFium, karena PDFium memegang pointer FPDF_FILEACCESS and will call back at any point while the document is open, not only during the initial load.
Mengapa callback adalah fungsi statis
Callback baca yang disimpan oleh PDFium di m_GetBlock adalah pointer fungsi C biasa dengan konvensi pemanggilan cdecl. Metode Delphi tidak dapat digunakan secara langsung, karena sebuah metode membawa argumen Self tersembunyi yang tidak diketahui oleh pemanggil C dan tidak akan pernah disediakan. Oleh karena itu, adaptor mendeklarasikan callback sebagai class function yang ditandai dengan cdecl; static, yang dikompilasi menjadi fungsi mandiri dengan tata letak frame C yang diharapkan PDFium tanpa adanya Self implisit.
Hal tersebut menyelesaikan masalah konvensi pemanggilan tetapi memunculkan pertanyaan kedua: tanpa adanya Self, bagaimana callback menjangkau stream spesifik yang seharusnya dibaca? Jawabannya adalah parameter pengguna buram (opaque). Ketika adaptor membangun record, ia menyimpan pointer instans-nya sendiri di m_Param. PDFium menyerahkan kembali pointer yang sama sebagai argumen pertama dari setiap callback. Fungsi statis mentransmisikan kembali (cast) pointer tersebut ke TPdfStreamAdapter dan mengirimkan pembacaan ke stream instans tersebut. Ini adalah metode transmisi (trampoline) standar untuk menyerahkan konteks objek melintasi batas C yang tidak memiliki konsep objek.
// Verbatim from the component: the cdecl trampoline back to the instance
class function TPdfStreamAdapter.GetBlockCallback(
param : Pointer;
position: FPDF_DWORD;
pBuf : PByte;
size : FPDF_DWORD): Integer; cdecl;
var
Adapter: TPdfStreamAdapter;
begin
Result := 0;
if (param = nil) or (pBuf = nil) or (size = 0) then
Exit;
Adapter := TPdfStreamAdapter(param); // recover the instance from m_Param
if Adapter.FStream = nil then
Exit;
try
Adapter.FStream.Position := Int64(position);
Adapter.FStream.ReadBuffer(pBuf^, Int64(size));
Result := 1;
except
Result := 0; // report failure by return value, never by raising
end;
end;
Batas atas 4 GiB dan mengapa ia memerlukan pelindung
Kolom panjang m_FileLen di FPDF_FILEACCESS adalah nilai unsigned 32-bit. Panjang terbesar yang dapat direpresentasikannya kurang satu byte dari 4 GiB. TStream melaporkan ukurannya sebagai Int64, sehingga stream dapat menggambarkan jauh lebih banyak byte daripada yang dapat ditampung oleh kolom tersebut. Begitu ukuran stream melebihi batas itu, tidak ada cara jujur untuk memberi tahu PDFium seberapa panjang file tersebut.
Tanggapan yang salah adalah menetapkan ukuran tersebut dan membiarkannya membungkus (wrap). Memotong panjang 5 GiB menjadi kolom 32-bit menghasilkan angka kecil yang terlihat masuk akal, dan PDFium kemudian akan mengurai file dengan keyakinan bahwa file berakhir sekitar satu gigabyte. Trailer dan tabel referensi silang berada di akhir file yang sebenarnya, jauh melewati panjang yang terpotong, sehingga penguraian gagal dengan cara yang tidak ada hubungannya dengan penyebab sebenarnya. Anda akan mendebug kesalahan referensi silang pada file yang sebenarnya valid, tanpa ada petunjuk bahwa integer meluap (wrapped) dua lapisan di atasnya.
Sebagai gantinya, adaptor menolak input. Konstruktor membandingkan ukuran stream dengan High(FPDF_DWORD) dan memunculkan EPdfError saat stream terlalu besar untuk digambarkan. Kesalahan yang eksplisit dan langsung menyebutkan masalah sebenarnya pada titik konstruksi. Pemotongan senyap menyembunyikannya di balik gejala menyesatkan yang akan Anda kejar jauh di kemudian hari. Batas 4 GiB adalah batasan nyata dari jalur pemuatan ini, dan hal yang jujur adalah memunculkannya dengan lantang daripada menutupi masalah tersebut dengan aritmatika yang kebetulan berhasil dikompilasi.
Kegagalan tidak boleh melintasi batas
Pembacaan dapat gagal. Stream mungkin merupakan objek berbasis jaringan yang mengalami timeout, handle blob yang ditutup di bawah Anda, atau file yang terpotong setelah dokumen dibuka. Kontrak PDFium untuk callback baca adalah nilai kembalian: bukan nol untuk sukses, nol untuk kegagalan. Ini adalah frame C, dan tidak memiliki mekanisme untuk menangkap atau menyebarkan pengecualian Pascal.
Inilah sebabnya mengapa fungsi transmisi (trampoline) membungkus proses pencarian (seek) dan pembacaan dalam try/except yang menelan pengecualian dan mengmengerahkan nilai nol. Jika pengecualian Delphi dibiarkan menyebar keluar dari callback, ia akan me-unwind melalui stack frame cdecl PDFium, yang tidak pernah dibuat untuk di-unwind oleh mesin pengecualian Pascal. Hasilnya adalah perilaku tidak terdefinisi (undefined behavior) paling baik dan crash keras paling buruk, jauh di dalam parser PDF tanpa adanya stack yang dapat digunakan. Mengmengembalikan nol menjaga kegagalan tetap berada di dalam kontrak. PDFium melihat pembacaan blok yang gagal, membatalkan operasi secara bersih, dan FPDF_LoadCustomDocument melaporkan dokumen tidak dapat dimuat, yang ditampilkan oleh komponen sebagai EPdfError pada sisi Pascal tempat ia seharusnya berada.
Membuka dokumen dengan cara ini
Metode komponen yang menggerakkan jalur streaming adalah LoadCustomDocument, yang dideklarasikan sebagai metode yang berbeda, bukan kelebihan beban (overload) LoadDocument lainnya sehingga meneruskan TMemoryStream tidak pernah secara tidak sengaja mendarat di jalur ber-buffer. Metode ini membangun adaptor, memanggil FPDF_LoadCustomDocument, dan menjaga adaptor tetap hidup selama masa pakai dokumen yang dimuat.
var
Pdf: TPdf;
FileStream: TFileStream;
begin
Pdf := TPdf.Create(nil);
FileStream := TFileStream.Create('Archive_4GB.pdf', fmOpenRead or fmShareDenyWrite);
try
// Hand stream ownership to Pdf: it frees FileStream when the document closes.
Pdf.LoadCustomDocument(FileStream, True);
// PDFium has read only the trailer and catalog so far.
// Rendering a page pulls just that page's bytes through the callback.
// ... render or inspect pages here ...
finally
Pdf.Free; // closes the document, which frees the adapter and the stream
end;
end;
Pemanggilan yang sama berfungsi untuk TMemoryStream, stream blob dari dataset database, atau turunan TStream kustom. Pemuatan sesuai permintaan (on-demand loading) terbukti berguna ketika file berukuran besar dan hanya sebagian saja yang akan dibaca: penampil arsip, generator thumbnail yang mengambil sampel beberapa halaman, indeks pencarian yang menarik satu halaman pada satu waktu. Ketika file kecil atau Anda tetap akan membaca semuanya, pemuatan ber-buffer lebih sederhana dan mekanisme streaming tidak memberikan keuntungan apa pun bagi Anda. Faktor penentunya adalah rasio byte yang benar-benar akan Anda sentuh dengan byte yang dikandung file.
Setelah halaman mengalir sesuai permintaan, kekhawatiran berikutnya adalah menjaga halaman yang dirender tetap responsif saat pengguna memperbesar dan menggulir, yang dibahas dalam catatan kami tentang render caching dan kinerja zoom. Ketika dokumen yang dialirkan adalah dokumen yang harus ditampilkan oleh penampil tetapi tidak membiarkan pengguna mengekspor atau mengubahnya, teknik dalam the secure PDF preview walkthrough berpasangan secara alami dengan jalur pemuatan ini. Both build on the streaming load described here, which ships as part of the PDFium Component for Delphi and C++Builder alongside the rendering, text extraction, and annotation APIs covered elsewhere on this blog.