技术文章

了解 PDF 文件结构:技术概述

· PDF 结构

PDF文件内部结构:完整解析

可移植文档格式(PDF)已成为文档交换和归档的实际标准。了解其内部结构对于开发人员、系统管理员以及所有参与文档处理工作流程的人员至关重要。本综合指南深入探讨PDF文件的复杂布局和内容,考察其四个主要部分以及构成每个组件的对象的详细语法。

PDF文件布局:四个关键组件

每个有效的PDF文件都遵循严格的架构模式,由四个主要部分组成,并按特定的顺序排列。这些组件协同工作,创建了一种既结构化又非常适合随机访问的格式。

  1. 标题 (Header) – 标识PDF版本号和二进制特性
  2. 正文 (Body) – 包含所有文档对象,包括页面、字体、图像和图形内容
  3. 交叉引用表 – 提供精确的字节偏移量映射,用于随机对象访问
  4. 预告片。 包含重要的元数据和导航指针。

剖析完整的PDF文件: “Hello, World” 示例。

为了理解这些组件如何协同工作,让我们来分析一个完整的、最小的PDF文件,该文件显示“Hello, World!”文本。这个示例展示了PDF结构中的所有关键元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
%PDF-1.0                          % Header starts here
%âãÏÓ
 
1 0 obj                           % Body starts here
<<
/Kids [2 0 R]
/Count 1
/Type /Pages
>>
endobj
 
2 0 obj
<<
/Rotate 0
/Parent 1 0 R
/Resources 3 0 R
/MediaBox [0 0 612 792]
/Contents [4 0 R]
/Type /Page
>>
endobj
 
3 0 obj
<<
/Font
<<
/F0
<<
/BaseFont /Times-Italic
/Subtype /Type1
/Type /Font
>>
>>
>>
endobj
 
4 0 obj
<<
/Length 65
>>
stream
1. 0. 0. 1. 50. 700. cm BT
  /F0 36. Tf
  (Hello, World!) Tj
ET
endstream
endobj
 
5 0 obj
<<
/Pages 1 0 R
/Type /Catalog
>>
endobj
 
xref                              % Cross-reference table starts here
0 6
0000000000 65535 f
0000000015 00000 n
0000000074 00000 n
0000000192 00000 n
0000000291 00000 n
0000000409 00000 n
 
trailer                           % Trailer starts here
<<
/Root 5 0 R
/Size 6
>>
startxref
459
%%EOF

💡 理解PDF对象图。

PDF对象形成一个有向图结构,其中节点是PDF对象,链接是间接引用。这种图表示允许高效地随机访问内容,而无需进行顺序的文件处理。文档目录(对象5)作为根节点,连接到页面树(对象1),后者引用单个页面及其资源。

头部:版本控制和二进制标识。

PDF头部具有两个关键功能,可确保在不同的系统和应用程序中正确处理文件:

1
2
%PDF-1.0
%âãÏÓ

第一行指定PDF版本(在此示例中为1.0)。PDF具有出色的向后兼容性,这意味着较新的阅读器可以无缝处理较旧的版本。它在一定程度上也具有向前兼容性,因为大多数PDF应用程序会尝试读取文件,而不管其声明的版本号。

第二行包含二进制字符,其ASCII码值高于127。这一点至关重要,因为PDF文件几乎总是包含二进制数据,如果在文件传输过程中修改了行尾,这些数据可能会损坏。这些高ASCII字符有助于旧的文件传输程序将文件识别为二进制文件,从而防止自动进行行尾转换,这会损坏文档。

百分号 (%) 在PDF语法中表示注释行,而特定的字符 âãÏÓ 是任意的字节,其值超过ASCII 127,用作传输协议的二进制标记。

Body:所有内容都位于此处。

文件主体构成主要内容存储库,由一系列对象组成。每个对象都遵循严格的语法结构:

1
2
3
[object_number] [generation_number] obj
[object_content]
endobj

每个对象都以对象编号、生成编号以及 obj 关键字开始,位于同一行,然后是对象内容,最后以 endobj 关键字结束。生成编号允许在更新交叉引用条目时重用对象,对于大多数情况,该值保持为零。

