PDF 文件格式解析

第1章 介绍

PDF(Portable Document Format,可移植文档格式)是一种适用于印刷和在线使用的页面描述语言,最初是 Adobe 的一个内部项目,旨在创建一种与平台无关的文档交换方法。

1.1 标准

PDF 格式有几种专门的变体,其中有两种:PDF/A 和 PDF/X。

PDF/A标准(ISO 19005-1:2005)为在图书馆,国家档案馆和官僚机构中长期存档的文件定义了一套规则。

PDF/X标准是印刷行业图形交换的ISO标准系列。

1.2 版本

PDF完全向后兼容(你可以将PDF版本1.0文档加载到为PDF 1.7设计的程序中) 并且大部分向前兼容(为PDF 1.0编写的程序通常可以加载PDF 1.7文件)。

PDF 版本 Acrobat Reader 版本 推出 新功能摘要
1.0 1.0 1993 首发
1.1 2.0 1996 设备无关的颜色空间,加密(40位),文章线程,命名目标和超链接
1.2 3.0 1996 AcroForms(交互式表单),电影和声音,更多压缩方法,Unicode支持。
1.3 4.0 2000 更多色彩空间,嵌入(附加)文件,数字签名,注释,蒙版图像,渐变填充,逻辑文档结构,印前支持
1.4 5.0 2001 透明度,128位加密,更好的表单支持,XML元数据流,标记PDF,JBIG2压缩
1.5 6.0 2003 对象流和交叉引用流,用于更紧凑的文件,JPEG 2000支持,XFA表单,公钥加密,自定义加密方法,可选内容组
1.6 7.0 2004 OpenType字体,3D内容,AES加密,新颜色空间
1.7 (later ISO 32000-1:2008) 8.0 2006 XFA 2.4,新类型的字符串,公钥体系结构的扩展
1.7 Extension Level 3 9.0 2008 256位AES加密
1.7 Extension Level 5 9.1 2009 XFA 3.0.
1.7 Extension Level 8 X 2011 未知

1.3 文件组成

典型的PDF文件包含数千个对象,其中有文本和字体、矢量图形、光栅图像、色彩空间、元数据、多媒体等。

第2章 一个简单的 PDF

2.1 一个简单的 PDF

下图是一个只有一页的 PDF 文件,页面上有 “Hello,World!” 字样。

image

对于这样的一个 PDF 文件,我们可以直接使用文本编辑器来查看其内容:

%PDF-1.0
%âãÏÓ

1 0 obj
<< /Type /Pages
    /Count 1
    /Kids [2 0 R]
>>
endobj

2 0 obj
<< /Type /Page
    /MediaBox [0 0 612 792]
    /Resources 3 0 R
    /Parent 1 0 R
    /Contents [4 0 R]
>>
endobj

3 0 obj
<< /Font
    << /F0
        << /Type /Font
            /BaseFont /Times-Italic
            /Subtype /Type1 >>
    >>
>>
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
<< /Type /Catalog
   /Pages 1 0 R
>>
endobj

xref
0 6
0000000000 65535 f 
0000000015 00000 n 
0000000074 00000 n 
0000000182 00000 n 
0000000281 00000 n 
0000000410 00000 n 

trailer
<< /Size 6
  /Root 5 0 R
>>
startxref
459
%%EOF

如果事先没有了解过 PDF 格式,我们肯定无法理解上面这些符号所表达的意思。不过,有一点可以确定的是,我们可以在其中找到熟悉的 “Hello, World!” 字样。

本章我们就借助这个文件来了解 PDF 的基本语法和结构。

2.1 语法

PDF 文档的内容由多个对象组成,其中包括五种基本对象:

  • 整数和实数,例如 42 和 3.1415
  • 字符串,括在括号中,例如前面的 (Hello, World!)
  • 名称 Names ,他们带有/,例如 /count
  • 布尔值,由关键字 true 和 false 表示
  • null 对象,由关键字 null 表示

和三种复合对象:

  • 数组 Arrays,包含其他对象的有序集合,如[1 0 0 0]
  • 字典 Dictionaries,包含 <name,object> 对的无序集合,例如 <</Contents 4 0 R /Resources 5 0 R>> 表示将/Contents映射到间接引用 4 0 R,将 /Resources 映射到间接引用 5 0 R
  • 流 Streams,保存图片、字体等二进制数据,连带一个描述数据属性(如长度、压缩参数)的字典。
  • 间接引用 Indirect Reference,一种将对象链接在一起的方式

整数和实数

