Lewati ke konten

Aliran dan filter

Evidence: Standard-backed

Sebagian besar byte dalam PDF sebenarnya berada di dalam aliran (stream): konten halaman, fon, gambar, dan aliran referensi-silang itu sendiri. Nyaris tidak ada byte tersebut yang disimpan mentah; semuanya melewati satu atau beberapa filter terlebih dahulu. Halaman ini membahas filter apa saja yang akan Anda temui, kegunaan masing-masing, di mana masalah biasanya muncul, dan mengapa NextPDF mengunci kompresinya agar masukan yang sama selalu menghasilkan byte yang sama.

Aliran beserta filternya adalah kontrak: “byte ini dikompresi dengan deflate, lalu dikodekan base-85 — dekode dengan urutan itu untuk mendapatkan data yang sebenarnya.” Jika entri /Filter tidak sesuai dengan isi byte yang sebenarnya, atau /Length salah, atau dua filter dicantumkan dengan urutan yang keliru, aliran tidak dapat didekode dan objek yang dibawanya hilang. Pembaca tidak menebak secara heuristik; ia melakukan apa yang diperintahkan oleh kamus.

Ada konsekuensi kedua yang lebih tersembunyi. Jika kompresor sebuah pustaka tidak deterministik — build zlib yang berbeda, level yang berbeda, batas blok internal yang berbeda — maka dua eksekusi yang seharusnya menghasilkan PDF identik justru menghasilkan dua berkas yang berbeda. Hal itu merusak reproduksibilitas pada tingkat byte. Reproduksibilitas yang rusak kemudian merusak pengujian golden-file, verifikasi signed-build, dan pipeline apa pun yang membandingkan keluaran. Filter menentukan apakah PDF valid dan apakah PDF sama persis.

  • Sebuah objek aliran adalah sebuah kamus ditambah sebuah blok byte, yang dibungkus dalam streamendstream, dengan sebuah /Length dan biasanya sebuah /Filter.
  • Entri /Filter menamai filter pendekode — atau sebuah larik filter yang diterapkan sebagai pipeline, secara berurutan.
  • Filter terbagi menjadi dua keluarga: kompresi (FlateDecode, LZWDecode, RunLengthDecode, DCTDecode, JPXDecode, JBIG2Decode) dan transport ASCII (ASCIIHexDecode, ASCII85Decode), ditambah filter Crypt khusus untuk enkripsi.
  • Yang paling sering Anda lihat adalah FlateDecode — zlib/deflate. Filter ini menjadi standar untuk konten, fon, dan aliran referensi-silang.
  • NextPDF mengunci keluaran Flate-nya pada level dan format tetap sehingga byte masukan yang sama selalu terkompresi menjadi byte keluaran yang sama.

NextPDF menulis objek aliran melalui satu helper buffer dan mengompresi melalui satu kompresor yang dikunci — secara sengaja.

BinaryBuffer::writeStream() (src/Support/BinaryBuffer.php) membungkus konten aliran dalam kamusnya, selalu menuliskan /Length yang sama dengan panjang byte sebenarnya dan menggabungkan entri tambahan apa pun yang disediakan pemanggil, seperti /Filter. Tidak ada jalur yang dapat membuat panjang terdeklarasi tidak sesuai dengan byte yang ditulis, karena panjang tersebut diambil dari string konten itu sendiri.

Kompresi berlangsung melalui PinnedZlibCompressor (src/Writer/PinnedZlibCompressor.php). Kelas ini dibuat untuk satu alasan. gzcompress tanpa level eksplisit mengikuti default runtime zlib, yang secara historis bervariasi antar build. Header zlib 2-byte bahkan mengodekan level secara tidak langsung, sehingga “default” bukanlah keluaran yang stabil. Kompresor mengunci level pada maksimum RFC 1951 dan selalu mengeluarkan deflate yang dibungkus zlib (header RFC 1950 + trailer Adler-32), yang persis seperti yang diharapkan oleh /Filter /FlateDecode. Kegagalan keras dari zlib diubah menjadi eksepsi bertipe, bukan fallback diam-diam ke keluaran yang tidak terkompresi — sebuah aliran tidak pernah ditulis mentah secara diam-diam.

Aliran referensi-silang itu sendiri adalah contoh penerapan semua hal ini: CrossReferenceStream (src/Core/CrossReferenceStream.php) membangun tabel biner, mengompresinya, dan menulisnya sebagai objek aliran dengan /Type /XRef, larik lebar-bidang /W, dan /Filter /FlateDecode. Indeks yang memungkinkan pembaca menemukan setiap objek itu sendiri merupakan aliran yang difilter.

