跳到內容

串流與篩選器

Evidence: Standard-backed

真實 PDF 的大部分位元組都在 串流 裡:頁面內容、字型、影像,以及交互參照串流本身。這些位元組幾乎都不是以原始形式儲存,而是會先通過一個或多個 篩選器。本頁說明你會遇到哪些篩選器、它們各自做什麼、常在哪裡惹出麻煩,以及 NextPDF 為何要固定壓縮設定,讓相同輸入永遠產生相同位元組。

串流及其篩選器是一份契約:「這些位元組先經過 deflate 壓縮,再經過 base-85 編碼——請依這個順序解碼,取得真正的資料。」如果 /Filter 項目與實際位元組內容不一致,或 /Length 有誤,或兩個篩選器的順序列錯,串流便無法解碼,它所承載的物件也就遺失了。讀取器不會用啟發式方式猜測;它只會照字典所說的執行。

另外還有一個較不顯眼的代價。如果某個函式庫的壓縮器不是確定性的——不同的 zlib 組建、不同的等級、不同的內部區塊邊界——那麼原本應該產生相同 PDF 的兩次執行,便會產生兩個不同檔案。這會破壞位元組層級的可重現性;可重現性一旦被破壞,連帶也會破壞 golden-file 測試、簽署組建驗證,以及任何會對輸出做差異比對的流程。篩選器同時決定 PDF 是否正確,以及 PDF 是否 相同

  • 串流物件 是由字典加上一段位元組區塊組成,包在 streamendstream 之間,並帶有一個 /Length,通常還有一個 /Filter
  • /Filter 項目指定解碼篩選器——或指定一個 陣列,以 管線 方式依序套用多個篩選器。
  • 這些篩選器分成兩大類:壓縮(FlateDecode、LZWDecode、RunLengthDecode、DCTDecode、JPXDecode、JBIG2Decode)與 ASCII 傳輸(ASCIIHexDecode、ASCII85Decode),外加用於加密的特殊 Crypt 篩選器。
  • 你最常見到的是 FlateDecode——zlib/deflate。它是內容、字型與交互參照串流的預設篩選器。
  • NextPDF 將 Flate 輸出固定在特定等級與格式,讓相同輸入位元組永遠壓縮成相同輸出位元組。

NextPDF 透過單一緩衝區輔助方法輸出串流物件,並以單一固定壓縮器進行壓縮——這是刻意的設計。

BinaryBuffer::writeStream()src/Support/BinaryBuffer.php)會把串流內容包進字典中,永遠寫入一個等於實際位元組長度的 /Length,並合併呼叫端提供的任何額外項目,例如 /Filter。沒有任何途徑會讓宣告長度與寫入位元組不一致,因為長度直接取自內容字串本身。

壓縮會透過 PinnedZlibCompressorsrc/Writer/PinnedZlibCompressor.php)進行。這個類別只為一個原因存在。未指定明確等級的 gzcompress 會沿用 zlib 執行階段預設值,而這個預設值在歷來不同組建之間並不一致。那 2 個位元組的 zlib 標頭甚至會間接編碼出等級,因此「預設值」不是穩定輸出。這個壓縮器將等級固定為 RFC 1951 的最高值,並永遠輸出 zlib 包裝的 deflate(RFC 1950 標頭加上 Adler-32 結尾),這正是 /Filter /FlateDecode 所預期的形式。zlib 的硬性失敗會成為型別化例外,而非靜默退回未壓縮輸出——串流絕不會被悄悄以原始形式輸出。

交互參照串流本身就是上述做法的實作範例:CrossReferenceStreamsrc/Core/CrossReferenceStream.php)會建立一個二進位表格、加以壓縮,並輸出為帶有 /Type /XRef/W 欄位寬度陣列,以及 /Filter /FlateDecode 的串流物件。那個讓讀取器能找到每個物件的索引,本身就是經過篩選的串流。

篩選器類別用途在哪裡出錯
FlateDecode壓縮zlib/deflate;內容、字型、xref 串流的預設值非確定性的 zlib 組建會讓「相同」PDF 在位元組層級彼此不同
LZWDecode壓縮較舊的 Lempel–Ziv–Welch 壓縮舊式格式;已被 Flate 取代,偶爾仍見於舊檔案
DCTDecode壓縮JPEG 編碼的 colour/grayscale 影像有損——對已是 DCT 的影像重新編碼,會再次造成劣化
JPXDecode壓縮JPEG 2000 小波影像資料某些封存設定檔不允許;廣泛支援程度參差不齊
JBIG2Decode壓縮雙階(1 位元)影像壓縮不得用於內嵌影像;有損模式可能會改變掃描影像
RunLengthDecode壓縮以位元組為單位的行程長度編碼只對含有長串單一位元組的資料有幫助;對其他資料反而可能 增大
ASCIIHexDecode傳輸以十六進位數字表示的二進位資料使大小加倍;僅用於 7 位元安全通道,絕不是為了縮小體積
ASCII85Decode傳輸以 base-85 ASCII 表示的二進位資料約 25% 的額外開銷;是傳輸上的便利手段,而非壓縮
Crypt安全性套用文件的安全性處理常式交互參照串流 不得 使用 Crypt 篩選器

