PDF Explained(翻译)第三章 文件结构
Posted ball球
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PDF Explained(翻译)第三章 文件结构相关的知识,希望对你有一定的参考价值。
本文是对PDF Explained(by John Whitington)第三章《File Structure》的摘要式翻译。
文件布局
一个简单合法的PDF文件按顺序可分为如下四部分:
- header,给出了PDF版本号
- body,包含了页面,图形内容,和许多辅助信息,它们都编码为一系列对象。
- 交叉引用表,列出了每个对象在文档中的位置,便于随机访问。
- trailer,包含一个字典,用于定位文件中的各个部分,同时列出了可以在不处理整个文件的情况下读取的各种元数据。
再来看下第二章中“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为参考详细看一下这四个部分。
Header
PDF文件的第一行指出了文档版本号。在我们的示例中,是:
%PDF-1.1
指明了该文件是PDF 1.1版本。
由于PDF文件通常都包含二进制数据,因此如果更改行结尾 ,它们可能会损坏(例如,文件通过FTP以文本模式传输)。 为了允许传统文件传输程序确定文件是二进制的, 通常在标头中包含一些编码大于127的字符。例如:
%âãÏÓ
百分号表示注释,其他几个字节是编码大于127的任意字符。 我们示例中的完整header是:
%PDF-1.0
%âãÏÓ
Body
文件正文由一系列对象组成,每个对象前会有单独的一行,该行包括一个对象编号,一个世代号以及关键字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。之后是trailer字典,至少包含/Size (交叉引用表中的条目数)和 /Root(它给出了文档目录对应的对象编号,文档目录是对象图的根元素)。
接下来的几行分别是:
- 关键字startxref,
- 一个数字(交叉引用表起始位置的字节偏移量),
- %%EOF,它表示PDF文件的结尾。
下面是例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支持五种基本对象:
- 整数和实数,例如42和3.1415
- 字符串,括在括号中。例:(The Quick Brown Fox)。
- 名称,用于字典中的键,也有很多其他用途。它们以/开头,例如/Blue。
- 布尔值,由关键字true和false表示。
- null对象,由关键字null表示。
三种复合对象:
- 数组,包含其他对象的有序集合,如[1 0 0 0]。
- 字典,无序集合,保存名称到对象的映射关系。 例如,<</Contents 4 0 R /Resources 5 0 R >>, 其将/Contents映射到间接引用4 0 R,将/Resources映射到间接引用5 0 R.
- 流,包含二进制数据以及描述数据属性的字典, 例如长度、压缩参数等。流用于存储图像,字体等。
将对象链接在一起的方法:
- 间接引用,它形成了对象间的链接。
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
在网络环境中查看大型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文件,将其从一系列字节转换为内存中的“对象图”,通常有如下步骤:
- 从文件开头读取PDF header,确认这确实是PDF文档并获取其版本号。
- 从文件末尾逆向检索,找到文件结束标记。然后读取trailer字典以及交叉引用表开关位置的字节偏移。
- 读取交叉引用表,获取每个对象在文件中的位置。
- 在此阶段,可以读取和解析所有对象,也可以在需要时再对每个对象进行处理。
- 使用数据提取页面,解析图形内容,提取元数据等。
这不是详尽的描述,因为可能存在许多复杂的情况(加密,线性化,对象和交叉引用流)。
下面以伪代码给出的递归数据结构可以表示一个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格式,只需要支持我们打算使用的子集。写作 PDF文件非常快,因为它只是将对象图展平为一系列字节。
步骤:
- 输出header。
- 删除PDF中未被其它对象引用的对象。这样可以避免写入无用的对象。
- 从1至n,重新对对象进行编号,其中n是文件中对象的个数。
- 从1号对象开始,逐个输出对象。记录每个对象的字节偏移量,为后续写入交叉引用表作准备。
- 写入交叉引用表。
- 写入trailer,trailer字典和文件结束标记
以上是关于PDF Explained(翻译)第三章 文件结构的主要内容,如果未能解决你的问题,请参考以下文章