Artikel Teknis

Object Stream dan Cross-Reference Stream PDF di Delphi

Object stream PDF 1.5 memadatkan banyak indirect object kecil ke dalam satu container terkompresi Flate, dan losLab PDF Library menerbitkannya saat full save lewat flag PackObjectStreams. Keuntungannya nyata: ratusan dictionary halaman, font, dan anotasi yang masing-masing menghabiskan puluhan byte tak terkompresi menyusut menjadi segelintir blob terkompresi. Biayanya adalah setiap packed object kini membutuhkan cross-reference stream untuk mendeskripsikannya

Bagian kedua itulah yang membuat banyak writer gagal. Membangun container /ObjStm hanyalah aritmatika; mengajarkan mesin cross-reference untuk menunjuk ke dalamnya adalah sebuah redesain. Writer yang menghasilkan container yang sepenuhnya valid lalu mendeskripsikan member-nya dengan offset type-1 biasa telah menghasilkan berkas yang akan dibuka Acrobat hanya cukup lama untuk menyatakannya rusak. Kedua fitur ini sebenarnya satu fitur, dan artikel ini membahas sisi penulisan keduanya, sebagaimana didefinisikan dalam ISO 32000-1 §7.5.7 dan §7.5.8

Apa yang sebenarnya berisi di dalam container ObjStm

Object stream adalah stream yang byte hasil dekodenya terdiri dari dua region yang digabung, dan ISO 32000-1 §7.5.7 memberi dictionary itu tepat tiga key yang penting untuk konstruksinya. /Type /ObjStm mengidentifikasinya, /N memberi jumlah member, dan /First memberi panjang byte dari region header — setara dengan offset tempat body dimulai. Header berupa pasangan object number dan offset yang dipisah whitespace; body adalah member yang diserialisasi berurutan, dengan setiap offset diukur dari awal body, bukan dari awal payload hasil dekode. Membaca container yang sudah sepenuhnya didekode membuatnya jelas: di bawah ini, /First bernilai 14 karena tiga baris header menempati empat belas byte, dan objek 7 berada di posisi 55 byte dalam body karena objek 4 terserialisasi menjadi 54 karakter ditambah satu separator

// Decoded payload of: 12 0 obj << /Type /ObjStm /N 3 /First 14
//                        /Filter /FlateDecode /Length 118 >> stream
4 0
7 55
9 90
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
<< /Type /ExtGState /CA 1 /ca 1 >>
[ 0 0 595 842 ]

Ada dua aturan keanggotaan yang mutlak dan keduanya langsung berasal dari §7.5.7. Sebuah stream object tidak pernah boleh menjadi member, karena stream membawa byte mentah yang harus disarangkan di dalam stream lain. Dan sebuah member harus berupa nilai objek yang lengkap, tidak pernah berupa indirect reference telanjang — objek terkompresi yang hanya berupa 5 0 R menciptakan indireksi yang tidak bisa diresolusi pembaca tanpa sudah tahu ke mana ia menunjuk. losLab PDF Library menyaring kedua kasus ini saat pengumpulan kandidat, bersama encryption dictionary dan objek 0, lalu memadatkan apa pun yang tersisa dalam kelompok 200 per container. Batas itu adalah keputusan random-access, bukan batasan spesifikasi: pembaca yang hanya ingin satu member harus meng-inflate seluruh container, sehingga container yang terlalu besar membuat lookup kecil jadi mahal

Kenapa member ObjStm harus memakai entri cross-reference type-2?

Karena packed object tidak memiliki file offset untuk dicatat. ISO 32000-1 §7.5.8 menjawab ini dengan tiga tipe entri dalam cross-reference stream biner: type 0 untuk objek bebas, type 1 untuk objek in-use biasa yang disimpan pada suatu byte offset, dan type 2 untuk objek terkompresi, yang dua field datanya menyimpan container object number dan indeks member di dalamnya. Tidak ada cara untuk menyatakan packed object dalam tabel xref plaintext klasik, dan itulah tepatnya kenapa PDF 1.5 memperkenalkan kedua fitur ini bersamaan

Urutan yang mengikuti ini menjegal hampir setiap implementasi pertama, termasuk milik kami. Objek biasa mendapat entri type-1. Container /ObjStm sendiri juga mendapat entri type-1, karena sebuah container adalah indirect stream object yang sepenuhnya normal dan ditulis pada offset yang sungguhan. Hanya member yang mendapat entri type-2. Dan cross-reference stream itu sendiri adalah indirect object dalam berkas, sehingga ia butuh entri type-1 miliknya sendiri yang menunjuk ke offset tempat ia baru saja ditulis — offset yang sama yang dicatat startxref. Versi awal writer kami mengecualikan nomor objek container dari write loop, bukan mengecualikan member, dan hasilnya adalah berkas dengan cross-reference stream tapi tanpa object stream sama sekali: koheren secara struktural, kosong secara semantik, ditolak di hilir. Nilai /Size menyembunyikan off-by-one yang serupa, karena nilainya adalah nomor objek tertinggi ditambah satu, dan cross-reference stream sendiri dialokasikan sebagai nomor objek tertinggi, sehingga ia juga harus dihitung