整数被写成一个或多个十进制数字 0-9,前面可包含加减号:

0    +1    -1    63

实数被写为一个或多个十进制数字,前面可包含加减号,以及一个小数点,可以在开头,中间或结尾:

0.0    0.    .0    -0.004    65.4

字符串

字符串包含一系列字节,写在括号之间:

(Hello, World!)

如果字符串中包含反斜杠''和括号'()',则必须在它们前面加上反斜杠进行转义:

 (Some \\ escaped \(characters) 

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

 <4F6Eff3> // 字节 0x4F, 0x6E, 0xFF, and 0x30 

如果序列个数为奇数,则在最后加一个0。

名称 Names

名称在整个PDF中有使用,用于定义对象和字典的键。名称前面引入了一个正斜杠,例如:

/Count

名称中一般是不包含空格的,如果需要的话,我们可以使用 "#20" 符号:

/Websafe#20Dark#20Green //  /Websafe Dark Green 

名称区分大小写,/Count 和 /count 是不一样的。

布尔值

PDF允许布尔值为 true 和 false 。它们经常在字典条目中用作标志。

数组

数组表示PDF对象的有序集合:

[0 0 400 500]

数组中对象不一定都是同一类型,也可以包括其他数组例如:

[/Green /Blue [/Red /Yellow]]

其中包含三个对象:名称 /Green 、名称 /Blue 和一个包含两个名称的数组 [/Red /Yellow]

字典 Dictionaries

字典表示由键值对组成的无序集合。键是名称,值可以是任何 PDF 对象,写在 << 和 >>之间,例如:

<</One 1 /Two 2 /Three 3>>

字典中也可以包含其他字典,嵌套字典构成了大多数PDF文件中大量的非图形结构化数据。

间接引用 Indirect References

为了将 PDF 内容拆分成独立的对象(以便按需读取),我们将它们与间接引用连接在一起。例如对象6的间接引用写为:

6 0 R

其中,6是对象编号,0是世代号(基本是0),R 是间接引用关键字。

下面是一个使用间接引用的典型字典:

 << /Resources 10 0 R /Contents [4 0 R] >> 

名称 /Resources 映射到了对象10的间接引用,/Contents 映射到了对象4的间接引用。

流 Streams

流用于保存二进制数据,它们由一个字典和一个二进制数据块组成。字典根据流的特定用途列出数据的长度和其他可选参数。

在结构上,流包含一个字典,后面跟随 stream 关键字,然后是0个多个字节的数据,最后是 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 条目,以字节为单位给出流的长度。

2.2 文件结构

一个简单有效的 PDF 文件按顺序包含4个部分:

  • header(文件头):提供 PDF 版本号;
  • body(文件体):包含页面,图形内容和大部分辅助信息,全部编码为一系列对象;
  • cross-reference table(交叉引用表):列出文件中每个对象的位置便于随机访问;
  • trailer(文件尾):通过它可以在不处理整个文件的情况下找到文件的各个部分。

文件头 Header

PDF 文件的第一行给出文档的版本号:

%PDF-1.0

这将文件定义为1.0版本。PDF 是向后兼容的,很大程度上也是向前兼容的,所以无论版本号是多少,大多数PDF程序都会尝试读取其内容。

由于PDF文件几乎总是包含二进制数据,为了允许传统文件传输程序确定文件是二进制文件,通常在标头中包含一些字符代码高于127的字节。例如:

 %âãÏÓ 

百分号表示另一个标题行,其他几个字节是超过127的任意字符代码。 因此,我们示例中的整个header是:

%PDF-1.0
%âãÏÓ

文件体 Body

文件体由一系列对象组成,每个对象在一行上都有一个对象编号(object number),世代号(generation number)和 obj 关键字,在这之后跟随一个 endobj 关键字:

1 0 obj
<< /Type /Pages
    /Count 1
    /Kids [2 0 R]
>>
endobj

在此处,对象编号是 1,世代号是 0(几乎总是),它的内容位于 1 0 obj 和 endobj 两行之间,是一个字典。字典在后面会做介绍。

交叉引用表 Cross-Reference Table

交叉引用表列出了文件正文中每个对象的字节偏移量。这允许随机访问对象,因此不必按顺序读取它们,一个从未使用过的对象就永远不会被读取。这尤其意味着,即使在大型文件上,像计算PDF文档中的页数这样的简单操作也可以很快。

交叉引用表由一个表示条目数的标题行组成,然后是一个特殊条目,接下来是文件体中的每个对象,如下所示:

xref // 交叉引用表从这里开始
0 6 // 表中有6个条目,从0开始
0000000000 65535 f // 特别条目
0000000015 00000 n // 对象1的字节偏移量为15
0000000074 00000 n // 对象2的字节偏移量为74
0000000182 00000 n // ...
0000000281 00000 n 
0000000410 00000 n // 对象5的字节偏移量为409

文件尾 Trailer

文件尾的第一行是 trailer 关键字。之后是 trailer 字典,其中至少包含 /Size 条目(给出交叉引用表中条目的数量)和 /Root 条目(给出文档目录的对象编号,它是Body中对象图的根元素)。

接下来是 startxref 关键字,然后是一个表示交叉引用表字节偏移量的数字,最后是 %%EOF ,PDF 文件的结束标记,如下:

trailer // trailer 关键字
// trailer 字典
<< /Size 6 
  /Root 5 0 R
>>
startxref // startxref 关键字
459 // 交叉引用表字节偏移量
%%EOF // 文件结束标记

Trailer 是从文件尾向前读取的: 先找到文件结束标记,然后提取交叉引用表的字节偏移量,接着解析trailer字典。trailer 关键字标记了trailer的上限。

2.3 PDF文件的读取流程

为了读取一个PDF文件,将其从一个扁平的字节序列转变成一个内存中的对象图形,一般有以下几个步骤:

  1. 从文件开头读取 header,确认它确实是一个PDF文档并检索其版本号;
  2. 接着通过从文件结尾向前搜索,找到文件结束标记。然后读取交叉引用表的字节偏移量和trailer字典;
  3. 接着通过读取交叉引用表,就可以知道文件中每个对象的位置;
  4. 到这一步,所有的对象都可以根据需要来读取和解析;
  5. 现在,可以提取页面,解析图形内容,提取元数据等等。

第3章 文档结构

本章我们来了解PDF的逻辑结构: trailer字典,文档信息字典,文档目录和页面树。

下图是一个典型文档的逻辑结构:

image

3.1 Trailer 字典

这个字典存在于文档尾而不是Body中,是程序读取PDF文档的第一步。以下是字典中的一些重要条目(*表示必需条目):

值类型
/Size* 整数 文件交叉引用表中的条目总数(通常等于文件中的对象数加1)
/Root* 间接引用字典 文档目录
/Info 间接引用字典 文档信息字典
/ID 2个字符串的数组 唯一标识工作流中的文件。第一个字符串在首次创建文件时确定,第二个字符串在工作流系统修改文件时进行修改

例如:

 << 
 /Size 421 
 /Root 377 0 R 
 /Info 375 0 R 
 /ID [<75ff22189ceac848dfa2afec93deee03> <057928614d9711db835e000d937095a2>] 
 >> 

一旦处理了trailer字典,我们就可以继续读取文档信息字典和文档目录。

3.2 文档信息字典

文档信息字典包含文件的创建和修改日期,以及一些简单的元数据。

下表是文档信息字典的条目:

值类型
/Title 文本字符串 文档标题,与第一页上显示的任何标题无关
/Subject 文本字符串 文件的主题。同样,这只是元数据,没有关于内容的特定规则
/Keywords 文本字符串 与此文档相关的关键字。没有给出关于如何构建的建议
/Author 文本字符串 文件作者的姓名
/CreationDate 日期字符串 文档创建的日期
/ModDate 日期字符串 上次修改文档的日期
/Creator 文本字符串 最初创建此文档的程序的名称
/Producer 文本字符串 将此文件转换为PDF的程序的名称

下面是一个典型的文档信息字典:

 << 
 /ModDate (D:20060926213913+02'00') 
 /CreationDate (D:20060926213913+02'00') 
 /Title (catalogueproduit-UK.qxd) 
 /Creator (QuarkXPress: pictwpstops filter 1.0) 
 /Producer (Acrobat Distiller 6.0 for Macintosh) 
 /Author (James Smith) 
 >> 

其在PDF浏览器中展示如下:

image

3.3 文档目录

文档目录是主对象图的根对象,通过间接引用可以找到所有其它对象。下表介绍了文档目录的一些必需条目的可选条目(*表示必需条目):

值类型
/Type* 名称 必须是 /Catalog
/Pages* 间接引用字典 页面树的根节点
/PageLabels number tree 一个数字树,给出了该文档的页面标签。 这种机制允许文档中的页面具有比1,2,3更复杂的编号
/Names 字典 名字词典。它包含各种名称树,它们将名称映射到实体,以避免使用对象编号来直接引用
/Dests 字典 将名称映射到目标的字典,和超链接相关
/ViewerPreferences 字典 一个查看器首选项字典,允许指定在屏幕上查看文档时PDF查看器的行为,例如打开文档的页面,初始查看比例等
/PageLayout 名称 指定PDF查看器要使用的页面布局
/PageMode 名称 指定PDF查看器要使用的页面模式
/Outlines 间接引用字典 大纲字典是文档大纲的根,通常称为书签
/Metadata 间接引用流 文档的XMP元数据

3.4 页面和页面树

PDF 文档的页面字典中包含了绘制图形和文本内容的指令,还包括页面尺寸以及一些定义剪裁的方框等等。

下表列出了页面字典的条目(*表示必需条目):

值类型
/Type* 名称 必须是 /Page
/Parent* 间接引用字典 页面树中该节点的父节点
/Resources 字典 页面的资源(字体,图像等)。如果完全省略此条目,则资源将从页面树中的父节点继承。如果确实没有资源,请包含此条目但使用空字典
/Contents 间接引用流或间接引用流数组 页面的图形内容。如果缺少此条目,则页面内容为空
/Rotate 整数 浏览页面的顺时针旋转角度,必须是90的倍数,默认值是0
/MediaBox* 矩形 页面的媒体框,也就是纸张大小。如果缺少此条目,则从父节点中继承
/CropBox 矩形 页面的裁剪框。这定义了在显示或打印页面时默认可见的页面区域。如果不存在,则将其值与媒体框相同

媒体框和其他方框的矩形数据用一个包含4个数字的数组表示。数组的前两个数字表示矩形的左下角坐标,后两个数字表示右上角坐标,例如:

 /MediaBox [0 0 500 800] 
 /CropBox [100 100 400 700]

定义了一个500x800磅的页面,以及一个在页面每侧裁除100磅的裁剪框。

页面使用页面树而不是一个简单的数组进行链接。这个树形结构使得在一个包含成百上千个页面的文档中查找一个指定的页面变得更快。

下表是页面树的根节点或者中间节点的条目(*表示必需条目):

值类型
/Type* 名称 必须是 /Pages
/Kids* 间接引用数组 此节点的直接子节点
/Count* 整数 此节点最终的页节点的数量
/Parent 树节点的间接引用 引用此节点的父节点。 如果不是页面树的根节点,则必须存在

下图是一个7页的页面树结构:

image

其对应的 PDF 对象如下:

1 0 obj // 根节点
<< /Type /Pages /Kids [2 0 R 3 0 R 4 0 R] /Count 7 >>
endobj

2 0 obj // 中间节点
<< /Type /Pages /Kids [5 0 R 6 0 R 7 0 R] /Parent 1 0 R /Count 3 >>
endobj

3 0 obj // 中间节点
<< /Type /Pages /Kids [8 0 R 9 0 R 10 0 R] /Parent 1 0 R /Count 3 >>
endobj

4 0 obj // 页面 7
<< /Type /Page /Parent 1 0 R /MediaBox [0 0 500 500] /Resources << >> >>
endobj

5 0 obj // 页面 1
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 500 500] /Resources << >> >>
endobj