例如,查看我们示例中的对象1:

1
2
3
4
5
6
7
1 0 obj
<<
/Kids [2 0 R]
/Count 1
/Type /Pages
>>
endobj

此对象(编号 1,第 0 代)包含一个字典,定义了页面树。 /Type /Pages 该条目标识其为一个页面树节点, /Count 1 指示它包含一个页面,并且 /Kids [2 0 R] 引用对象 2 作为其子页面。

交叉引用表:导航骨干

交叉引用表代表 PDF 中用于性能优化的最巧妙的功能。它提供从对象编号到文件内字节位置的直接映射,从而可以在不进行顺序扫描的情况下实现随机访问:

1
2
3
4
5
6
7
8
xref
0 6                              % Six entries starting at object 0
0000000000 65535 f               % Special entry for free objects
0000000015 00000 n               % Object 1 at byte offset 15
0000000074 00000 n               % Object 2 at byte offset 74
0000000192 00000 n               % Object 3 at byte offset 192
0000000291 00000 n               % Object 4 at byte offset 291
0000000409 00000 n               % Object 5 at byte offset 409

每个交叉引用条目由正好 20 个字节组成:一个 10 位数字的字节偏移量(带前导零)、一个 5 位数字的生成编号,以及一个字符(对于正常对象,使用 "n";对于空对象,使用 "f"),后面是必有的空格。这种固定长度的格式可以实现对交叉引用表本身的随机访问。

第一个条目(对象 0)始终是一个特殊条目,指向空对象列表的头部,其生成编号为 65535。这种机制允许 PDF 在增量更新期间重用对象编号,从而在删除对象时可以重用。

预告片:重要的元数据和文件导航。

预告片部分提供了至关重要的信息,用于 PDF 处理程序导航文档结构。

1
2
3
4
5
6
7
8
trailer
<<
/Root 5 0 R                      % Document catalog reference
/Size 6                          % Number of xref entries
>>
startxref
459                              % Byte offset of xref table
%%EOF                            % End-of-file marker

预告片以 trailer 关键字开头,后跟包含重要导航信息的预告片字典。 /Size 条目指定交叉引用表中条目的总数,而 /Root 指向文档目录,它是对象图的根元素。

好的。 startxref 关键字前面是一个数字,表示交叉引用表开始的字节偏移量。最后, %%EOF 标记 PDF 文件的结束。PDF 阅读器通过找到此文件结束标记开始处理,然后反向查找预告片和交叉引用表,然后按需加载对象。

词汇约定:PDF语法的基石。

PDF文件是遵循特定词汇规则的8位字节序列,用于解析成token。理解这些约定对于PDF处理至关重要:

字符分类

PDF 识别三种类型的字符:

  • 普通字符 – 所有非空格和分隔符的字符。
  • 空格字符 – 用于token分隔。
  • 分隔符 – 特殊字符: ( ) < > [ ] { } / %

PDF 中的空白字符包括:

Character Code Meaning
0 Null
9 Tab
10 Line feed
12 Form feed
13 Carriage return
32 Space

PDF 文件可以使用 <CR>、<LF> 或 <CR><LF> 序列来结束行。但是,批量更改行尾符很可能会损坏文件,因为它会影响压缩二进制数据部分中的行尾符序列。

PDF 对象类型:完整的分类

PDF 支持八种基本对象类型,这些类型是所有文档内容的构建块。这些类型分为基本对象、复合对象和链接机制:

基本对象

整数和实数

数字是 PDF 数字系统的基础。

1
2
3
4
5
% Integer examples
0 +1 -1 63
 
% Real number examples  
0.0 0. .0 -0.004 65.4

整数由十进制数字(0-9)组成,可以选择在前面加上正号或负号。 浮点数遵循类似的规则,但可以包含一个小数点,该小数点可以出现在数字的开头、中间或结尾。 值得注意的是,指数表示法(如 4.5e-6)在 PDF 中是不允许的。

