今天我们认为高保真的PDF文件交换是理所当然的,因为知道这里发送的文件在哪里看起来都是相同的,并且它可以在屏幕上和纸上同等地显示。
但其他文件交换情况并非如此。例如word,当进行文字排版并设置相应字体后,在其他机器上若没有相应字体,则会面临显示不一致问题。
典型的PDF文件包含数千个对象,多种压缩机制,不同的字体格式,以及矢量和光栅图形的混合以及各种元数据和辅助内容。我们在这里简要介绍这些元素,以供下文使用。
PDF完全向后兼容(你可以将PDF版本1.0文档加载到为PDF1.7设计的程序中)。并且大部分向前兼容(为PDF1.0编写的程序通常可以加载PDF1.7文件)。确保前向兼容性是因为读者忽略了他们不理解的内容-只有在引入新的压缩方法或对象存储机制时才会被忽略。自2003年的PDF1.5以来,这种变化很小。如下表格总结了PDF版本及其功能。
PDF广泛用于各行各业,我们在这里描述一些。
在本书中,我们使用各种软件来帮助我们举例。幸运的是,你需要的一切都是免费提供的。你需要一个PDF查看器:
有个关键的命令行工具:
本文适合:
我们将在文本编辑器中手动构建PDF内容。然后我们将使用免费的pdftk程序将其转换为有效的PDF文件,并在PDF查看器中查看输出。
PDF文件至少包含三种不同的部分:
文件结构包括:
文档内容包括由以下元素构建的对象:
页面内容是运算符列表,每个运算符前面都有零个或多个操作数。
如下是一系列操作符,用于在36号字体选择/F0字体并放置当前位置的文字:
/F036.0Tf(Hello,World)Tj这里,Tf和Tj是运算符,而/F0,36.0和(Hello,World)是操作数。
你可以看到一些语法元素(例如,名称和字符串)是共享的跨页面内容。
我们将要构建的示例只是最简单有意义的PDF文件。我们会:
然而,它还需要另外较多的元素。除上所述,最小的PDF文档还必须包含许多基本部分:
这种安排如下图所示。
在编写我们的示例文件时,我们将对许多文件结构使用不完整的值,依靠pdftk来填写细节。例如,我们手动编写交叉引用表是不切实际的。
文件头通常由两行组成。第一行将文件标识为PDF和给出它的版本号:
%PDF-1.0%PDF版本号为1.0的文件头第二行很难输入文本编辑器,因为它包含不可打印的字符。我们将有pdftk为我们处理。
pdf文件读取是从下往上进行读取。
最后一行,文件结束标记%%EOF。
向上两行,给出交叉引用表开始的字节偏移量(我们写0,pdftk将进行替换处理)。
再向上是trailer部分,给出了交叉引用表的行数,以及文档目录的引用对象。
交叉引用表在示例中是xref开头至trailer中间部分,它给出了文件中的每个对象的字节偏移量。我们将用pdftk为我们填写此内容。
由trailer获得的文档目录引用对象为50R,则向上找到50obj对象。该对象包含的是文档目录的根对象图。
流对象由字典后跟原始数据流组成,包含一个一系列PDF操作数和运算符。通常,这将被压缩以减少文件大小,但我们手动输入,所以我们不压缩它。我们还必须以字节为单位指定流的长度(pdftk将为我们添加所需的/Length条目到流字典)。
现在我们准备将这些部分放在一起了
无效的hello-broken.pdfPDF文件适合手动创建
注:Reader2023.006.20380ChineseWindows(64Bit)已经可以直接打开hello-broken.pdf文件。打开后关闭时,会提示是否需要保存。查看保存的PDF发现使用PDF1.6规范并且已经线性化。
我们可以使用免费的pdftk工具来修复hello-broken.pdf文件,将输出写入hello.pdf:
pdftkhello-broken.pdfoutputhello.pdf
pdftk读取文件及其对象,并为缺失部分计算正确的数据,生成有效文件。注意一些语法的间距和格式已经改变(每个PDF制做人对此有不同的选择)。
完成的PDF文件hello.pdf。你可以使用文本编辑器查看现有的PDF文件。但是,有些数据(例如构成页面内容的图形运算符)很可能被压缩,因此不可读。
一个简单有效的PDF文件按顺序包含四个部分:
我们从上文经pdftk处理后的pdf作为示例进行讲解。四个部分中的每一部分的第一行都有注释。
在我们的例子中,节点是PDF对象,链接是间接引用。读一个PDF文档是在文件中创建PDF对象的图形的过程。这个图是直接链接只走一条路。
我们现在依次仔细研究这四个部分中的每一个,使用上图作为参考。
PDF文件的第一行给出文档的版本号。在我们的示例中,是:
%PDF-1.0这将文件PDF版本定义为1.0。PDF是向后兼容的,它在很大程度上也是向前兼容的,因此PDF1.5的程序可以读取PDF1.3文档。所有大多数PDF程序都会尝试读取任何PDF文件,无论假设的版本号是什么。
由于PDF文件几乎总是包含二进制数据,因此如果更改行结尾(例如,如果文件通过FTP以文本模式传输),它们可能会损坏。为了允许传统文件传输程序确定文件是二进制文件,通常在标头中包含一些字符代码高于127的字节。
例如:
%忏嫌百分号标识一行注释,其他几个字节是超过127的任意字符代码。因此,我们示例中的整个header是:
%PDF-1.0%忏嫌Body文件正文由一系列对象组成,每个对象前会有单独的一行,该行包括一个对象编号,一个世代号以及关键字obj。紧跟在对象之后的是endobj关键字,它同样独占一行。
交叉引用表列出了文件正文中每个对象的字节偏移量。这允许随机访问对象,因此不必按顺序读取它们。这意味着,即使在大型文件上,像计算PDF文档中的页数这样的简单操作也可以很快。
PDF文件中的每个对象都有一个对象编号和一个世代编号。当重用交叉引用表条目时使用世代号——我们在这里不考虑它们(它们将始终为零)。
在我们的文件中,我们可以认为交叉引用表由一个表示条目数的标题行组成,然后是一个特殊条目,然后是文件体中每个对象的一行。
06%表中的六个条目,从0开始000000000065535f%特别条目000000001500000n%对象1的字节偏移量为15000000007400000n%对象2的字节偏移量为74000000018200000n%等等...000000028100000n000000040000000n%对象5的字节偏移量为400请注意,字节偏移量以前导零(不足位数补0)存储,以确保每个条目都相同长度。因此,我们也可以通过随机访问来读取交叉引用表。
Trailer的第一行只是Trailer关键字。之后是Trailer字典,至少包含/Size条目(给出交叉引用表中的条目数)和/Root条目(给出文档根目录对象编号,它是正文中对象图的根元素)。
接下来一行只包含startxref关键字,再一行包含一个数字(文件中交叉引用表开头的字节偏移量),然后是行%%EOF,它表示PDF文件的结尾。
PDF文件是8位字节的序列,这些字符可以分组为标记(例如关键字和数字)和文件解析。
有三种标记字符:常规字符,空白字符和分隔符。
如上hello.pdf源码中,最顶行插入如下包含空白字符的字符串,可正常打开。
文件解析字符含义如下表格所列:
PDF文件由对象图组成,将对象链接在一起的方法:间接引用,它形成从一个对象到另一个对象的链接。
PDF支持五个基本对象:
和三个复合对象:
整数写为一个或多个十进制数字0~9,可选地以加号或减号开头:
0+1-163实数被写为一个或多个十进制数字,可选地前面带有加号或减号,并且可选地有一个小数点,可以是内部,或以下:
0.00..0-0.00465.4通常,规范允许给定对象是整数或实数。其他时候它必须是整数。此外,整数和实数的范围和准确性由PDF实现定义,而不是标准。在某些实现中,如果整数超出可用范围,则将其转换为实数。
字符串由一系列字节组成,写在括号之间:(Hello,World)
反斜杠\字符和括号字符()必须通过在它们前面加上反斜杠进行转义。例如,写作:(Some\\escaped\(characters)表示字符串"Some\escaped(characters"。外部存在已经平衡的括号对在字符串内不需要转义。例如(Red(Rouge))表示字符串“Red(Rouge)”。
反斜杠也可用于引入其他字符代码以实现可读性(参见表3-2)。
它在功能上与以通常方式描述字符串相同。
名称在整个PDF中使用,作为字典的键来定义各种值对象。一个名称引入正斜杠。例如:
/French/字符是名称的一部分——事实上,/它本身就是一个有效的名称。名称可能不包含空格或分隔符,但名称需要与之对应一些具有这些字符的外部名称(例如空格),我们可以使用哈希符号后跟两个十进制数字:
/Websafe#20Dark#20Green这表示名称/WebsafeDarkGreen,因为在ASCII中,十六进制20是空格的代码。名称区分大小写(/French和/french不同)。
PDF允许布尔值为true和false。它们经常在字典条目中用作标志。
数组表示PDF对象的有序集合,包括其他数组。对象不一定都是同一类型。例如,数组:
[00400500]按顺序包含四个数字:0,0,400,500。数组:
[/Green/Blue[/Red/Yellow]]包含三个项目:名称/Green,名称/Blue和两个名称的数组[/Red/Yellow]。
为了将PDF内容拆分为单独的对象(因此只有在需要时才能读取数据),我们将它们与间接引用连接在一起。对对象6的间接引用写为:
60R这里,6是对象编号,0是世代号(这里我们不考虑),R是间接参考关键字。
例如,这是使用间接引用的典型字典:
流用于存储二进制数据。它们由字典和一大块二进制数据组成。字典根据流所放置的特定用途列出数据的长度,以及可选的其他参数。
所有流必须是间接对象。流几乎总是使用各种机制进行压缩,如下表所示。
以下是压缩流的示例:
/Filter[/ASCII85Decode/DCTDecode]
需要外部参数的过滤器(例如,在数据流本身之外定义压缩参数)也会将这些参数存储在流字典中。
此更新过程可能会发生多次。副作用是以这种方式更新的文件能进行撤销一次或多次,从而能检索到文档的早期版本。
更改经过数字签名的文档时,必须以增量方式进行所有更新,否则,数字签名将无效。收件人可以撤消增量更新以检索原始的,经过认证的文档。
当一个文件以递增方式更新时,会添加一个新的trailer,其中包含前一个trailer中的所有条目,以及一个/Prev条目,它给出了之前交叉引用表的字节偏移量。因此,已逐步更新的文件将具有多个trailer词典和文件结束标记。
通过这种方式,PDF应用程序可以以相反的顺序读取交叉引用部分,以构建文件中每个对象的最新版本的列表。
从PDF1.5开始,引入了一种新机制,通过允许将多个对象放入单个对象流中来进一步压缩PDF文件,整个流被压缩。同时,引入了一种用于引用这些流中的对象的新机制——交叉引用流。
使用这些机制压缩的文件很难手动读取,因此我们可以像往常一样使用pdftk中的解压缩操作,将它们重写为解压缩以供检查。
在网络环境中查看大型PDF文件时,尤其是当数据速率较低或网络延迟较高时,用户不希望等待整个文件下载以查看它。
在Web浏览器中查看文档时,这一点尤为重要。我们希望第一页快速显示,并且要更改为另一页(通过单击超链接或书签)尽可能快。
网络传输机制例如HTTP(超文本传输协议,用于在Web浏览器中获取网页)通常允许获取任意数据块。但是,因为延迟,我们希望获取一个包含页面所有数据的块,而不是数百个小块,每个对象一个。
PDF1.2引入了这样一种机制,线性化PDF。这将添加有关如何对文件中的对象进行排序的规则。该系统是向后兼容的,因此线性化的PDF文件可以由不理解线性化PDF的阅读器读取。
线性化的PDF文件可以通过文件顶部直接在标题之后存在线性化字典来识别。例如:
这不是详尽的描述,因为存在许多可能的复杂性(线性化,对象和交叉引用流,加密)。
以下伪代码中给出的递归数据结构可以包含PDF对象。
Dictionary((Name(/Kids),Array(Indirect2)),(Name(/Count),Integer(1)),(Name(/Type),Name(/Pages)))如何编写PDF文件将PDF文档写入文件中的一系列字节要比阅读它简单得多,我们不需要支持所有PDF格式,只需要支持我们打算使用的子集。写作PDF文件非常快,因为它只是将对象图展平为一系列字节。
现在我们考虑下文档结构。trailer字典,文档目录和页面树。我们枚举每个对象中的必需条目。然后我们看看PDF文件中的两个常见结构:文本字符串和日期。
下图显示了典型文档的逻辑结构。
这个字典驻留在文件的trailer而不是文件的主体中,是程序想要读取PDF文档时要处理的第一件事。它包含允许读取交叉引用表的条目,从而可进行后续文件对象的读取。
这是一个示例trailer词典:
文档信息字典包含文件的创建日期和修改日期,以及一些简单的元数据。文档信息字典条目在如下表格描述。
这是一个示例Info词典:
PDF文档中的页面字典汇集了使用指令来操作资源(字体,图像和其他外部数据)从而绘制图形和文本内容的说明。它还包括页面大小,以及定义裁剪等。
如下表格总结了页面字典中的条目。
媒体框和其他框的矩形数据结构是四个数字的数组。这些定义了矩形的对角相对的角:数组的前两个元素是一个角的x和y坐标,后两个元素是另一个角的x和y坐标。
通常,给出左下角和右上角,如下示例:
/MediaBox[00500800]/CropBox[100100400700]定义一个500x800点的页面,裁剪框在页面的每一侧删除100个点。
页面使用页面树而不是简单的数组链接在一起。这种树结构使得在具有数百或数千页的文档中查找给定页面变得更快。
好的PDF应用程序构建了一个平衡树(一个节点数量最小的树)。这可确保快速定位特定页面。没有子节点的节点就是页面本身。
下表总结了中间或根页面树节点中的条目(即,不是页面本身)。
下图显示了七页的示例页面树结构。
PDF对象编写如下,
页面的实际文本内容之外的字符串(例如,书签名称,文档信息等)被称为文本字符串。它们使用PDFDocEn编码或(在最近的文档中)Unicode编码。PDFDocEncoding基于ISOLatin-1编码。它完全记录在ISO标准32000-1:2008的附录D中。
编码为Unicode的文本字符串通过查看前两个字节来区分:这些字符将是254后跟255.这是Unicode字节顺序标记U+FEFF,表示UTF16BE编码。这意味着PDFDocEncoding字符串不能以t(254)后跟(255)开头,但这在任何合理的情况下都不太可能发生。
文档信息字典中的创建和修改日期/CreationDate和/ModDate是PDF日期格式的示例,对字符串中的日期进行编码,包括有关时区的信息。
日期字符串的格式为:(D:YYYYMMDDHHmmSSOHH'mm')
其中括号表示通常的字符串。该日期的其他部分在如下表格中进行了总结。
一年之后的所有日期都是可选的。例如,(D:1999)完全有效。但是,很明显,如果省略一个部分,则必须省略后面的所有内容,否则结果将是模糊的。DD和MM的默认值为01,对于所有其他部分,默认值为零。
例如:(D:20060926213913+02'00')
这是一个手动创建的文本,由pdftk处理成有效的PDF文件,它是一个三页文档,包含文档信息字典和页面树。