本文是对PDF Explained(by John Whitington)第三章《File Structure》的摘要式翻译。
一个简单合法的PDF文件按顺序可分为如下四部分:
再来看下第二章中“Hello World”的例子,每个部分的第一行都作了标注。
例3-1
%PDF-1.1 //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文件就是将文件中的对象转换为图的过程。这个图是有向的,每个链接都是单一方向的。
下图展现了例3-1对应的对象图
下面我们以例3-1为参考详细看一下这四个部分。
PDF文件的第一行指出了文档版本号。在我们的示例中,是:
%PDF-1.1
指明了该文件是PDF 1.1版本。
由于PDF文件通常都包含二进制数据,因此如果更改行结尾 ,它们可能会损坏(例如,文件通过FTP以文本模式传输)。 为了允许传统文件传输程序确定文件是二进制的, 通常在标头中包含一些编码大于127的字符。例如:
%âãÏÓ
百分号表示注释,其他几个字节是编码大于127的任意字符。 我们示例中的完整header是:
%PDF-1.0
%âãÏÓ
文件正文由一系列对象组成,每个对象前会有单独的一行,该行包括一个对象编号,一个世代号以及关键字obj。紧跟象之后的是endobj关键字,它同样独占一行。
1 0 obj
<<
/Kids [2 0 R]
/Count 1
/Type /Pages
>>
endobj
这里,对象编号是1,世代号是0(它几乎总是0)。 对象1的内容位于1 0 obj和endobj两行之间。 本例中,它是字典
<</Kids [2 0 R] /Count 1 /Type /Pages >>
交叉引用表列出了每个对象在文件中的字节偏移量。 这允许对对象进行随机访问,不必对未使用的对象进行解析。
PDF文件中的每个对象都有一个对象编号和一个世代编号。 当交叉引用表中的条目被重用时,世代号将不再为0,此处我们不考虑这种情况。
我们可以认为交叉引用表由以下几部分组成:一个表示条目数的标题行, 然后是一个特殊条目,接下来的每行对应文件中的一个对象。在我们的文件中:
0 6 //交叉引用表中有6个条目,从0开始
0000000000 65535 f 特别条目
0000000015 00000 n 对象1的字节偏移量为15
0000000074 00000 n 对象2的字节偏移量为74
0000000192 00000 n 等等...
0000000291 00000 n
0000000409 00000 n 对象5的字节偏移量为409
请注意,字节偏移量以前导零补齐,以确保每个条目都相同 长度(译者注:10位偏移量,5位世代号)。因此,我们也可以随机访问交叉引用表。
trailer的第一行是关键字trailer。之后是trailer字典,至少包含/Size (交叉引用表中的条目数)和 /Root(它给出了文档目录对应的对象编号,文档目录是对象图的根元素)。
接下来的几行分别是:
下面是例3-1中的Trailer:
trailer //关键字trailer
<< //trailer字典
/Root 5 0 R
/Size 6
>>
startxref //startxref keyword
459 //交叉引用表的字节偏移量
%%EOF //文件结束标记
从文件末尾向后读取trailer:找到文件结束标记, 提取交叉引用表的字节偏移量,然后解析trailer字典。 trailer关键字标记trailer的开始。
有三种字符:常规字符,空白字符和分隔符。空白符如下表所示:
字符编码 | 含义 |
---|---|
0 | Null |
9 | Tab |
10 | 换行(LF) |
12 | 换页 |
13 | 回车(CR) |
32 | 空格 |
PDF文件可以使用, 或作为行尾。整体替换行尾(比如在文本编辑器中)可能导致文件的损毁。因为它会更改在压缩的二进制数据中的"行尾字符",也可能会改变对象长度,进而使得交叉引用表失效。
分隔符包括() <> [] {} / %,用于定义数组,字典等。 所有其他字符都是常规字符,没有特殊含义。
PDF支持五种基本对象:
三种复合对象:
将对象链接在一起的方法:
PDF文件由对象图组成,间接引用形成它们之间的链接。例3-1的对象图如图3-1所示。
整数
0 +1 -1 63
实数
0.0 0. .0 -0.004 65.4
通常,规范允许给定对象是整数或实数。其他时候它必须是整数。此外,整数和实数的范围和精度由PDF实现(而非标准)决定。在某些实现中,如果整数超出可用范围,就会被转换为实数。
注意,不允许使用指数符号。比如,4.5e-6是非法的。
字串由括号间的一串字节组成:
(Hello, World!)
若要表示反斜杠\和括号(),必须在它们前面加上反斜杠进行转义。例如:
(Some \\ escaped \(characters)
表示字符串"Some \ escaped(characters"。平衡的括号对在字符串内不需要转义。例如,
(Red(Rouge))
表示字符串“Red(Rouge)”。
反斜杠也可用于引入其他字符代码,如下表所示:
字符序列 | 含义 |
---|---|
\n | 换行 |
\r | 回车 |
\t | 水平制表符 |
\b | 退格 |
\f | 换页符 |
\ddd | 三个8进行数组成的字符编码 |
字符串也可以表示为<和>之间的十六进制数字序列,每对代表一个字节:
<4F6Eff00> //Bytes 0x4F, 0x6E, 0xFF, and 0x00
当有奇数位数时,最后一位会假定为0。(译者注:比如代表0xAB, 0xC0)
十六进制字符串的作用是使得二进制数据对用户可读,功能上与常规的描述字串相同。
名称的使用遍布整个PDF,作为字典的key以及定义各种多值对象。名称 由一个正斜杠引入。例如:
/French
/是名称的一部分–事实上,/它本身就是一个有效的名称。 名称不能含有空格或分隔符,但如果名称需要与包含这些字符(比如空格)的外部名子相对应时,我们可以使用#后接两个十进制数字表示:
/Websafe#20Dark#20Green
这表示名称/Websafe Dark Green,因为在ASCII中, 十六进制20代表空格。名称区分大小写(/French和/french是不同的)。
true/false
数组是PDF对象的有序集合,可以包含其他数组。对象不一定都是同一类型。例:
[0 0 400 500]
数组包含四个数字,顺序为:0,0,400,500。
[/Green /Blue [/Red /Yellow]]
数组包含三个条目:名称/Green,名称/Blue和数组[/Red /Yellow].
字典是键值对的无序集合。key是句子,值可以是任意PDF对象。字典数据写在<<和>>之间。例:
<</One 1 /Two 2 /Three 3>>
//One映射为1
//Two映射为2
字典是可以包含其他字典。
为了将PDF内容拆分为单独的对象,我们使用间接引用将它们连接在一起。对对象6的间接引用写为:
6 0 R
6是对象编号,0是世代号,R是间接参考关键字。
下例中的字典使用了间接引用:
<< /Resources 10 0 R
/Contents [4 0 R] >>
对象10和4在字典的值中被引用。
流用于存储二进制数据。它们由一个字典和紧随其后的二进制数据块组成。 字典列出了数据的长度,以及其它可选参数。
从语法上讲,流的构成如下:一个字典,后跟stream关键字,换行符(或 ),零个或多个字节的数据,换行符,最后是endstream关键字。例:
4 0 obj //对象4
<<
/Length 65 //数据长度
>>
stream //流关键字
1. 0. 0. 1. 50. 700. cm //65字节的数据,这里是图形流
BT
/F0 36. Tf
(Hello, World!) Tj
ET
endstream //结束流关键字
endobj //对象的结束
这里,字典只包含/Length条目,它以字节为单位给出流的长度。
所有流必须是间接对象。流通常会被压缩,可用的压缩算法如下: 所有流必须是间接对象。流通常会被压缩,可用的压缩机算法如下:
/ASCIIHexDecode
/ASCII85Decode
/LZWDecode
/FlateDecode
/RunLengthDecode
/CCITTFaxDecode
/DCTDecode //JPEG有损压缩。整个JPEG文件可以放在这里,包含文件头。
/JPXDecode //JPEG2000有损和无损压缩。仅限于JPX基准功能集。
下面是一个压缩流的例子:
796 0 obj
<</Length 275 /Filter /FlateDecode>>
stream
HTKO0÷u //And 268 more bytes...
endstream
endobj
可以使用多个过滤器,其方法是为流的字典中的/Filter条目指定数组而不是一个名称。
例如,使用JPEG方法压缩然后使用ASCII85编码的图像可能具有以下过滤器条目:
/Filter [/ASCII85Decode /DCTDecode]
如果过滤器需要外部参数(例如,在数据流本身之外定义压缩参数),也会将这些参数存储在流字典中。
增量更新允许将修改附加到文件末尾,从而更新文件, 因此无需再对文件进行整体修改(这对于大文件来说可能需要很长时间)。
更新会创建新对象或修改老对象,以及更新交叉引用表。 这意味着保存更改所花费的时间更少,但文件可能会变得臃肿(因为无用的对象无法删除)。
这个更新过程可能会发生多次。使用这种方式更新文件,其副作用是,可以撤销之前的更改,恢复至早期版本(译者注:也许出于某些原因,你不希望别人看到文件的各种早期版本)。
若文档有数字签名则必须以增量方式进行所有更新–否则 数字签名将无效。收件人可以撤消增量更新以检索原始的,经过认证的文档。
当一个文件以递增方式更新时,会添加一个新的trailer,它会包含前一个trailer 中的所有条目,以及一个/Prev条目,/Prev给出了先前交叉引用表的字节偏移量。 因此,增量更新的文件将具有多个trailer字典和文件结束标记。 通过这种方式,PDF应用程序可以逆序读取交叉引用部分, 以构建每个对象的最新版本的列表。已替换的对象会保持原有的对象编号(译者注:世代号会改变)。
从PDF 1.5开始,引入了一种新机制来进一步压缩PDF文件。这种机制允许将多个对象放入单个对象流,然后再对整个流进行压缩。同时引入了一种引用流中对象的机制–交叉引用流。
文件通常使用几组对象流,同时被需要的对象会组合在一起。例如第一页上的所有对象,第二页上的所有对象,等等。
这种方式保留了文档的随机访问特性,如果将文件中的所有对象放入 单个对象流中,文档将不具备这种特性。对象流不能包含其他流。
使用这些机制压缩的文件很难直接阅读,我们可以 使用pdftk中的解压缩操作,将它们解压以供审阅。
在网络环境中查看大型PDF文件时,尤其是当网速较慢时, 用户不希望等待整个文件下载后再查看它。在Web浏览器中查看文档时,这一点尤为重要。
我们希望第一页快速显示,并且可以尽快跳转到另一页(通过单击超链接或书签)。 在单个页面较大时,我们希望页面内容逐步显示,最重要的内容首先出现。 网络传输机制例如HTTP 通常允许获取任意数据块。但是,因为延迟,我们希望获取一个包含页面所有数据的块, 而不是数百个小块,每个对象一个。
PDF 1.2引入了这样一种机制,线性化PDF。 该机制给出了文件中对象的排序规则,同时引入了提示表(hint table)用来指出对象的具体排序方式。系统是向后兼容的,因此线性化的PDF文件也可视为普通的PDF,可以被不支持线性化PDF的阅读器读取。
线性化的PDF文件可以通过文件顶部(header之后)的线性化字典加以识别。例:
%PDF-1.4
%âãÏÓ
4 0 obj
<< /E 200967
/H [ 667 140 ]
/L 201431
/Linearized 1
/N 1
/O 7
/T 201230
>>
endobj
GhostScript附带的命令行程序pdfopt可以将文件线性化。例:
pdfopt input.pdf output.pdf
这会将input.pdf线性化并将结果写入output.pdf。
要读取PDF文件,将其从一系列字节转换为内存中的“对象图”,通常有如下步骤:
这不是详尽的描述,因为可能存在许多复杂的情况(加密,线性化,对象和交叉引用流)。
下面以伪代码给出的递归数据结构可以表示一个PDF对象。
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 Array of (string, pdfobject) pairs
| Stream of (pdfobject, bytes) //Stream dictionary and stream data
| Indirect of int
例如,对象<< /Kids [2 0 R] /Count 1 /Type /Pages >>可能表示为:
Dictionary
((Name (/Kids), Array (Indirect 2)),
(Name (/Count), Integer (1)),
(Name (/Type), Name (/Pages)))
将PDF文档比读简单得多, 我们不需要支持所有PDF格式,只需要支持我们打算使用的子集。写作 PDF文件非常快,因为它只是将对象图展平为一系列字节。
步骤: