串流与筛选器
ISO 32000-2 §7.4 Evidence: Standard-backed
快速概览
标题为“快速概览”的章节真实 PDF 的大部分字节都位于 串流 中:页面内容、字体、图像,以及交互参照串流本身。这些字节几乎都不会以原始形式保存,而是会先通过一个或多个 筛选器。本页说明你会遇到哪些筛选器、它们各自的用途、容易在哪里出问题,以及 NextPDF 为什么要固定压缩设置,以便相同输入始终产生相同字节。
为什么这很重要
标题为“为什么这很重要”的章节串流及其筛选器是一份契约:「这些字节先经过 deflate 压缩,再经过 base-85 编码——请按这个顺序解码,才能得到真正的数据。」如果 /Filter 项与实际字节内容不一致,或 /Length 有误,或两个筛选器的顺序串错,串流就无法解码,它所承载的对象也会丢失。读取器不会用启发式方式猜测;它只会按照字典中的声明执行。
还有第二个不太显眼的代价。如果某个库的压缩器不是确定性的——不同的 zlib 构建、不同的等级、不同的内部区块边界——那么本应生成相同 PDF 的两次运行,就会生成两个不同的文件。这会破坏字节级可重现性。可重现性一旦被破坏,也会连带破坏 golden-file 测试、签署构建验证,以及任何会对输出做差异比对的流程。筛选器同时决定 PDF 是否正确,也决定 PDF 是否 相同。
精简版说明
标题为“精简版说明”的章节- 串流对象 是一个字典加一段字节块,包裹在
stream…endstream之间,并带有一个/Length,通常还有一个/Filter。 /Filter项指明解码筛选器——也可以是一个 数组,表示多个筛选器会以 管线 方式依序套用。- 这些筛选器分为两大类:压缩(FlateDecode、LZWDecode、RunLengthDecode、DCTDecode、JPXDecode、JBIG2Decode)与 ASCII 传输(ASCIIHexDecode、ASCII85Decode),另有用于加密的特殊 Crypt 筛选器。
- 你最常见到的是 FlateDecode——zlib/deflate。它是内容、字体以及交互参照串流的默认筛选器。
- NextPDF 将其 Flate 输出固定为固定的等级与格式,好让相同的输入字节永远压缩成相同的输出字节。
NextPDF 的处理方式
标题为“NextPDF 的处理方式”的章节NextPDF 通过单一缓冲区辅助方法输出串流对象,并通过单一固定压缩器进行压缩——这是有意为之。
BinaryBuffer::writeStream()(src/Support/BinaryBuffer.php)会将串流内容包进其字典中,始终写入一个等于实际字节长度的 /Length,并合并调用方提供的任何额外项,例如 /Filter。不存在任何路径会让声明的长度与写入的字节不一致,因为长度直接取自内容字符串本身。
压缩会通过 PinnedZlibCompressor(src/Writer/PinnedZlibCompressor.php)进行。这个类只有一个存在理由:未指定明确等级的 gzcompress 会沿用 zlib 运行时的默认值,而这个默认值在不同构建之间并不一致。那 2 个字节的 zlib 标头甚至会间接编码出等级,因此「默认值」并不是稳定输出。这个压缩器将等级固定为 RFC 1951 的最高值,并永远输出 zlib 包装的 deflate(RFC 1950 标头加上 Adler-32 结尾),这正是 /Filter /FlateDecode 所预期的形式。zlib 的硬性失败会变成带类型的异常,而不会静默退回到未压缩输出——串流绝不会被悄悄以原始形式输出。
交互参照串流本身就是上述机制的实现示例:CrossReferenceStream(src/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 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 ISO 32000-2 §7.5.8 ),其 /W 数组指明 解码后串流中 每个项目字段的字节宽度。标准要求它不得加密,也不得使用 Crypt 筛选器。
NextPDF 的 CrossReferenceStream 完全按此处理——FlateDecode、
明确的 /W,不加密。
实务范例
标题为“实务范例”的章节一个以 Flate 压缩的页面内容串流。这是最常见的形态:一个带有 /Length 与 /Filter 的字典,后面跟着 stream 与 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读取器则执行相反的过程:读取 /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 构建)可能在不改变解压缩内容的情况下改变压缩后的字节——结果正确,但并非 完全相同,这会破坏字节级验证。
相关文档
标题为“相关文档”的章节- PDF 究竟是什么——本页中的串流所在的对象模型。
- 字体:困难的部分——内嵌字体程序是经过筛选的串流,有其各自的失败模式。
- PDF 2.0:有什么改变——2.0 基准如何处理串流,以及 NextPDF 默认采用的交互参照串流。
词汇表
标题为“词汇表”的章节- 串流对象——一个字典加一段位于
stream与endstream之间的字节块,带有一个/Length,通常还有一个/Filter。 - 筛选器——读取器对串流字节所套用的命名解码转换(例如
FlateDecode)。 - 筛选器管线——一个依序套用的筛选器数组;数组顺序就是解码顺序。
- FlateDecode——zlib/deflate 筛选器;内容、字体以及交互参照串流的默认压缩方式。
- DCTDecode——JPEG 图像筛选器;有损,因此重新编码会再次使图像劣化。
- ASCII 传输筛选器——ASCIIHexDecode/ASCII85Decode;以体积为代价让数据变成 7 比特安全——并非压缩。
- 确定性压缩——对相同的输入产生字节相同的压缩输出,通过固定压缩器的等级与格式实现。