跳至内容
100% 本地 · 上传 0 KB压缩 PDF

PDF 如何存储文本和图像(内部揭秘)

PDF 是一个内部含有二进制块的文本文件。其结构出奇地可读:一个头、一列编号对象、一个交叉引用表和一个尾。本页逐步讲解该结构,并展示文本、字体和图像实际存在哪里。面向想理解 PDF 工具读写文件时在做什么的开发者和任何人。

概览

PDF 的四个部分

每个 PDF 文件都有相同的四部分布局:

  1. 头——一行,例如 %PDF-1.7,声明版本。
  2. 体——一列编号的间接对象。一切都住在这里:页面、字体、图像、元数据。
  3. 交叉引用表(xref)——每个对象的字节偏移,使阅读器无需扫描整个文件就能找到它们。
  4. 尾——指向根对象(文档目录)、xref 位置以及可选的加密信息。阅读器先读尾,再跳到根。

尾优先设计正是 PDF 可以在不加载整个文件的情况下以常数时间读取的原因——对巨大文档和流式阅读器很重要。

体

间接对象

体中每个对象都有编号,形如:

3 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>
endobj

3 0 obj 表示"这是对象 3,世代 0"。<< ... >> 中的体是一个字典——一列键/值对。值可以是数字、字符串、名字(以 / 为前缀)、数组、其他字典,或像 4 0 R("去看对象 4")这样的引用。这就是页面如何指向其内容流和字体而不内联它们的方式。

世代号通常是 0。当增量更新删除并重写一个对象时它会递增——这一特性让 PDF 可以通过在文件末尾追加更改来编辑,而无需重写一切。大多数现代写入器选择重写整个文件,所以世代保持为 0。

内容

文本如何存储

页面内容存在于一个内容流中——一个值为字节流(通常是 Flate 压缩的)的对象,包含 PDF 绘图算子。文本用 BT(开始文本)、Tf(设置字体和大小)、Td(移动位置)、Tj(显示文本)和 ET(结束文本)等算子绘制。一个简单的"Hello"大致如下:

BT
/F1 24 Tf
100 700 Td
(Hello) Tj
ET

字符串 (Hello) 使用字体的编码。对于简单拉丁文本,通常是 WinAnsiEncoding 或字体内置编码。对于 Unicode 文本,字符串是 16 位编码的字节串,字体有 /ToUnicode 映射,让阅读器能恢复实际码点用于复制粘贴和搜索。如果一个 PDF 复制粘贴乱码但可见文本正确,/ToUnicode 映射缺失或错误——这是常见的导出 bug。

文本不以 HTML 或段落形式存储。没有语义"段落"对象。行、换行和定位都是显式绘图命令。这就是把 PDF 重排到不同页面宽度很难的原因:阅读器必须从绘图命令逆向工程出布局。

字体

字体嵌入

PDF 中的字体对象有两部分:命名字体并指向其数据的字体字典,以及作为流嵌入的字体程序本身(Type 1、TrueType 或 OpenType)。PDF 规范要求字体被嵌入以使文档在任何地方都同样渲染——但不强制,这就是为什么有些 PDF 在没有字体的机器上替换字体。

子集化只嵌入文档中实际使用的字形。一个用于单个标题的 250 KB 字体变成 8 KB 子集。大多数现代导出器默认子集化。较老的导出器或某些"打印到 PDF"路径有时嵌入完整字体,这是文件臃肿的常见原因。完整分解见我们的文件大小指南。

图像

图像如何存储

图像作为 XObject(外部对象)存储——原始或压缩像素数据的流,加上描述宽度、高度、色彩空间、每分量位数和过滤器的字典。页面内容流然后用 Do 算子绘制图像。

PDF 支持几种图像过滤器(压缩):

  • DCTDecode——JPEG。有损,适合照片。图像密集 PDF 中尺寸的最常见来源。
  • FlateDecode——zlib/deflate。无损,适合线条艺术和颜色少的图像。
  • JPXDecode——JPEG2000。高质量下压缩优于 JPEG,但更慢且通用性较差。
  • JBIG2Decode——用于二值(1 位)扫描文本。可大幅缩小扫描页面。
  • CCITTFaxDecode——用于二值图像的经典传真压缩。

图像密集的 PDF 主要是压缩图像流的序列。用更激进的 JPEG 质量重新编码这些流是扫描文档最大的尺寸杠杆。

索引

xref 表和 trailer

xref 表列出每个间接对象的字节偏移。阅读器打开文件,读取 trailer(位于距文件末尾已知偏移处),找到 xref,并利用它直接跳到任何对象而无需解析整个文件。这就是 PDF 可随机访问的原因。

PDF 1.5 增加了交叉引用流,它压缩 xref 表本身。与对象流(许多小对象打包进一个压缩流)结合,这就是使现代 PDF 比其 1.4 等价物更小的结构性压缩。实际效果见我们的压缩解析。

trailer还指向 /Root(文档目录,列出页面树)、/Info 字典(Title、Author、CreationDate——元数据),以及如果文件被加密则可选的 /Encrypt 字典。

压缩

流和过滤器

任何值为二进制数据的对象(内容流、图像、嵌入字体)都是流:一个字典,后接 stream、原始字节和 endstream。字典的 /Filter 条目告诉阅读器如何解码字节——FlateDecode 用于 zlib,DCTDecode 用于 JPEG,等等。多个过滤器可以链式使用,例如一个先解码再下采样的图像。

这就是为什么"压缩 PDF"不是一个操作。每个流独立用自己的过滤器压缩。用对象流重新保存的压缩工具缩小结构开销;用更激进的 JPEG 质量重新编码图像缩小图像流。它们是独立的杠杆。

实践

这对 PDF 工具为何重要

当像 IXPDF 这样的工具合并、拆分或旋转 PDF 时,它解析 xref,遍历页面树,复制或重排页面对象,并重写 xref 和 trailer。内容流(文本和图像)逐字节复制——不重新编码,无质量损失。这就是为什么结构性操作快速且无损:它们重排对象引用,不重新渲染页面。

压缩是例外:它用对象流重新序列化结构。即便如此,除非用户明确要求重新编码,图像和字体流原样复制。

所有这些都通过 Web Worker 在你的浏览器中发生——文件在本地被解析、修改和重新序列化。你的 PDF 永不离开你的设备。

继续阅读

相关资源