Artikel Teknis

Output PDF Linearized di Delphi: Hint Table HotPDF

HotPDF menulis file PDF linearized, layout yang oleh Acrobat disebut Fast Web View, lewat properti LinearizeOutput pada THotPDF. Mengaturnya sebelum BeginDoc membuat HotPDF menyusun ulang graf objek yang sudah jadi sehingga reader yang paham byte-range bisa menampilkan halaman satu setelah hanya mengambil bagian awal file, alih-alih mengunduh seluruh dokumen lebih dulu. Mekanismenya adalah ISO 32000-1 Annex F

Alasan ini penting sifatnya tidak glamor. PDF biasa menaruh tabel cross-reference-nya di akhir, jadi viewer harus mencapai byte terakhir sebelum tahu di mana apa pun berada. Berikan browser laporan hasil scan 200 halaman dan pengguna hanya menatap spinner selama transfer penuh, padahal yang mereka inginkan hanyalah halaman 1. Linearization memperbaiki itu dengan membayar biaya saat penulisan. Artikel ini secara spesifik membahas jalur penulisan itu, partisi, loop pengukuran dan batasan kerasnya; untuk latar belakang konseptual tentang manfaat Fast Web View, artikel sebelumnya penjelasan optimasi PDF linearization dan Fast Web View membahas dasarnya

Apa sebenarnya yang dijamin layout linearized itu?

File linearized adalah PDF biasa dengan urutan fisik yang sangat spesifik, dan setiap jaminan yang ditawarkannya berasal dari urutan itu, bukan dari tipe objek baru mana pun. HotPDF menerbitkan bagian-bagiannya dalam urutan yang ditetapkan Annex F: dictionary parameter linearization di dalam 1024 byte pertama, tabel cross-reference awal, objek level dokumen, hint stream utama, halaman pertama dan objek privatnya, lalu halaman-halaman sisanya, lalu objek bersama, lalu semuanya yang lain, dan terakhir tabel cross-reference utama

Partisinya diturunkan, bukan dinyatakan. HotPDF menelusuri graf referensi dari setiap objek halaman dan mencatat, untuk setiap objek tak langsung, berapa banyak halaman yang menjangkaunya dan halaman mana yang pertama kali menjangkaunya. Objek yang dipakai persis oleh satu halaman menjadi privat untuk halaman itu. Objek yang dijangkau lebih dari satu halaman menjadi bersama. Catalog, ditambah apa pun yang dirujuknya di bawah /ViewerPreferences, /OpenAction, /Threads dan /AcroForm, ditambah dictionary enkripsi saat proteksi aktif, membentuk grup level-dokumen yang harus mendahului segalanya. Node page tree sengaja ditahan agar tidak mencemari bagian halaman pertama

Dictionary parameter membawa angka-angka yang dibutuhkan reader sebelum ia membaca apa pun yang lain: /L untuk panjang total file, /H untuk offset dan panjang hint stream, /O untuk nomor objek halaman pertama, /E untuk byte tempat bagian halaman pertama berakhir, /N untuk jumlah halaman dan /T untuk offset entri tabel cross-reference utama. Setiap satu dari itu adalah offset byte ke dalam file yang belum ada pada saat Anda perlu menulisnya

Kenapa offset hint table harus konvergen?

Karena angka-angka di dictionary parameter menggambarkan file yang berisi angka-angka itu sendiri, dan mengubah salah satunya mengubah filenya. Itulah kesulitan sentral dari writer linearized, dan itulah kenapa HotPDF mengukur berulang kali alih-alih menulis sekali. Perlebar /T dari 6 digit menjadi 7 dan dictionary parameter bertambah satu byte; header bertambah; setiap objek bergeser; tabel cross-reference utama berpindah; /T sekarang butuh nilai berbeda. Layout harus mencapai titik tetap sebelum satu byte pun output nyata dikomit

HotPDF menangani ini dengan iterasi terbatas. Pertama-tama ia menyerialisasi setiap objek ke dalam stream penghitung yang mencatat panjang tanpa menyimpan byte-nya, sehingga setiap objek punya ukuran serialisasi yang diketahui. Kemudian ia menjalankan pass layout yang menetapkan offset ke grup level-dokumen, hint stream, grup halaman pertama, grup halaman-halaman berikutnya, grup bersama dan sisanya, dan melaporkan di mana tabel cross-reference utama akan berada. Hasil itu dimasukkan kembali sebagai input untuk pass berikutnya. Loop-nya dibatasi hingga delapan percobaan, dan kegagalan konvergensi memicu exception alih-alih menghasilkan file dengan offset yang terlihat masuk akal tapi salah