6 0 obj // 页面 2
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 500 500] /Resources << >> >>
endobj

7 0 obj // 页面 3
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 500 500] /Resources << >> >>
endobj

8 0 obj // 页面 4
<< /Type /Page /Parent 3 0 R /MediaBox [0 0 500 500] /Resources << >> >>
endobj

9 0 obj // 页面 5
<< /Type /Page /Parent 3 0 R /MediaBox [0 0 500 500] /Resources << >> >>
endobj

10 0 obj // 页面 6
<< /Type /Page /Parent 3 0 R /MediaBox [0 0 500 500] /Resources << >> >>
endobj

3.5 合在一起

下面的例子是一个手动创建的3页文档,包含文档信息字典和页面树:

%PDF-1.0
1 0 obj // 页面树根节点
<< /Kids [2 0 R 3 0 R] /Type /Pages /Count 3 >>
endobj

4 0 obj // 页面1的内容
<< >>
stream
1. 0.000000 0.000000 1. 50. 770. cm BT /F0 36. Tf (Page One) Tj ET
endstream
endobj

2 0 obj // 页面 1
<<
   /Rotate 0 
   /Parent 1 0 R 
   /Resources
     << /Font << /F0 << /BaseFont /Times-Italic /Subtype /Type1 /Type /Font >> >> >> 
   /MediaBox [0.000000 0.000000 595.275590551 841.88976378]
   /Type /Page
   /Contents [4 0 R]