数字的范围和精度取决于 PDF 的实现,而不是规范。 一些实现会将整数转换为浮点数,当整数超出可用范围时。

字符串:两种表示方法

PDF 提供了两种不同的字符串格式,适用于不同的用例。

文本字符串

文本字符串出现在括号内,并支持转义序列:

1
2
3
4
5
6
7
8
% Simple string
(Hello, World!)
 
% String with escaped characters
(Some \\ escaped \(characters\))
 
% String with balanced parentheses (no escaping needed)
(Red (Rouge))

文本字符串中的转义序列包括:

Sequence Meaning
\n Line feed
\r Carriage return
\t Horizontal tab
\b Backspace
\f Form feed
\ddd Character code in three octal digits
十六进制字符串

十六进制字符串提供了一种替代表示方法,尤其适用于二进制数据:

1
2
<4F6Eff00>                       % Bytes 0x4F, 0x6E, 0xFF, 0x00
<48656C6C6F>                     % "Hello" in ASCII hex

每两个十六进制数字代表一个字节。当出现奇数个数字时,假设最后一个数字后面跟着一个 0。这种格式使二进制数据易于人类阅读,同时保持与字面字符串的功能等效性。

名称:PDF 的标识符系统

名称在 PDF 中用作标识符,充当字典键和符号常量:

1
2
3
4
/French                         % Simple name
/                               % Valid name (just the slash)
/Websafe#20Dark#20Green         % Name with encoded spaces (#20 = space)
/A#42                           % Name with encoded character (#42 = 'B')

名称以正斜杠开头,不允许直接包含空格或分隔符字符。特殊字符使用哈希编码,编码方式为两个十六进制数字。名称区分大小写,因此 /French/french 代表不同的标识符。

布尔值和空值

PDF 支持标准的布尔值和空对象。

1
2
3
true                             % Boolean true
false                            % Boolean false
null                             % Null object

这些用作字典条目的标志和对象结构中的占位符值。

复合对象

数组:有序集合

数组包含任何 PDF 对象的有序序列,包括其他数组。

1
2
3
[0 0 400 500]                   % Four integers (typical rectangle)
[/Green /Blue [/Red /Yellow]]   % Mixed types with nested array
[1 0 R 2 0 R 3 0 R]             % Array of indirect references

数组不需要类型一致性,元素可以是数字、字符串、名称、其他数组或任何 PDF 对象类型。

字典:键值映射

字典表示无序的键值对集合,其中键始终是名称。

1
2
3
4
5
6
7
8
<</One 1 /Two 2 /Three 3>>      % Simple mappings
<<                               % Multi-line dictionary
  /Type /Page
  /Parent 1 0 R
  /Resources 3 0 R
  /MediaBox [0 0 612 792]
  /Contents [4 0 R]
>>

字典是 PDF 结构化数据的核心,包含从页面定义到字体规范的所有内容。 它们可以任意嵌套,形成复杂的层次结构。

流:二进制数据容器

流将字典与二进制数据结合,对于图像、字体和压缩内容至关重要。

1
2
3
4
5
6
7
8
9
10
11
12
4 0 obj
<<
/Length 65                       % Stream length in bytes
/Filter /FlateDecode            % Optional compression filter
>>
stream
1. 0. 0. 1. 50. 700. cm BT      % Binary or text data
  /F0 36. Tf
  (Hello, World!) Tj
ET
endstream
endobj

流由一个字典(至少包含一个条目),一个关键词,一个换行符,数据字节,另一个换行符,以及另一个关键词组成。 所有流都必须是间接对象,并且通常使用压缩来提高效率。 /Length entry stream 关键词 endstream 关键词

间接引用:对象链接

间接引用在对象之间创建链接,从而形成图结构,这使得 PDF 具有高效性。

1
2
6 0 R                            % Reference to object 6, generation 0
<</Resources 10 0 R /Contents [4 0 R]>>  % Dictionary using references

该格式由对象编号、生成编号以及关键字组成。 R 这种机制允许对象相互引用,而无需嵌入完整的定义,从而实现共享和随机访问。

流和过滤器:高级数据处理。