CandidateMainOffset := 0;
for Attempt := 0 to 7 do
begin
  CalculateLayout(CandidateMainOffset, FirstXRefData,
    HintOffset, EndFirstPage, NewMainOffset);
  if NewMainOffset = CandidateMainOffset then
    Break;
  CandidateMainOffset := NewMainOffset;
end;
if NewMainOffset <> CandidateMainOffset then
  raise Exception.Create('Linearization layout did not converge');

Dua detail menjaga loop itu tidak thrashing. Dictionary parameter ditulis ke slot tetap 384 byte, diisi padding spasi, sehingga pertumbuhannya sendiri tidak pernah mengguncang layout; jika teks dictionary itu sampai melebihi cadangan tersebut, HotPDF memicu exception alih-alih diam-diam menggeser segalanya. Dan setelah konvergensi, HotPDF menjalankan satu pass layout konfirmasi lagi dan memeriksa ulang panjang hint stream, karena hint stream itu sendiri meng-encode offset yang baru diketahui setelah layout stabil. Hasil dari seluruh pengukuran ini adalah HotPDF tidak pernah menyimpan salinan kedua dari dokumen: begitu offset ditetapkan, objek diserialisasi langsung ke dalam stream tujuan, dengan assertion di setiap batas bagian bahwa byte yang ditulis cocok dengan offset yang dijanjikan

Mengaktifkannya dari Delphi

Permukaan API-nya hanya satu Boolean, dan satu-satunya syaratnya adalah Anda mengaturnya sebelum generasi dimulai. LinearizeOutput defaultnya False, dan pass layout berjalan saat dokumen ditulis, jadi menetapkannya setelah EndDoc tidak mengubah apa pun

var
  PDF: THotPDF;
begin
  PDF := THotPDF.Create(nil);
  try
    PDF.FileName := 'fast-view.pdf';
    PDF.Version := pdf17;
    PDF.LinearizeOutput := True;      // must precede BeginDoc
    PDF.BeginDoc;
    PDF.Canvas.TextOut(72, 72, 'First page');
    PDF.EndDoc;
  finally
    PDF.Free;
  end;
end;

Satu peringatan deployment mengalahkan segalanya di sisi kode. Linearization hanya berguna saat transport mendukung HTTP range request. Sajikan file yang sama dari endpoint yang men-stream-nya utuh, atau dari konfigurasi CDN yang mengabaikan Range, dan Anda baru saja membeli jalur penulisan yang lebih lambat dan file yang lebih besar tanpa keuntungan yang terlihat bagi pengguna. Periksa servernya sebelum memeriksa kodenya

Kenapa linearization meng-override UseXRefStream dan UseObjectStreams?

Karena writer linearized membutuhkan setiap objek punya offset byte yang bisa dialamatkan langsung sendiri-sendiri, dan kedua fitur itu menghilangkan hal tersebut. Karena itu HotPDF menerbitkan tabel cross-reference teks tradisional dan objek tak langsung yang tidak dipaketkan kapan pun LinearizeOutput diaktifkan, bahkan jika pemanggil juga mengatur UseXRefStream atau UseObjectStreams. Ini adalah override yang disengaja, bukan konflik yang harus Anda selesaikan sendiri

Alasannya mengikuti dari hint table. Hint table menggambarkan di mana bagian halaman mulai dan berapa panjangnya, sehingga reader bisa meminta persis rentang itu. Objek yang dipaketkan ke dalam kontainer /ObjStm sama sekali tidak punya offset independen; ia hanya ada sebagai irisan di dalam stream terkompresi lain yang harus diambil dan di-inflate sebagai satu unit. Jika Anda mengandalkan object stream untuk ukuran file, pahami bahwa linearization dan kompresi menarik ke arah berlawanan di sini, dan bacalah trade-off-nya di artikel pendamping tentang object stream dan incremental update di HotPDF. Ketegangan yang sama membentuk file hybrid-reference, yang ada persis untuk membuat reader lama tetap berfungsi berdampingan dengan tabel berbasis stream, sebagaimana dibahas di artikel tentang hybrid cross-reference stream pada PDF hasil Office

Ada juga batas versi minimum. Linearization membutuhkan PDF 1.2 atau lebih baru. Jika versi yang dipilih lebih lama, HotPDF menaikkannya secara otomatis, kecuali StrictVersionLock diatur, dalam hal mana penulisan memicu exception alih-alih diam-diam mempromosikan dokumen yang sengaja Anda pin

Batas 4 GiB, dan kenapa HotPDF menolak alih-alih memotong

