参考:https://resources.infosecinstitute.com/topic/pdf-file-format-basic-structure/
PDF是一种可移植的文档格式,可用于显示包含文本,图像,多媒体元素,网页链接等内容。PDF文件格式规范链接:https://opensource.adobe.com/dc-acrobat-sdk-docs/。官方链接中仅仅文件格式的文档就有800多页,因此通读该文档并不是意见容易的事。
PDF不仅具有文本功能,还具有更多的功能:它可以包含图像和其他多媒体元素,受密码保护,执行JavaScript等。下图显示了PDF文件的基本结构:
下文中[]中的内容为上图中的结构体。
Header
[PDFHeader]这是PDF文件的第一行,它指定文档使用的所用PDF规范的版本号.此处PDF文档版本为1.7.
[PDFComment] 这部分是PDF文档的注释部分
Body
在PDF文档的正文中,有一些对象通常包括文本流,图像,其他多媒体元素等。“正文”部分用于保存所有显示给用户的文档数据。
即[PDFObj] sPdfObj[0]-sPdfObj[12]都是Body部分。
xref table
这是交叉引用表,其中包含对文档中所有对象的引用。交叉引用表的目的是允许它随机访问文件中的对象,因此我们无需阅读整个PDF文档即可找到特定的对象。每个对象由交叉引用表中的一个条目表示,该条目始终长20个字节。让我们来看一个例子:
xref
0 14
0000000000 65536 f
0000000017 00000 n
0000000066 00000 n
0000000122 00000 n
0000000258 00000 n
0000000367 00000 n
0000000500 00000 n
0000000831 00000 n
0000001013 00000 n
0000001197 00000 n
0000010222 00000 n
0000010294 00000 n
0000010366 00000 n
0000010431 00000 n
我们可以通过使用文本编辑器简单地打开PDF并滚动到文档底部来显示PDF文档的交叉引用表。
0 14
这行中的第一个数字0对应于对象编号,而第二列显示当前子节中的对象数量14。
0000000000 65536 f
0000000017 00000 n
0000000066 00000 n。。。。。。
每个对象由一个条目表示,该条目长20个字节(包括CRLF)。
前10个字节是对象从PDF文档开始到该对象开始的偏移量。接下来是一个空格分隔符,其中另一个数字指定对象的世代号。之后,还有另一个空格分隔符,后跟字母“ f”或“ n”以指示对象是空闲的还是正在使用的。
Trailer
PDF Trailer 指定读取PDF文档的应用程序应如何找到交叉引用表和其他特殊对象。所有PDF阅读器都应从文件末尾开始阅读PDF。以下是一个Trailer示例:
trailer
<<
/Root 1 0 R
/Info 3 0 R
/Size 14/ID[<EA61F2FE98704E5009E01A59031F93DD><EA61F2FE98704E5009E01A59031F93DD>]>>
startxref
10750
%%EOF
PDF文档的最后一行包含“ %% EOF”文件字符串的结尾。在文件标记末尾之前,有一行带有startxref字符串的行,用于指定从文件开头到交叉引用表的偏移量。在此例中,交叉引用表从偏移量10750字节开始。在此之前是Trailer字符串,用于指定当前部分的开始。此节的内容嵌入在<<和>>字符内(这是一个接受键值对的字典)。
我们可以看到此部分定义了几个键,每个键用于一个特定的动作。此部分指定以下键:
- / Size [integer]:指定交叉引用表中的条目数(也对更新部分中的对象进行计数)。使用的号码不应是间接引用。
- / Prev [integer]:指定从文件开头到上一个交叉引用节的偏移量,如果有多个交叉引用节,则使用此偏移量。该数字应为交叉引用。
- / Root [dictionary]:指定文档目录对象的引用对象,这是一个特殊对象,其中包含指向不同种类的其他特殊对象的各种指针(稍后会对此进行更多介绍)。
- / Encrypt [dictionary]:指定文档的加密字典。
- / Info [dictionary]:指定文档信息字典的参考对象。
- / ID [array]:指定由两个字节的未加密字符串组成的数组,这些字符串构成文件标识符。
- / XrefStm [integer]:指定从文件开头到解码流中的交叉引用流的偏移量。这仅存在于混合引用文件中,如果我们也想打开文档,即使应用程序不支持压缩的参考流,也可以指定该文件。
我们必须记住,如果以后再更新PDF文档,则可以修改初始结构。该更新通常在文件末尾附加其他元素。
增量更新
PDF的设计考虑了增量更新,因为我们可以在不重写整个文件的情况下将一些对象附加到PDF文件的末尾。因此,可以快速保存对PDF文档的更改。下图显示了PDF文档的新结构:
我们可以看到PDF文档仍然包含原始的标题,正文,交叉引用表和trailer。此外,PDF文档中还添加了其他正文,交叉引用和trailer部分。其他交叉引用部分将仅包含已更改,替换或删除的对象的条目。删除的对象将保留在文件中,但将带有“ f”标记。每个trailer都需要以“ %% EOF”标签终止,并应包含/ Prev条目,该条目指向上一个交叉引用部分。
在PDF版本1.4和更高版本中,我们可以在文档的目录字典中指定版本条目,以覆盖PDF标头中的默认版本。
例子
本篇文章所用示例文档下载地址:https://download.csdn.net/download/lacoucou/15709804
此文档是使用wps创建的,内容仅仅有一行 ,其二进制结构如下图:
010editor 模版解析出来的结构:
PDF文件数据类型
PDF 文件基本元素是 PDF 对象(PDF Object), PDF 对象包括直接对象(Direct Object)和间接对象(Indirect Object)。直接对象如下八种类型;间接对象,又叫 labelled object
, 嵌套在关键词 n 0 obj
和 endobj
之间, 是用一种表示来标识一个 PDF 对象,通过标识来让别的 PDF 对象引用,这个标识叫做间接对象的 ID. https://lazymind.me/2017/10/pdf-structure/
直接对象类型
-
Boolean value(布尔)
布尔类型,值只能是
true
和false
。 -
Integer and Real number(数值)
数值类型,包括整数和实数,与普通编程语言中的数值类型大体相同。
-
String(字符串)
字符串类型,包括包含在圆括号
( )
内的文字字符串(literal string
)和包含在单尖括号< >
内的十六进制字符串(hexadecimal string
)两种。例:
1. (Hello World) 2. <9ADCF1>
-
Name(名字?)
名字类型,用字符组成的字符串,用
/
作为前导符号,在 PDF 文件中具有唯一性,相同的名字表示相同的对象(the same sequence of character denotes the same object
)。常见用在Dictionary
里面作Key
,用来表示对象名称。例:
/Page /Kid
-
Array(数组)
数组类型,存在于方括号
[ ]
内,元素可以是除Stream
外的所有类型。PDF 中数组只支持一维数组。例:
[/Page false 17 (hello)] 该数组包含了4种类型元素
-
Dictionary(字典)
字典类型,包含在双尖括号
<< >>
内,每两个元素为一对,第一个为key
, 第二个为value
,key
只能是Name
类型,value
可以是任意类型,即可以嵌套为Dictionary
。例:
<</Page 1 0 obj /Filter /FlateDecode /Name (Hello)>>
-
Stream(流对象)
流对象,是用字节表示的序列,长度理论上没限制。包含在
stream
和endstream
之间。以CRLF
或LF
结尾,不能单独以CR
结尾。dicionary
里的内容用来描述该stream
的相关信息。dictionary stream ...... endstream
-
Null object(空对象)
空对象类型,用关键词
null
表示。
间接对象类型
使用 unique object identifier 来表示,方便其他对象引用。结构如下:
12 0 obj
........
endobj
第一行第一个 12
规定为 positive integer
, 表示对象 ID; 第二个 0
表示生成号(generation number
),通常为0;第三个为固定 obj
表示,以最后一行 endobj
表示结束。中间 ......
表示内容。其他地方引用该对象时,使用如下格式,其中 R
为关键字:
12 0 R
文件结构
PDF文档由PDF文件的主体部分中包含的对象组成。PDF文档中的大多数对象都是字典。文档的每个页面都由页面对象表示,页面对象是一个字典,其中包含对页面内容的引用。页面对象连接在一起并形成页面树,该页面树在文档目录中使用间接引用进行声明。
在上图中,我们可以看到文档目录包含对页面树,大纲层次结构,文章线程,命名目的地和交互式表单的引用( page tree, outline hierarchy, article threads, named destinations and interactive form)。我们不会详细介绍每个部分的功能,但仅介绍最重要的部分,即“页面树”。
Document catalog
从上图可以看出,文档目录 是PDF文档中对象的根。我们已经说过,“PDF”尾部中的/ Root元素指定了文档目录。文档目录包含对其他定义文档内容的对象的引用。它还包含声明如何在屏幕上显示文档的信息。文档目录中的条目如下:
- / Type:目录描述的PDF对象的类型(在我们的示例中,这是Catalog,因为这是文档目录对象)。
- / Version:文档所依据的PDF规范的版本。
- / Extensions:有关本文档中开发人员扩展的信息。
- / Pages:对对象的间接引用,该对象是文档页面树的根。
- /Dests:对对象的间接引用,该对象是指定的目标对象的根。
- / Outlines:对大纲目录对象的间接引用,该对象是文档大纲层次结构的根。
- / Threads:对表示文档文章线程的线程词典数组的间接引用。
- / Metadata:对包含文档元数据的元数据流的间接引用。
其他 Catalog字典中,常用的字段一般有以下一些:
-
字段
类型
值
Type
name
(必须)必须为Catalog。
Version
name
(可选)PDF文件所遵循的版本号(如果比文件头指定的版本号高的话)。如果这个字段缺省或者文件头指定的版本比这里的高,那就以文件头为准。一个PDF生成程序可以通过更新这个字段的值来修改PDF文件版本号。
Pages
dictionary
(必须并且必须为间接对象)当前文档的页面集合入口。
PageLabels
number tree
(可选) number tree,定义了页面和页面label对应关系。
Names
dictionary
(可选)文档的name字典。
Dests
dictionary
(可选;必须是间接对象)name和相应目标对应关系字典。
ViewerPreferences
dictionary
(可选)阅读参数配置字典,定义了文档被打开时候的行为。如果缺省,则使用阅读器自己的配置。
PageLayout
name
(可选) 指定文档被打开的时候页面的布局方式。SinglePageDisplay 单页OneColumnDisplay 单列TwoColumnLeftDisplay 双列,奇数页在左TwoColumnRightDisplay 双列,奇数页在右TwoPageLeft 双页,奇数页在左TwoPageRight 双页,奇数页在右缺省值: SinglePage.
PageMode
name
(可选) 当文档被打开时,指定文档怎么显示UseNone 目录和缩略图都不显示UseOutlines 显示目录UseThumbs 显示缩略图FullScreen 全屏模式,没有菜单,任何其他窗口UseOC 显示Optional content group 面板UseAttachments显示附件面板缺省值: UseNone.
Outlines
dictionary
(可选;必须为间接对象)文档的目录字典
Threads
array
(可选;必须为间接对象)文章线索字典组成的数组。
OpenAction
array or dictionary
(可选) 指定一个区域或一个action,在文档打开的时候显示(区域)或者执行(action)。如果缺省,则会用默认缩放率显示第一页的顶部。
AA
dictionary
(可选)一个附加的动作字典,在全局范围内定义了响应各种事件的action。
URI
dictionary
(可选)一个URI字典包含了文档级别的URI action信息。
AcroForm
dictionary
(可选)文档的交互式form (AcroForm)字典。
Metadata
stream
(可选;必须是间接对象)文档包含的元数据流。
我们可以看到还有许多其他条目属于文档目录,但是这里不再对其进行描述。读者可以查看我们的资源以了解详细信息。下面显示了文档目录的示例:
1 0 obj
<</Type/Catalog/Pages 2 0 R >>
endobj
Page tree
通过页面树可以访问文档的页面,该页面树定义了PDF文档中的所有页面。该树包含代表PDF文档页面的节点,该节点可以有两种类型:中间节点和叶节点。中间节点也称为页面树节点,而叶节点称为页面对象。
最简单的页面树结构可以由单个页面树节点组成,该节点直接引用所有页面对象(因此所有页面对象都是叶子)。
页面树中的每个节点必须具有以下条目:
- / Type:此对象描述的PDF对象的类型(在本例中为Pages ,因为我们正在谈论页面树节点)。
- /Parent:应该在所有页面树节点中存在,但在根目录中除外,在根目录中不得存在该条目。此项指定其父项。
- / Kids:应该出现在除叶子以外的所有页面树节点中,并指定可从当前节点直接访问的所有子元素。
- / Count:指定在后续页面树中作为该节点后代的叶节点的数量。
我们必须记住,页面树与PDF文档中的任何内容都不相关,例如页面或章节。
2 0 obj
<</Type/Pages/Count 1/Kids[ 4 0 R ]>>
endobj4 0 obj
<</Type/Page/Parent 2 0 R /MediaBox[ 0 0 595.3 841.9]/Resources 10 0 R /Contents 13 0 R >>
endobj
字段 |
类型 |
值 |
---|---|---|
Type |
name |
(必须)必须是Page。 |
Parent |
dictionary |
(必须;并且只能是间接对象)当前page节点的直接父节点page tree 。 |
LastModified |
date |
(如果存在PieceInfo字段,就必须有,否则可选)记录当前页面被最后一次修改的日期和时间。 |
Resources |
dictionary |
(必须; 可继承)记录了当前page用到的所有资源。如果当前页不用任何资源,则这是个空字典。忽略所有字段则表示继承父节点的资源。 |
MediaBox |
rectangle |
(必须; 可继承)定义了要显示或打印页面的物理媒介的区域(default user space units) |
CropBox |
rectangle |
(可选; 可继承)定义了一个可视区域,当前页被显示或打印的时候,它的内容会被这个区域裁剪。默认值就是 MediaBox。 |
BleedBox |
rectangle |
(可选) 定义了一个区域,当输出设备是个生产环境( production environment)的时候,页面显示的内容会被裁剪。默认值是 CropBox. |
Contents |
stream or array |
(可选) 描述页面内容的流。如果这个字段缺省,则页面上什么也不会显示。这个值可以是一个流,也可以是由几个流组成的一个数组。如果是数组,实际效果相当于所有的流是按顺序连在一起的一个流,这就允许PDF生成的时候可以随时插入图片或其他资源。流之间的分割只是词汇上的一个分割,并不是逻辑上或者组织形式的切割。 |
Rotate |
integer |
(可选; 可继承) 顺时钟旋转的角度数,这个必须是90的整数倍,默认是0。 |
Thumb |
stream |
(可选)定义当前页的缩略图。 |
Annots |
array |
(可选) 和当前页面关联的注释。 |
Metadata |
stream |
(可选) 当前页包含的元数据。 |
在我们这个例子中,所有对象的引用关系如下图:
上图内容解释:
0bj0 代表对象编号,后边的色块为对象包含的内容。