前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >PDF Explained(翻译)第三章 文件结构

PDF Explained(翻译)第三章 文件结构

作者头像
跑马溜溜的球
发布2021-07-16 11:13:22
1.3K0
发布2021-07-16 11:13:22
举报
文章被收录于专栏:日积月累1024

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

文件布局

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

  1. header,给出了PDF版本号
  2. body,包含了页面,图形内容,和许多辅助信息,它们都编码为一系列对象。
  3. 交叉引用表,列出了每个对象在文档中的位置,便于随机访问。
  4. trailer,包含一个字典,用于定位文件中的各个部分,同时列出了可以在不处理整个文件的情况下读取的各种元数据。

再来看下第二章中“Hello World”的例子,每个部分的第一行都作了标注。

例3-1

代码语言:javascript
复制
%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文件的第一行指出了文档版本号。在我们的示例中,是:

代码语言:javascript
复制
%PDF-1.1

指明了该文件是PDF 1.1版本。

由于PDF文件通常都包含二进制数据,因此如果更改行结尾 ,它们可能会损坏(例如,文件通过FTP以文本模式传输)。 为了允许传统文件传输程序确定文件是二进制的, 通常在标头中包含一些编码大于127的字符。例如:

代码语言:javascript
复制
%âãÏÓ

百分号表示注释,其他几个字节是编码大于127的任意字符。 我们示例中的完整header是:

代码语言:javascript
复制
%PDF-1.0
%âãÏÓ

Body

文件正文由一系列对象组成,每个对象前会有单独的一行,该行包括一个对象编号,一个世代号以及关键字obj。紧跟象之后的是endobj关键字,它同样独占一行。

代码语言:javascript
复制
1 0 obj
<<
/Kids [2 0 R]
/Count 1
/Type /Pages
>>
endobj

这里,对象编号是1,世代号是0(它几乎总是0)。 对象1的内容位于1 0 obj和endobj两行之间。 本例中,它是字典

代码语言:javascript
复制
<</Kids [2 0 R] /Count 1 /Type /Pages >>

交叉引用

交叉引用表列出了每个对象在文件中的字节偏移量。 这允许对对象进行随机访问,不必对未使用的对象进行解析。

PDF文件中的每个对象都有一个对象编号和一个世代编号。 当交叉引用表中的条目被重用时,世代号将不再为0,此处我们不考虑这种情况。

我们可以认为交叉引用表由以下几部分组成:一个表示条目数的标题行, 然后是一个特殊条目,接下来的每行对应文件中的一个对象。在我们的文件中:

代码语言:javascript
复制
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:

代码语言:javascript
复制
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所示。

整数和实数

整数

代码语言:javascript
复制
0 +1 -1 63

实数

代码语言:javascript
复制
0.0 0. .0 -0.004 65.4

通常,规范允许给定对象是整数或实数。其他时候它必须是整数。此外,整数和实数的范围和精度由PDF实现(而非标准)决定。在某些实现中,如果整数超出可用范围,就会被转换为实数。

注意,不允许使用指数符号。比如,4.5e-6是非法的。

字串

字串由括号间的一串字节组成:

代码语言:javascript
复制
(Hello, World!)

若要表示反斜杠\和括号(),必须在它们前面加上反斜杠进行转义。例如:

代码语言:javascript
复制
(Some \\ escaped \(characters) 

表示字符串"Some \ escaped(characters"。平衡的括号对在字符串内不需要转义。例如,

代码语言:javascript
复制
(Red(Rouge))

表示字符串“Red(Rouge)”。

反斜杠也可用于引入其他字符代码,如下表所示:

字符序列

含义

\n

换行

\r

回车

\t

水平制表符

\b

退格

\f

换页符

\ddd

三个8进行数组成的字符编码

十六进制字符串

字符串也可以表示为<和>之间的十六进制数字序列,每对代表一个字节:

代码语言:javascript
复制
<4F6Eff00> //Bytes 0x4F, 0x6E, 0xFF, and 0x00

当有奇数位数时,最后一位会假定为0。(译者注:比如代表0xAB, 0xC0)

十六进制字符串的作用是使得二进制数据对用户可读,功能上与常规的描述字串相同。

名称

名称的使用遍布整个PDF,作为字典的key以及定义各种多值对象。名称 由一个正斜杠引入。例如:

代码语言:javascript
复制
/French

/是名称的一部分–事实上,/它本身就是一个有效的名称。 名称不能含有空格或分隔符,但如果名称需要与包含这些字符(比如空格)的外部名子相对应时,我们可以使用#后接两个十进制数字表示:

代码语言:javascript
复制
/Websafe#20Dark#20Green

这表示名称/Websafe Dark Green,因为在ASCII中, 十六进制20代表空格。名称区分大小写(/French和/french是不同的)。

布尔值

true/false

数组

数组是PDF对象的有序集合,可以包含其他数组。对象不一定都是同一类型。例:

代码语言:javascript
复制
[0 0 400 500]

数组包含四个数字,顺序为:0,0,400,500。

代码语言:javascript
复制
[/Green /Blue [/Red /Yellow]]

数组包含三个条目:名称/Green,名称/Blue和数组[/Red /Yellow].

字典

字典是键值对的无序集合。key是句子,值可以是任意PDF对象。字典数据写在<<和>>之间。例:

代码语言:javascript
复制
<</One 1 /Two 2 /Three 3>>  
//One映射为1
//Two映射为2

字典是可以包含其他字典。

间接引用

为了将PDF内容拆分为单独的对象,我们使用间接引用将它们连接在一起。对对象6的间接引用写为:

代码语言:javascript
复制
6 0 R

6是对象编号,0是世代号,R是间接参考关键字。

下例中的字典使用了间接引用:

代码语言:javascript
复制
<< /Resources 10 0 R
 /Contents [4 0 R] >>

对象10和4在字典的值中被引用。

流和过滤器

流用于存储二进制数据。它们由一个字典和紧随其后的二进制数据块组成。 字典列出了数据的长度,以及其它可选参数。

从语法上讲,流的构成如下:一个字典,后跟stream关键字,换行符(或 ),零个或多个字节的数据,换行符,最后是endstream关键字。例:

代码语言:javascript
复制
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条目,它以字节为单位给出流的长度。

所有流必须是间接对象。流通常会被压缩,可用的压缩算法如下: 所有流必须是间接对象。流通常会被压缩,可用的压缩机算法如下:

代码语言:javascript
复制
/ASCIIHexDecode
/ASCII85Decode 
/LZWDecode 
/FlateDecode 
/RunLengthDecode 
/CCITTFaxDecode
/DCTDecode //JPEG有损压缩。整个JPEG文件可以放在这里,包含文件头。
/JPXDecode //JPEG2000有损和无损压缩。仅限于JPX基准功能集。

下面是一个压缩流的例子:

代码语言:javascript
复制
796 0 obj
<</Length 275 /Filter /FlateDecode>> 
stream
HTKO0÷u  //And 268 more bytes...
endstream
endobj

可以使用多个过滤器,其方法是为流的字典中的/Filter条目指定数组而不是一个名称。

例如,使用JPEG方法压缩然后使用ASCII85编码的图像可能具有以下过滤器条目:

代码语言:javascript
复制
/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之后)的线性化字典加以识别。例:

代码语言:javascript
复制
%PDF-1.4
%âãÏÓ
4 0 obj
<< /E 200967
 /H [ 667 140 ]
 /L 201431
 /Linearized 1
 /N 1
 /O 7
 /T 201230
>>
endobj

GhostScript附带的命令行程序pdfopt可以将文件线性化。例:

代码语言:javascript
复制
pdfopt input.pdf output.pdf

这会将input.pdf线性化并将结果写入output.pdf。

如何读PDF文件

要读取PDF文件,将其从一系列字节转换为内存中的“对象图”,通常有如下步骤:

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

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

下面以伪代码给出的递归数据结构可以表示一个PDF对象。

代码语言:javascript
复制
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 >>可能表示为:

代码语言:javascript
复制
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字典和文件结束标记
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/07/14 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文件布局
    • Header
      • Body
        • 交叉引用
          • Trailer
          • 词法约定
          • 对象
            • 整数和实数
              • 字串
                • 十六进制字符串
                  • 名称
                    • 布尔值
                      • 数组
                        • 字典
                          • 间接引用
                          • 流和过滤器
                          • 增量更新
                          • 对象和交叉引用流
                          • 线性化的PDF
                          • 如何读PDF文件
                          • 如何写PDF文件
                          相关产品与服务
                          文件存储
                          文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档