FilterKeluargaKegunaannyaDi mana ia keliru
FlateDecodeKompresizlib/deflate; standar untuk konten, fon, aliran xrefBuild zlib yang tidak deterministik membuat PDF yang tampak “identik” berbeda byte demi byte
LZWDecodeKompresiKompresi Lempel–Ziv–Welch yang lebih lawasWarisan format lama; digantikan oleh Flate, sesekali masih terlihat di berkas lama
DCTDecodeKompresiGambar warna/grayscale berkode JPEGLossy — mengodekan ulang gambar yang sudah berkode DCT kembali menurunkan kualitasnya
JPXDecodeKompresiData gambar wavelet JPEG 2000Tidak diizinkan oleh sebagian profil arsip; dukungan luasnya tidak merata
JBIG2DecodeKompresiKompresi gambar bilevel (1-bit)Tidak boleh digunakan dengan gambar inline; mode lossy dapat mengubah hasil pemindaian
RunLengthDecodeKompresiRun-length berorientasi byteHanya membantu data dengan deret byte tunggal yang panjang; dapat memperbesar data lainnya
ASCIIHexDecodeTransportBiner sebagai digit heksadesimalMenggandakan ukuran; hanya untuk kanal aman-7-bit, tidak pernah untuk mengecilkan ukuran
ASCII85DecodeTransportBiner sebagai ASCII base-85Overhead ~25%; memudahkan transport, bukan kompresi
CryptKeamananMenerapkan penangan keamanan dokumenAliran referensi-silang tidak boleh menggunakan filter Crypt

Kumpulan filter standar PDF menurut keluarga, beserta kegagalan yang relevan untuk masing-masing. NextPDF menulis FlateDecode untuk konten, fon, dan aliran referensi-silang; filter transport ASCII ditujukan untuk kanal 7-bit, bukan untuk mengurangi ukuran.

Mekanisme filter didefinisikan oleh Spec: ISO 32000-2, §7.4 . Sebuah kamus aliran menamai filternya melalui /Filter. Jika entri tersebut mencantumkan lebih dari satu filter, filter-filter itu membentuk pipeline pendekodean dan diterapkan secara berurutan. Penulis mengodekan aliran untuk mengompresinya atau untuk membuatnya aman-7-bit. Pembaca menjalankan filter pendekode yang sesuai untuk memulihkan data asli. Evidence: Standard-backed

Tabel filter standar mengklasifikasikan setiap filter. FlateDecode mendekompresi data yang dienkode zlib/deflate, mereproduksi teks atau data biner asli. DCTDecode mereproduksi sampel gambar yang mendekati aslinya melalui JPEG — kata “mendekati” adalah cara standar menyatakan bahwa proses ini lossy. LZWDecode, RunLengthDecode, JBIG2Decode, JPXDecode, dan filter Crypt juga didefinisikan di sana, dengan JBIG2 secara eksplisit dilarang untuk gambar inline.

Aliran referensi-silang menerapkan mekanisme format yang sama pada dirinya sendiri: ia adalah objek aliran (/Type /XRef, Spec: ISO 32000-2, §7.5.8 ) yang larik /W-nya menyatakan lebar byte setiap bidang entri di dalam aliran yang telah didekode. Standar mengharuskan aliran ini tidak dienkripsi dan tidak menggunakan filter Crypt. CrossReferenceStream milik NextPDF mengikuti ketentuan ini secara persis — FlateDecode, /W eksplisit, tanpa enkripsi.

Berikut sebuah aliran konten halaman yang dikompresi dengan Flate. Ini bentuk yang sangat lumrah: sebuah kamus dengan /Length dan /Filter, lalu byte terkompresi di antara stream dan endstream.

<?php
declare(strict_types=1);
use NextPDF\Writer\PinnedZlibCompressor;
// The marking operators a page content stream carries, uncompressed.
$content = "BT /F1 12 Tf 72 712 Td (Hello) Tj ET\n";
// NextPDF compresses through the pinned compressor: fixed level,
// fixed zlib-wrapped format. The same $content always yields the
// same $compressed bytes, on any supported PHP/zlib build.
$compressed = PinnedZlibCompressor::compress($content);
// Emitted as a stream object. /Length is the real byte length of
// $compressed; /Filter names the decode the reader must apply.
// N 0 obj
// << /Length <strlen($compressed)> /Filter /FlateDecode >>
// stream
// <$compressed bytes>
// endstream
// endobj