Mengukur array /W: kenapa empat byte tidak cukup

Array /W mendeklarasikan lebar byte masing-masing dari tiga field, dan losLab PDF Library menuliskannya sebagai /W [1 Field2 Field3] dengan field 1 tetap satu byte untuk kode tipe dan field 3 tetap dua byte, yang mencakup nomor generation hingga 65535 sekaligus indeks member. Field 2 adalah yang tidak bisa dibuat konstan, karena ia membawa dua besaran yang tidak berhubungan: pada entri type-1 ia adalah byte offset yang hanya dibatasi ukuran berkas, sementara pada entri type-2 ia adalah container object number, dan pada entri type-0 ia adalah objek bebas berikutnya dalam rantai. Field 2 dengan empat byte tetap bekerja baik-baik saja sampai berkas melewati 4 GB, di titik mana setiap offset yang melewati batas itu terpotong secara diam-diam dan seluruh tabel menjadi sampah. Karena itu writer memindai tabel yang sudah disusun untuk mencari nilai terbesar yang mungkin dipegang slot field-2 mana pun, termasuk offset dari cross-reference stream itu sendiri, dan melebarkan field hingga delapan byte

// Field 2 must hold the largest byte offset AND the largest
// ObjStm container number AND the largest free-chain target.
MaxField2Value := XRefStart;
for X := 0 to MaxObj do
begin
  if XRefTable[X].InUse and (XRefTable[X].ObjStrNum > 0) then
    Field2Value := XRefTable[X].ObjStrNum   // type-2: container number
  else
    Field2Value := XRefTable[X].ObjPos;     // type-1 offset / type-0 next-free
  if Field2Value > MaxField2Value then
    MaxField2Value := Field2Value;
end;

Field2 := 4;
while (Field2 < 8) and
      (MaxField2Value > ((Int64(1) shl (Field2 * 8)) - 1)) do
  Inc(Field2);
Field3 := 2;   // generation numbers and member indices both fit

Setelah lebar field diketahui, ukuran payload diketahui secara persis, sehingga writer mengalokasikan sebelumnya seluruh buffer dan mengisinya lewat indeks; menambahkan entri byte demi byte ke sebuah AnsiString membuat konstruksi tabel jadi kuadratik, yang tidak disadari siapa pun pada invoice sepuluh halaman dan disadari semua orang pada dokumen dengan dua ratus ribu objek. Dua detail lagi menjaga agar pembaca yang ketat tetap puas. /Index mendeklarasikan rentang nomor objek mana yang dicakup tabel, dan untuk penulisan ulang penuh itu sekadar [0 N] tanpa celah. Dan setiap slot yang sebenarnya tidak diterbitkan writer harus default ke bebas, bukan in-use: objek 0 mengepalai rantai bebas, setiap slot bebas ditautkan ke berikutnya, dan slot yang pernah menampung objek terhapus menjaga nomor generation-nya dinaikkan satu. Catatan pendamping tentang keamanan memori saat mengurai PDF yang tidak dipercaya membuat argumen batas yang sama dari sisi pembacaan

Kenapa cross-reference stream tidak boleh pernah dienkripsi?

Karena pembaca harus mengurainya lebih dulu sebelum tahu cara mendekripsi apa pun. Cross-reference stream itulah yang memberi tahu pembaca di mana dictionary /Encrypt berada; jika byte-nya sendiri terenkripsi, pembaca akan membutuhkan file key untuk menemukan objek yang mendeskripsikan file key itu sendiri. losLab PDF Library menegakkan ini lewat satu predikat tunggal: ShouldCryptStreamData mengembalikan False setiap kali stream dictionary membawa /Type /XRef, sehingga pengecualian ini berlaku apa pun jalur yang mencapai serializer