Hint table linearization menyimpan offset sebagai nilai 32-bit, jadi file linearized tidak bisa mengalamati apa pun pada atau di atas 4 GiB, dan HotPDF menolak output semacam itu dengan exception eksplisit alih-alih menulis file dengan offset yang terlipat (wrapped). Batas ini bukan pilihan implementasi HotPDF; ini adalah lebar field yang didefinisikan Annex F

Pemeriksaannya diterapkan di tiga tempat, dan ketiganya penting. HotPDF memvalidasi setiap objek begitu panjang serialisasinya diketahui, memvalidasi panjang setiap bagian halaman saat membangun entri hint, dan memvalidasi panjang file akhir setelah tabel cross-reference utama diukur. Gagal lebih awal adalah keseluruhan intinya: hint table dengan offset yang diam-diam terpotong menghasilkan file yang terbuka dengan benar di viewer yang mengunduhnya utuh dan hanya gagal untuk klien byte-range yang justru menjadi tujuan linearization, yang merupakan mode kegagalan terburuk karena viewer test Anda tidak pernah mereproduksinya. Jika Anda menghasilkan output multi-gigabyte, linearization bukan alatnya, dan pendekatan streaming yang dijelaskan pada catatan Direct File API untuk alur kerja PDF besar adalah arah yang harus dilihat

Mendeteksi linearization pada file yang Anda muat

THotPDF.IsLoadedLinearized melaporkan apakah dokumen yang saat ini dimuat sudah ditulis dalam bentuk linearized, dan ia menjawab dari snapshot yang diambil sebelum parsing, bukan dari stream yang hidup. HotPDF membaca 1024 byte pertama dari posisi nol pada stream sumber, memindainya untuk keyword obj pertama lalu untuk entri /Linearized dengan nilai 1, dan meng-cache hasil boolean-nya

var
  PDF: THotPDF;
  PageCount: Integer;
begin
  PDF := THotPDF.Create(nil);
  try
    PageCount := PDF.LoadFromFile('incoming.pdf');
    if (PageCount > 0) and (not PDF.IsLoadedLinearized) then
      Writeln('Source is not Fast Web View ready');
  finally
    PDF.Free;
  end;
end;

Dua batasan dalam deskripsi itu bersifat menentukan. Deteksinya tidak bisa mengandalkan posisi stream, karena pada saat kode aplikasi menanyakan pertanyaan itu, parser sudah menggesernya, dan ia tidak bisa membaca ulang sesuai permintaan karena LoadFromFile melepaskan stream sumber internal begitu pemuatan selesai. Karena itulah desainnya menangkap-sebelum-parsing-lalu-cache. Pemindaiannya juga sengaja bersifat literal soal nilainya: hanya /Linearized 1 atau bentuk yang secara numerik ekuivalen dengan pecahan seluruhnya nol yang diterima, karena file yang dictionary parameternya menyatakan sesuatu yang lain tidak sedang membuat janji Annex F

Jebakan record Delphi yang layak dicontek

Record lokal yang berisi array dinamis menginisialisasi field managed-nya dan tidak lebih. Jika Anda menyimpan field Count biasa di samping array itu, Anda harus membersihkannya sendiri. Ini menggigit partisi linearization selama pengembangan, dan ini jenis bug yang menghabiskan satu hari persis karena satu platform menyembunyikannya

type
  THPDFLinearIndexList = record
    Values: THPDFIntegerArray;  // managed field: cleared for you
    Count: Integer;             // plain field: whatever was on the stack
  end;

// Required, not cosmetic:
Part4 := Default(THPDFLinearIndexList);
Part6 := Default(THPDFLinearIndexList);
Part8 := Default(THPDFLinearIndexList);
Part9 := Default(THPDFLinearIndexList);

Field array dinamis itu reference-counted, jadi compiler mengosongkannya. Count di sampingnya adalah integer biasa tanpa jaminan seperti itu, dan Count yang tidak terinisialisasi mengirim append pertama ke indeks yang sembarangan. Pada Win32, slot stack itu kebetulan berisi nol, append-nya mendarat di indeks 0, dan setiap test lolos. Pada Win64, kode yang sama menulis melewati akhir array. Pelajarannya berlaku jauh lebih luas dari sekadar linearization: ketika sebuah record mencampur field managed dan unmanaged, tetapkan Default(TRecord) dan berhenti menalar field mana yang dicakup compiler, dan jangan pernah memperlakukan test Win32 yang hijau sebagai bukti bahwa inisialisasinya benar

Anggota LinearizeOutput dan IsLoadedLinearized yang dijelaskan di sini hadir dalam HotPDF Component standar untuk Delphi dan C++Builder; halaman produknya membawa referensi properti lengkap, termasuk aturan interaksi dengan cross-reference stream, object stream dan version locking