>>
endobj

5 0 obj // 文档目录
<< /PageLayout /TwoColumnLeft /Pages 1 0 R /Type /Catalog >> 
endobj

6 0 obj // 页面 3
<<
  /Rotate 0 
  /Parent 3 0 R 
  /Resources
    << /Font << /F0 << /BaseFont /Times-Italic /Subtype /Type1 /Type /Font >> >> >> 
  /MediaBox [0.000000 0.000000 595.275590551 841.88976378]
  /Type /Page
  /Contents [7 0 R] 
>>
endobj

3 0 obj // 中间树节点,指向页面2和3
<< /Parent 1 0 R /Kids [8 0 R 6 0 R] /Count 2 /Type /Pages >> 
endobj

8 0 obj // 页面 2
<<
  /Rotate 270
  /Parent 3 0 R
  /Resources
     << /Font << /F0 << /BaseFont /Times-Italic /Subtype /Type1 /Type /Font >> >> >> 
  /MediaBox [0.000000 0.000000 595.275590551 841.88976378]
  /Type /Page
  /Contents [9 0 R]
>>
endobj

9 0 obj // 页面2的内容
<< >>
stream
q 1. 0.000000 0.000000 1. 50. 770. cm BT /F0 36. Tf (Page Two) Tj ET Q
1. 0.000000 0.000000 1. 50. 750 cm BT /F0 16 Tf ((Rotated by 270 degrees)) Tj ET 
endstream
endobj