Container /ObjStm mendapat perlakuan sebaliknya, dan asimetri ini disengaja. Sebuah container dienkripsi secara utuh, dengan kunci berdasarkan nomor objeknya sendiri, persis seperti stream lainnya. Member-nya tidak dienkripsi satu per satu — mereka dipadatkan dalam bentuk plaintext hasil dekripsinya, dan satu pass tunggal atas container yang sudah disusun mencakup mereka semua, termasuk string. Mengenkripsi member dua kali menghasilkan berkas yang saat didekripsi berubah menjadi ciphertext, dan karena layer luarnya berhasil, kegagalannya muncul sebagai parse error jauh di dalam object graph, bukan sebagai kegagalan autentikasi. Ada satu objek yang sepenuhnya berada di luar skema ini: pada dokumen terenkripsi, Catalog tetap disimpan sebagai direct type-1 object dan tidak pernah dipadatkan, karena memadatkannya akan memaksa loader untuk meng-inflate dan mendekripsi sebuah object stream demi mencapai document root, padahal konteks dekripsi yang justru dibantu dibangun oleh root itu belum sepenuhnya terbentuk

Mengaktifkan packing dari Delphi

Sakelar publiknya adalah PackObjectStreams, yang diekspos sebagai field pada TPDFlibSaveOptions, sebagai setter mandiri SetPackObjectStreams, dan sebagai property pada objek dokumen. Defaultnya aktif dan dibatasi otomatis berdasarkan versi: writer hanya memadatkan bila dokumen sudah PDF 1.5 atau lebih baru, dan ia memanggil penjaga versi-minimum internal sehingga dokumen yang dipadatkan dinaikkan ke 1.5, bukan malah salah label. Setelah save, GetLastSaveUsedObjectStreams melaporkan apakah gerbang itu benar-benar terbuka, dan itulah assertion yang Anda inginkan dalam regression test, bukan perbandingan ukuran byte

var
  Doc: TPDFlib;
  Options: TPDFlibSaveOptions;
begin
  Doc := TPDFlib.Create;
  try
    if Doc.LoadFromFile('report.pdf', '') <= 0 then
      Exit;

    Doc.SetInformation(0, '1.5');        // packing is gated on PDF 1.5+

    FillChar(Options, SizeOf(Options), 0);
    Options.CompressContent    := True;
    Options.GarbageCollect     := True;  // drop orphans before packing
    Options.PackObjectStreams  := True;

    if Doc.SaveToFileOptions('report-packed.pdf', Options) = 1 then
      if Doc.GetLastSaveUsedObjectStreams = 1 then
        Writeln('Saved with ObjStm containers and an xref stream');
  finally
    Doc.Free;
  end;
end;

Urutan penting antara packing dan garbage collection. Analisis reachability harus berjalan lebih dulu, karena member yang bertahan masuk ke sebuah container menyeret container itu ikut bertahan — jika objek yang hidup dipadatkan, nomor container-nya menjadi reachable menurut definisi, dan menyapu container itu akan mendamparkan member tanpa cara untuk menemukannya. Menjalankan collector lebih dulu juga berarti objek mati tidak pernah masuk ke sebuah container sama sekali, dan dari situlah keuntungan ukuran yang bertumpuk berasal. Packing melengkapi tuas ukuran lainnya, bukan menggantikannya; penjelasan lengkap di optimasi ukuran berkas PDF dan font subsetting membahas tuas yang bekerja pada payload stream, sedangkan object stream bekerja pada struktur

Batasan yang perlu diketahui sebelum mengaktifkannya

Incremental save tidak pernah memadatkan apa pun. Incremental update menambahkan objek baru dan cross-reference section baru sambil membiarkan revisi sebelumnya tetap utuh secara fisik, sehingga memadatkan ulang objek yang sudah ada ke dalam container baru akan mendamparkan entri type-1 yang masih direferensikan oleh revisi sebelumnya; losLab PDF Library menonaktifkan packing setiap kali mode append aktif, dan artikel tentang incremental update dan streaming mode-append membahas jalur itu secara lengkap. Dokumen di bawah PDF 1.5 tetap mempertahankan tabel cross-reference plaintext tanpa syarat: consumer versi 1.4 tidak tahu apa arti /ObjStm, dan menaikkan versi dokumen secara diam-diam hanya karena writer lebih memilih berkas yang lebih kecil adalah trade-off yang salah untuk dibuat atas nama caller. Satu key opsional yang sengaja tidak kami terbitkan adalah /Extends, yang didefinisikan ISO 32000-1 §7.5.7 agar sebuah container bisa menyebut pendahulunya dan pembaca bisa memperlakukan rantai container sebagai satu kelompok logis. Key ini benar-benar opsional, setiap container yang kami tulis bersifat self-contained dan bisa didekode secara independen, dan melewatkannya menghilangkan satu kelas bug siklus dan dangling-reference dari writer — meski tentu saja pembaca tetap harus menghormati /Extends ketika menjumpainya di berkas dari producer lain

Object-stream packing dan output cross-reference stream hadir sebagai bagian dari losLab PDF Library untuk Delphi dan C++Builder, berdampingan dengan garbage collector dan content-stream optimizer yang berpadu dengannya; halaman produk memuat referensi lengkap save-options