本文是对PDF Explained(by John Whitington)第三章《File Structure》的摘要式翻译。

文件布局

一个简单合法的PDF文件按顺序可分为如下四部分:

  1. header,给出了PDF版本号
  2. body,包含了页面,图形内容,和许多辅助信息,它们都编码为一系列对象。
  3. 交叉引用表,列出了每个对象在文档中的位置,便于随机访问。
  4. 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文件,将其从一系列字节转换为内存中的“对象图”,通常有如下步骤:

  1. 从文件开头读取PDF header,确认这确实是PDF文档并获取其版本号。
  2. 从文件末尾逆向检索,找到文件结束标记。然后读取trailer字典以及交叉引用表开关位置的字节偏移。
  3. 读取交叉引用表,获取每个对象在文件中的位置。
  4. 在此阶段,可以读取和解析所有对象,也可以在需要时再对每个对象进行处理。
  5. 使用数据提取页面,解析图形内容,提取元数据等。

这不是详尽的描述,因为可能存在许多复杂的情况(加密,线性化,对象和交叉引用流)。

下面以伪代码给出的递归数据结构可以表示一个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文件非常快,因为它只是将对象图展平为一系列字节。

步骤:

  1. 输出header。
  2. 删除PDF中未被其它对象引用的对象。这样可以避免写入无用的对象。
  3. 从1至n,重新对对象进行编号,其中n是文件中对象的个数。
  4. 从1号对象开始,逐个输出对象。记录每个对象的字节偏移量,为后续写入交叉引用表作准备。
  5. 写入交叉引用表。
  6. 写入trailer,trailer字典和文件结束标记

PDF Explained(翻译)第三章 文件结构相关推荐

  1. c++ primer 第五版 翻译 第三章

    字符串,向量和数组 3.1 命名空间using声明 3.2 库类型string 3.3 库类型vector 3.4 迭代器介绍 3.5 数组 3.6 多维数组 小结 专业术语 除了第二章介绍的内置类型 ...

  2. 《Fundamentals of Computer Grahpics 4ed》虎书第四版翻译——第三章光栅图像

    文章目录 3.1 光栅设备 3.1.1 显示器 3.1.2 硬拷贝设备 3.1.3 输入设备 3.2 图像,像素和形状 3.2.1 像素值 3.2.2 显示器强度和伽马 3.3 RGB颜色 3.4 A ...

  3. 计算机审计 pdf,计算机审计第三章作业.pdf

    1. 审计软件的审计实施阶段前,包括哪些内容? 答: a. 项目管理 b. 数据准备 c. 审计准备 2. 新建审计项目时,在"项目登记"界面里,在定义'审计时限范围'时,可以创建 ...

  4. Lua manual翻译——第三章第七、八、九节

    因为受到经济危机的影响,我在 bokee.com 的博客可能随时出现无法访问的情况:因此将2005年到2006年间在 bokee.com 撰写的博客文章全部迁移到 csdn 博客中来,本文正是其中一篇 ...

  5. Lua manual翻译——第三章第五、六节

    因为受到经济危机的影响,我在 bokee.com 的博客可能随时出现无法访问的情况:因此将2005年到2006年间在 bokee.com 撰写的博客文章全部迁移到 csdn 博客中来,本文正是其中一篇 ...

  6. Lua manual翻译——第三章第三、四节

    因为受到经济危机的影响,我在 bokee.com 的博客可能随时出现无法访问的情况:因此将2005年到2006年间在 bokee.com 撰写的博客文章全部迁移到 csdn 博客中来,本文正是其中一篇 ...

  7. Lua manual翻译——第三章第一、二节

    因为受到经济危机的影响,我在 bokee.com 的博客可能随时出现无法访问的情况:因此将2005年到2006年间在 bokee.com 撰写的博客文章全部迁移到 csdn 博客中来,本文正是其中一篇 ...

  8. 关于对《Spring Security3》翻译 (第一章 - 第三章)

    原文:http://lengyun3566.iteye.com/category/153689?page=2 翻译说明 最近阅读了<Spring Security3>一书,颇有收获(封面见 ...

  9. PDF Explained(翻译)第七章 文档元数据和导航

    本文是对PDF Explained(by John Whitington)第七章< Document Metadata and Navigation>的摘要式翻译,并加入了一些自己的理解. ...

最新文章

  1. 规格表管理之保存规格表数据表数据
  2. ThinkJS前端搭配vue时的Nginx配置
  3. 深耕“工业互联网”,华为云持续开花
  4. python numpy loadtxt
  5. Win7系统不能拖动文件夹的问题怎么解决?
  6. 【Python学习21】Python中函数的用法,使用函数进行简单的数学运算
  7. LINUX分辨率修改
  8. 期刊论文格式模板 电子版_期刊论文发表流程是怎样的呢?
  9. 基础算法(三)---二分排序(Java)
  10. java打飞机游戏完整代码
  11. 怎样设定目标(一)——目标金字塔
  12. 802.1Q VLAN 简介
  13. 结婚证照片尺寸是多少?教你拍出好看的证件照照片
  14. MATLAB机器人可视化
  15. rabbitmq reply-code=404, reply-text=NOT_FOUND
  16. matlab 直和,MATLAB求空间sdm的直接和间接效应
  17. 邻域、邻接、通路、连通、连通集、区域概念区分​​​​​​​
  18. 如何入手学习halcon课程
  19. 2019年繁星屠龙1-8窍门_狂打一星,新版《倚天屠龙记》就那么烂?
  20. 人工智能数学基础4:离差、平均差、方差、标准差、协方差、皮尔森相关系数

热门文章

  1. DOM(文档对象模型)(二)深入了解DOM节点
  2. 发点牢骚! 真的觉得日本人好牛~~
  3. 竞价推广怎么找到精准流量?
  4. 这几个宝藏成长利器,快码起
  5. 毕业设计 Spring Boot的药品管理系统(含源码+论文)
  6. 一种新的爬虫网络解锁器
  7. javascript ←AES.CBC→ go 相互加密解密
  8. 试题 E: 玩具蛇(C/C++)
  9. 拥抱国产云桌面,焱融科技与酷栈科技完成产品兼容认证
  10. PaddleSpeech TTS 设计要素 — 实验输出目录