7 0 obj // 页面3的内容
<< >>
stream
1. 0.000000 0.000000 1. 50. 770. cm BT /F0 36. Tf (Page Three) Tj ET
endstream
endobj

10 0 obj // 文档信息字典
<<
   /Title (PDF Explained Example) 
   /Author (John Whitington) 
   /Producer (Manually Created) 
   /ModDate (D:20110313002346Z) 
   /CreationDate (D:2011)
>>
endobj

xref
0 11
trailer // trailer 字典
<<
  /Info 10 0 R
  /Root 5 0 R
  /Size 11
  /ID [<75ff22189ceac848dfa2afec93deee03> <057928614d9711db835e000d937095a2>]
>> 
startxref 
0
%%EOF

上面的例子的对象图如下:

image

实际查看效果如下图:

image

第4章 文本

本章,我们来了解如何使用操作符和操作数在页面上显示文本。默认情况下,PDF坐标系的原点位于页面的左下角,x和y分别向右和向上增加。

在页面上打印文本需要:

  1. 选择字体;
  2. 选择位置,大小和方向;
  3. 选择间距,颜色,文本渲染模式和其他参数;
  4. 从字体中选择字符,并在页面上显示。

一段文本包括在 BT(begin text)和 ET(end text)操作符之间。用于在页面的内容流中显示文本的操作符可能仅出现在BT和ET之间。 但是,用于改变文本状态的操作符不受这种限制。

例如前面的“Hello, World!”:

1. 0. 0. 1. 50. 700. cm // 文本位置为 (50, 700) 
BT // 文本块开始
  /F0 36. Tf // 选择 /F0 字体,字号为 36磅
  (Hello, World!) Tj // 在当前位置显示字符串
ET // 文本块结束

在这里,我们使用带有字体名称和大小的 Tf 操作符来选择字体,使用 Tj 操作符来显示文本字符串,依靠图形运算符 cm 来定位文本。这些操作符及其说明可以在下表中找到,每个操作符前面都有0个或多个操作数:

操作符 操作数 说明
Tf font, size 选择对应字号的字体
Tj string 在当前位置显示字符串
T* - 将文本位置移动到下一行
Tc charSpace 设置字符间距
Tw wordSpace 设置字间距
TL leading 设置前导的文本
...

下面的例子中我们使用各种操作符来显示一些文本:

BT
/F0 36 Tf // 选择36磅的 /F0 字体
1 0 0 1 120 350 Tm // 将文本位置设置为(120,350)
50 TL // 将前导设置为50磅
(Character and Word Spacing) Tj T* //显示字符串并移动到下一行
3 Tc // 设置字符间距为3磅
(Character and Word Spacing) Tj T* // 再次绘制字符串
10 Tw // 设置字间距为10磅
(Character and Word Spacing) Tj // 第三次绘制字符串
ET

显示结果如下:

image

参考

[1] PDF Explained

[2] PDF Reference 1.7

[3] https://zxyle.github.io/PDF-Explained/

posted @ 2023-01-30 10:24  theyangfan  阅读(6666)  评论(0编辑  收藏  举报