PDF 標準篩選器集合,依類別排列,並標註各自所對應的失敗模式。NextPDF 為內容、字型與交互參照串流寫入 FlateDecode;ASCII 傳輸篩選器用於 7 位元通道, 絕不用於縮減體積。

篩選器機制由 Spec: ISO 32000-2, §7.4 定義。串流的篩選器由其字典中的 /Filter 項目指定,而且篩選器 可以串接成一條管線,讓串流依序通過兩個以上的解碼轉換。標準本身的範例是先 LZW、再 ASCII base-85,並依此順序解碼。寫入器會對串流進行編碼,是為了壓縮它,或讓它成為 7 位元安全。讀取器則呼叫對應的解碼篩選器,還原出原始資料。 Evidence: Standard-backed

標準的篩選器表格會為每個篩選器分類。FlateDecode 會解壓縮 zlib/deflate-encoded 資料,還原出原始文字或二進位資料。DCTDecode 會透過 JPEG 還原出 近似 原始影像的取樣值——「近似」正是標準在告訴你它是有損的。LZWDecode、RunLengthDecode、JBIG2Decode、JPXDecode 與 Crypt 篩選器也都在這裡定義,其中 JBIG2 明確禁止用於內嵌影像。

交互參照串流把格式本身的機制套用在自己身上:它是一個串流物件(/Type /XRef Spec: ISO 32000-2, §7.5.8 ),其 /W 陣列指明了 解碼後串流中 每個項目欄位的位元組寬度。標準要求它不得加密,也不得使用 Crypt 篩選器。 NextPDF 的 CrossReferenceStream 完全照此辦理——FlateDecode、 明確的 /W,不加密。

一個以 Flate 壓縮的頁面內容串流。這是極常見的形態:一個帶有 /Length/Filter 的字典,接著是 streamendstream 之間的壓縮位元組。

<?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

讀取器則反向操作:讀取 /Length 個位元組,因為 /Filter 如此指定,便讓它們通過 FlateDecode,取回原始運算子。固定壓縮器之後,這趟往返不只是正確而已;它每一次都 完全相同,也正是 golden-file 與簽署組建檢查所仰賴的性質。

陷阱在於把 ASCII 篩選器誤認為壓縮。ASCIIHexDecode 與 ASCII85Decode 會讓串流 變大——分別大約增大一倍與大約 25%。它們存在的目的,是把二進位資料送過只對 7 位元文字安全的通道,而不是節省空間。選用 ASCII85 來「縮小」PDF 只會適得其反。同一個誤解的另一面,是相信 FlateDecode 能對影像「免費」做到無損。Flate 確實 無損,但如果該影像原本就已是 DCT(JPEG)編碼,再包一層或讓它通過有損篩選器轉碼,都會使其劣化,不管外層 Flate 做了什麼。篩選器管線會原封不動保留你餵給它的東西——包括你不小心餵進去的重新壓縮痕跡。

本頁說明篩選器如何宣告與套用,而不是各個篩選器內部的位元層級演算法。這項確定性保證,明確只針對 NextPDF 寫入串流時產生的 Flate 輸出。它在 PHP 次版本與符合標準的 zlib 組建之間都成立,但標準明確允許 deflate 編碼器選擇不同的內部區塊邊界,因此跨 真正不同的 zlib 實作(例如原版 zlib 與 zlib-ng)產生位元組相同的輸出,並不在保證範圍內。組建環境正是為此而固定。

NextPDF 會為它輸出的資料選用 FlateDecode 與 ASCII 傳輸篩選器。它不是影像轉碼器,也未承諾要重新封裝任意傳入的 JPEG2000 或 JBIG2 串流;有損影像的取捨是來源資料的固有屬性,並非寫入器能逆轉。

為什麼到處都是 FlateDecode? 它無損、通用、支援良好,而且很適合大多數 PDF 中那種文字加運算子的內容。它是內容串流、內嵌字型與交互參照串流的安全預設值。

我可以關閉壓縮嗎? 你可以省略 /Filter,直接儲存原始位元組,讀取器也會接受。檔案會變大,其他方面沒有改善;除了除錯之外,很少有理由這麼做。

為什麼非要固定壓縮等級? 為了讓輸出可重現。未固定的等級(或 zlib 組建)可能在不改變解壓縮內容的情況下改變壓縮後的位元組——結果正確,但並非 完全相同,這會破壞位元組層級的驗證。

  • 串流物件——一個字典加上一段位於 streamendstream 之間的位元組區塊,帶有一個 /Length,通常也有一個 /Filter
  • 篩選器——讀取器套用到串流位元組上的具名解碼轉換(例如 FlateDecode)。
  • 篩選器管線——一個依序套用的篩選器陣列;陣列順序就是解碼順序。
  • FlateDecode——zlib/deflate 篩選器;內容、字型與交互參照串流的預設壓縮。
  • DCTDecode——JPEG 影像篩選器;有損,因此重新編碼會再次使影像劣化。
  • ASCII 傳輸篩選器——ASCIIHexDecode/ASCII85Decode;以體積為代價讓資料變成 7 位元安全——不是壓縮。
  • 確定性壓縮——對相同輸入產生位元組相同的壓縮輸出,透過固定壓縮器的等級與格式來達成。