流是 PDF 中存储二进制数据的关键机制。 大部分 PDF 内容,包括页面图形和嵌入的字体,都存储在流中,通常进行压缩以提高空间效率。

综合过滤器类型。

PDF 支持多种压缩和编码过滤器,每种过滤器都针对特定数据类型进行了优化。

Filter Name Description and Use Cases
/ASCIIHexDecode Converts hexadecimal digit pairs to bytes. ‘>’ indicates end of data. Primarily for 7-bit data transmission compatibility.
/ASCII85Decode More efficient 7-bit encoding using printable characters ‘!’ through ‘u’ and ‘z’. Sequence ‘~>’ marks end of data.
/LZWDecode Lempel-Ziv-Welch compression, identical to TIFF implementation. Good general-purpose compression.
/FlateDecode Deflate compression (RFC 1950), used by zlib. Most common PDF compression method. Supports predictors for enhanced compression.
/RunLengthDecode Simple run-length encoding for data with repeated byte sequences.
/CCITTFaxDecode Group 3/4 fax compression. Excellent for monochrome (1-bit) images, poor for general data.
/JBIG2Decode Advanced compression for monochrome, grayscale, and color images. Superior to CCITT methods.
/DCTDecode JPEG lossy compression. Complete JPEG files with headers can be embedded directly.
/JPXDecode JPEG2000 compression supporting both lossy and lossless modes. Limited to JPX baseline feature set.

多重过滤器链。

过滤器可以链接使用,以满足复杂的处理需求。

1
2
/Filter [/ASCII85Decode /DCTDecode]    % JPEG data then ASCII85 encoded
/Filter [/ASCIIHexDecode /FlateDecode] % Deflate compression then hex encoding

在解码过程中,过滤器按相反的顺序应用,数组中的最后一个过滤器在读取数据时首先应用。

高级 PDF 架构

增量更新:非破坏性修改

增量更新允许通过追加更改而不是重写整个文件来修改 PDF。这个关键特性提供了以下几个好处:

  • 性能 – 仅写入新的/已更改的对象。
  • 数字签名 – 原始的已签名内容保持不变。
  • 版本历史 – 可以恢复之前的文档状态。
  • 大型文件效率。 – 针对大型文档,采用尽可能少的写入操作。

在增量更新过程中,新的对象和新的交叉引用部分会追加到文件末尾。新的尾部信息包含一个。 /Prev 一个条目,指向前一个交叉引用表的字节偏移量,从而创建一个文档版本的链表。

对象和交叉引用流 (PDF 1.5 及以上版本)

现代PDF版本引入了对象流和交叉引用流,以实现更好的压缩比。

  • 对象流 (Object Streams) – 多个对象压缩在一起形成单个流。
  • 交叉引用流 (Cross-Reference Streams) – 交叉引用数据以压缩流的形式存储。
  • 分组策略 (Grouping Strategy) – 对象按使用模式进行分组(例如,将所有第 1 页的对象放在一起)。

这种方法在显著减小文件大小的同时,保持了随机访问能力,尤其适用于包含许多小对象的文档。

线性化 PDF:Web 优化结构。

线性化 PDF(在 PDF 1.2 中引入)重新组织文件结构,以实现最佳的网络浏览体验。

1
2
3
4
5
6
7
8
9
10
11
12
%PDF-1.4
%âãÏÓ
4 0 obj                          % Linearization dictionary
<< /E 200967                     % End of first page
   /H [ 667 140 ]               % Hint stream location and length
   /L 201431                    % File length
   /Linearized 1                % Linearization flag
   /N 1                         % Number of pages
   /O 7                         % First page object number
   /T 201230                    % Traditional xref table offset
>>
endobj

线性化文件可以实现:

  • 快速显示第一页 – 文件中首先出现第 1 页的对象
  • 渐进式加载 – 在下载过程中,内容会逐步显示
  • 高效导航。 – 提示表优化页面访问
  • 向后兼容 文件仍然可以被非线性阅读器读取。

PDF 文件处理:技术实现。

读取算法:从字节到对象。