Pembaca melakukan proses sebaliknya: membaca /Length byte, menjalankannya melalui FlateDecode karena /Filter mengatakan demikian, dan mendapatkan kembali operator aslinya. Saat kompresornya dikunci, perjalanan bolak-balik itu tidak hanya benar. Hasilnya identik setiap kali, yang menjadi dasar pemeriksaan golden-file dan signed-build.

Jebakannya adalah memperlakukan filter ASCII sebagai kompresi. ASCIIHexDecode dan ASCII85Decode membuat sebuah aliran lebih besar — yang satu kira-kira dua kali lipat, yang lain kira-kira 25%. Keduanya ada untuk memindahkan data biner melalui kanal yang hanya aman untuk teks 7-bit, bukan untuk menghemat ruang. Memilih ASCII85 untuk “mengecilkan” sebuah PDF justru melakukan sebaliknya. Separuh kedua dari kesalahpahaman yang sama adalah meyakini bahwa FlateDecode bersifat lossless untuk gambar begitu saja. Flate memang lossless, tetapi jika gambar sudah berkode DCT (JPEG), membungkusnya lagi atau mentranskodekannya melalui filter lossy akan menurunkan kualitasnya, terlepas dari apa pun yang dilakukan Flate di sekitarnya. Pipeline filter mempertahankan persis apa yang Anda masukkan ke dalamnya — termasuk artefak rekompresi yang Anda masukkan secara tidak sengaja.

Halaman ini membahas bagaimana filter dideklarasikan dan diterapkan, bukan algoritma tingkat bit di dalam masing-masingnya. Jaminan determinisme secara khusus berlaku untuk keluaran Flate NextPDF bagi aliran yang ditulisnya. Jaminan ini berlaku di seluruh versi minor PHP dan build zlib yang sesuai standar, tetapi standar secara eksplisit mengizinkan pengode deflate memilih batas blok internal yang berbeda, sehingga keluaran yang identik pada tingkat byte di seluruh implementasi zlib yang benar-benar berbeda (misalnya zlib bawaan versus zlib-ng) tidak dijanjikan. Lingkungan build dikunci karena alasan itu.

NextPDF memilih FlateDecode dan filter transport ASCII untuk data yang dikeluarkannya. Ia bukan transkoder gambar. Ia tidak menjanjikan akan mengemas ulang sembarang aliran JPEG2000 atau JBIG2 yang masuk, dan kompromi lossy pada gambar adalah sifat data sumber, bukan sesuatu yang dapat dibatalkan oleh penulis.

Mengapa FlateDecode ada di mana-mana? Ia lossless, serbaguna, didukung dengan baik, dan cocok untuk konten teks-dan-operator pada sebagian besar PDF. Filter ini merupakan pilihan standar yang aman untuk aliran konten, fon tertanam, dan aliran referensi-silang.

Bisakah saya mematikan kompresi? Anda dapat menghilangkan /Filter dan menyimpan byte mentah, dan pembaca akan menerimanya. Berkas menjadi lebih besar dan tidak ada hal lain yang menjadi lebih baik; jarang ada alasan di luar proses debugging.

Mengapa mengunci level kompresi sama sekali? Agar keluarannya dapat direproduksi. Level yang tidak dikunci (atau build zlib) dapat mengubah byte terkompresi tanpa mengubah konten yang terdekompresi — tetap benar, tetapi tidak identik, yang menggagalkan verifikasi tingkat-byte.

  • Objek aliran — sebuah kamus ditambah sebuah blok byte di antara stream dan endstream, yang membawa sebuah /Length dan biasanya sebuah /Filter.
  • Filter — transformasi pendekodean bernama yang diterapkan pembaca pada byte sebuah aliran (misalnya FlateDecode).
  • Pipeline filter — sebuah larik filter yang diterapkan secara berurutan; urutan larik adalah urutan pendekodean.
  • FlateDecode — filter zlib/deflate; kompresi standar untuk konten, fon, dan aliran referensi-silang.
  • DCTDecode — filter gambar JPEG; lossy, sehingga pengodean ulang akan menurunkan kualitas gambar lagi.
  • Filter transport ASCII — ASCIIHexDecode / ASCII85Decode; membuat data aman-7-bit dengan ukuran yang lebih besar — bukan kompresi.
  • Kompresi deterministik — menghasilkan keluaran terkompresi yang identik byte untuk masukan yang identik, dicapai dengan mengunci level dan format kompresor.