PDF 阅读器采用复杂的解析策略。

  1. 头部验证。 验证 PDF 签名并提取版本信息。
  2. 预告片位置。 从文件末尾向后搜索以查找 %%EOF 标记。
  3. 交叉引用解析. – 从交叉引用表中构建对象位置映射.
  4. 预告词典处理. – 提取文档目录和元数据.
  5. 对象加载策略. – 采用按需加载或预加载关键对象的方式.
  6. 内容树构建. – 从对象图中构建逻辑文档结构.

此过程处理包括加密、线性化、对象流和增量更新等复杂情况。

写入算法:从对象到字节。

PDF 生成遵循一个更直接的过程:

  1. 头部生成。 – 输出 PDF 版本和二进制标记。
  2. 对象图分析。 – 移除未引用的对象以减小文件大小。
  3. 对象重编号。 – 逐个分配从 1 到 n 的序列号。
  4. 对象序列化。 – 在记录字节偏移量的同时写入对象。
  5. 交叉引用生成。 – 从记录的偏移量创建交叉引用表。
  6. 尾部信息创建。 – 生成尾部字典和文件结束标记。

数据结构表示。

一个完整的 PDF 对象可以使用这种递归数据结构来表示。

1
2
3
4
5
6
7
8
9
10
pdfobject ::= Null
            | Boolean of bool
            | Integer of int  
            | Real of real
            | String of string
            | Name of string
            | Array of pdfobject array
            | Dictionary of (string, pdfobject) array
            | Stream of (pdfobject, bytes)
            | Indirect of int

例如,字典对象 << /Kids [2 0 R] /Count 1 /Type /Pages >> 将被表示为:

1
2
3
4
5
Dictionary [
  ("Kids", Array [Indirect 2]);
  ("Count", Integer 1);
  ("Type", Name "Pages")
]

实用工具和专业工作流程

几个命令行工具可以帮助进行 PDF 分析和操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
% Linearize PDF for web optimization
pdfopt input.pdf output.pdf
 
% Decompress streams for manual inspection  
pdftk input.pdf output decompressed.pdf uncompress
 
% Extract and analyze PDF structure
pdf-parser --stats document.pdf
 
% Repair corrupted PDF files
pdftk broken.pdf output repaired.pdf
 
% Extract specific pages
pdftk document.pdf cat 1-3 output pages1-3.pdf
 
% Get comprehensive PDF information
pdfinfo -meta -struct document.pdf
 
% Convert PDF to PostScript for analysis
pdftops document.pdf document.ps

安全性和完整性注意事项

理解 PDF 结构对于安全分析至关重要。

  • 嵌入内容检测 识别隐藏的流和对象。
  • 恶意代码分析。 检查 JavaScript 和表单操作。
  • 元数据提取。 恢复文档历史和作者信息。
  • 数字签名验证。 验证增量更新的完整性。

结论:掌握 PDF 架构。

理解 PDF 文件的结构是进行高级文档处理、法医分析和应用程序开发的基础。这种格式优雅的设计——由四个主要部分协同工作——创造了一个既易于人类阅读(在未压缩状态下)又非常适合复杂文档的系统。

从简单的“Hello, World”示例,到包含数千页且具有复杂交互功能的企业级文档,相同的基本原则适用。这种一致性使得 PDF 既具有可扩展性,又在各种用例中都具有可靠性。

从 PDF 1.0 发展到现在的版本,体现了对向后兼容的谨慎考虑,同时引入了强大的功能,例如对象流、高级压缩和网页优化。理解这些架构决策有助于更有效地进行 PDF 处理和故障排除。

⚠️ 实施注意事项

虽然本指南涵盖了 PDF 结构的基本概念,但完整的规范包含数百页的内容,详细描述了各种特殊情况、可选功能以及兼容性要求。对于生产环境的应用,请使用成熟的 PDF 库(例如)。 HotPDF 组件。Delphi PDF 库) 而不是从头开始实现解析器。这些库可以处理许多复杂情况和可选功能,而这些内容在本入门指南中没有涵盖。