浅谈pdf协议码

Posted by mjTree on December 18, 2023

更新于:2023-12-25 12:00

一、PDF 简介

PDF(Portable Document Format) 是一种广泛使用的文档格式,由 Adobe Systems 开发。它以其可跨平台和保真度高而著称,能够在不同设备和操作系统上保持一致的显示效果。

下面展示 Adobe 的 PDF 版本,具体版本的信息可以参考 维基百科

年份 PDF版本 特点
1993 PDF 1.0 Acrobat 1.0
1994 PDF 1.1 文档加密(40字节),线索树,名字树,链接,设备独立色彩资源
1996 PDF 1.2 表单,半色调屏幕,高级色彩特性,对CJK语言支持
2000 PDF 1.3 数字签名,逻辑结构,JavaScript,嵌入式文件,Masked Images
2001 PDF 1.4 文件加密(128字节),标签式PDF,访问控制,透明,元数据流
2003 PDF 1.5 文档加密(公钥),JPEG 2000压缩,可选内容组,注解类型
2005 PDF 1.6 文档加密(AES),3D支持,最大文件支持,注解类型
2006 PDF 1.7 PDF包,表单域自动识别,共享审阅,支持AutoCAD
2008 PDF 1.7 Extension Level 3 256位AES加密
2009 PDF 1.7 Extension Level 5 XFA 3.0
2011 PDF 1.7 Extension Level 8 XFA 3.3

PDF 1.7版本以后,Adobe 公司宣布不会再发布新的 PDF 格式版本,未来的版本将由 ISO 技术协会提供。关于PDF-2.0不在本篇文章介绍范围内,本篇文章通过PDF 1.0版本的一些简单特性介绍。

二、编写pdf文件

首先我们可以编写一个文本文件,把下面的内容复制粘贴到文本文件中,然后保存文件,最后把文件的格式后缀修改成 pdf 格式即可。可以通过新版本(2023-12-18)wps软件chrome浏览器打开文件即可看到 pdf 的内容。

%PDF-1.0    % PDF 版本号为 1.0 的文件头,header-StartIndex
1 0 obj % 对象1      % body-StartIndex
<< /Type /Pages     % 这是一个页面列表
   /Count 1         % 只有一页
   /Kids [2 0 R]    % 页面对象编号列表,这里只是对象2
>>
endobj  % 对象1结束
2 0 obj
<< /Type /Page              % 这是一个页面
   /MediaBox [0 0 612 792]  % 设置纸张尺寸
   /Resources 3 0 R         % 对象3的资源引用
   /Parent 1 0 R            % 引用备份到父页面列表
   /Contents [4 0 R]        % 图形内容在对象4中
>>
endobj
3 0 obj
<< /Font  % 字体字典
     << /F0  % 只有一种字体,称为/F0
          << /Type /Font  % 引用了内置字体Times-Italic
            /BaseFont /Times-Italic
            /Subtype /Type1 >>
     >>
>>
endobj
4 0 obj     % 页面内容流
<< >>
stream      % 流的开始
1. 0. 0. 1. 50. 700. cm % 位置在(50,700)
BT  % 开始文本块
 /F0 36. Tf         % 在36pt选择/F0字体
 (Hello, PDF!) Tj % 放置文本字符串
ET  % 结束文本块
endstream   % 流结束
endobj
5 0 obj
<< /Type /Catalog   % 文件目录
   /Pages 1 0 R     % 参考页面列表
>>
endobj
xref    % 我们跳过了交叉引用表的开始(交叉引用表—StartIndex)
0 6
trailer
<< /Size 6      % 交叉引用表中的行数(对象数加1)
   /Root 5 0 R  % 参考文档目录
>>
startxref
0       % xref表开始的字节偏移量,我们将其设置为0
%%EOF   % 文件结束标记

display_pdf

上图是在chrome浏览器中打开展示的截图,首先需要说明一下,上面的内容是不符合 PDF 协议规范,但是由于一些展示软件做了很多兼容操作使得我们可以直接打开。我们可以通过第三方工具把上面的内容进行一次转换,变成一个符合 PDF 协议的文件。下面提供了 pdftk 工具的安装方法,可以按照自己的电脑型号按照使用。

Linux(ubuntu): apt-get install pdftk
Macos(intel): https://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/pdftk_server-2.02-mac_osx-10.11-setup.pkg
Windows(win 7/8): https://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/pdftk_free-2.02-win-setup.exe
其他版本:需自行搜索查找,推荐在Linux下安装使用。
pdftk simple_demo.pdf output hello.pdf

通过 pdftk 工具,通过命令完成文件的转换,转换文件。之后借用 SublimeText 这类的软件工具打开转换后的 pdf 文件内容,会比原先我们编写的多出一些内容信息。

三、文件结构

一个简单有效的 PDF 文件按顺序包含四个部分,上面手写的 PDF 内容包含了 header、body、交叉引用表。trailer 可以查看 pdftk 转换的文件中会存在该信息。  

1、header:提供PDF版本号;
2、body:包含页面,图形内容和大部分辅助信息的主体,全部编码为一系列对象;
3、cross-reference-table:列出文件中每个对象的位置便于随机访问;
4、trailer:包括trailer字典,它有助于找到文件的每个部分, 并列出可以在不处理整个文件的情况下读取的各种元数据。

ReadPdfLinkSequence

1、header

%PDF-1.0
%âãÏÓ

PDF 文件的第一行会给出文档的版本号,由于绝大多数 PDF 文件包含二进制数据,因此如果更改行结尾(例如,如果文件通过FTP以文本模式传输)可能会损坏。为了允许传统文件传输程序确定文件是二进制文件,通常在标头中包含一些字符代码高于 127 的字节。

2、body

文件的正文是由一系列对象组成,如上图所示,每个对象在一行上都有一个对象编号产生号(generation number)、obj关键字,中间是字典,在另一行之后是endobj关键字。例如下面的例子,对象编号是 1,产生号是 0,对象 1 的内容在1 0 objendobj两行中间。

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

3、cross-reference-table

交叉引用表(cross-reference-table)列出了文件正文中每个对象的字节偏移量,因此我们可以随机访问相关对象(类似数据结构中的数组,可以随机读取信息,不需要像链表结构读取信息)。这就意味着即使在大文件中,我们获取 PDF 文档中的页数或者获取某一页对象信息的操作可以很快完成。PDF 文件中的每个对象都有一个对象编号和一个产生号,当一个交叉引用表项被重用时,会使用到产生号。由于产生号基本为 0,我们可以认为交叉引用表是由表示条目数量的首行、一个特殊的条目以及文件正文中每个对象的组成信息构成。

打开上面通过 pdftk 工具转换成的文件,可以看到里面的交叉引用表内容。

0 6 % 表中的六个条目,从0开始
0000000000 65535 f % Special entry
0000000015 00000 n % 对象1的字节偏移量为15
0000000074 00000 n % 对象2的字节偏移量为74
0000000182 00000 n % 对象3的字节偏移量为182
0000000281 00000 n % 对象4的字节偏移量为281
0000000529 00000 n % 对象5的字节偏移量为529

4、trailer

关于Trailer的组成,第一行只是 Trailer 关键字。之后是Trailer的字典,至少会包含/Size条目(给出条目数在交叉引用表中)和/Root条目(给出文档目录的对象编号,它是正文中对象图的根元素)。之后的一行内容只包含 startxref 关键字,还有一行包含了一个数字(文件中交叉引用表开头的字节偏移量),最后一行是 %%EOF 内容,它表示PDF文件的结尾。

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

我们可以从文件末尾向后读取Trailer信息,找到文件结束标记,提取交叉引用表的字节偏移量,然后解析Trailer字典信息,trailer 关键字标记 Trailer 的上限。

四、逻辑结构

PdfLogicalStructure

1、Trailer Dictionary

Trailer 字典是存储在 pdf 文件的trailer 结构中,是读取 pdf 文件信息时要做的第一件事,它包含能读取交叉引用表的信息,从而可以读取文件的对象信息,核心构成结构如下图所示。

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

下面展示一个 pdf 文件的 trailer 内容,当读取了对应信息之后就可以读取Document CatalogDocument Information Dictionary

trailer
<<
/Info 844 0 R
/ID [<87e1fd77ca098034464a33653a2ef902> <6345be6e1d9a1295528c04e827e163a4>]
/Root 827 0 R
/Size 845
>>

2、Document Information Dictionary

文档信息字典包含了文件的创建日期和修改日期,以及一些简单的元数据,下图提供一个文档信息字典的结构描述表。

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

下面展示文档信息字典的实际样例。

<<
/ModDate (D:20231212105943+08'00')
/CreationDate (D:20231212025939+00'00')
/Title ()
/Creator (Chromium)
/Producer (Skia/PDF m99; modified using iText® 7.5.0 ©2016-2019 )
/Author (*****)
>>

3、Document Catalog

文档目录是主对象图的根对象,所有其他对象都可以从主对象图的通过间接引用中获得。因此,可以通过文档目录获取Page Tree以及Page的信息。

值类型
/Type* name 必须是/Catalog
/Pages* 间接引用字典 页面树的根节点
/PageLabels number tree 一个数字树,给出了该文档的页面标签
/Names dictionary 名字词典,它包含各种名称树,它们将名称映射到实体,以防止必须使用对象编号直接引用它们
/Dests dictionary 将名称映射到目标的字典,目的地是超链接向用户发送的PDF文档中的位置的描述
/ViewerPreferences dictionary 一个查看器首选项字典,允许标志指定在屏幕上查看文档时的PDF查看器的行为,例如打开文档的页面,初始查看比例等
/PageLayout name 指定PDF查看器要使用的页面布局,默认值:/SinglePage
/PageMode name 指定PDF查看器要使用的页面模式,默认值:/UseNone
/Outlines 间接引用字典 大纲字典是文档大纲的根,通常称为书签
/Metadata 间接引用流 文档的XMP元数据

4、Page Tree

PDF 文件中的page dictionary是包括了用于绘制图形和文本内容的指令以及指令所使用的资源(字体、图像和其他外部数据),还包括页面大小,以及定义裁剪等的许多其他框。

值类型
/Type* name 必须是/Pages
/Parent* 间接引用字典 页面树中此节点的父节点
/Resources dictionary 页面的资源(字体,图像等),省略此项则资源将从页面树中的父节点继承
/Contents 对此类引用的流或数组的间接引用 一个或多个部分中页面的图形内容,如果缺少此项,则页面为空
/Rotate Integer 页面的旋转角度,从北向顺时针,值必须是90的倍数。默认值为0;如果缺少此项,其值将从页面树中的父节点继承
/MediaBox* rectangle 页面的媒体框(其媒体的大小,即纸张),在大多数情况下用于页面大的小;如果缺少此项,其值将从页面树中的父节点继承
/CropBox rectangle 页面的裁剪框,定义了在显示或打印页面时默认可见的页面区域;如果不存在,则将其值定义为与媒体框相同

MediaBox 和 CropBox 的矩形数据结构是一个由四个数字组成的数组,表示了两个点的坐标(x, y),通常是左下角和右上角,下面通过举例描述一些。

% 定义一个500 * 800点的页面,裁剪框在页面的每一侧缩减了100个点
/MediaBox [0 0 500 800]
/CropBox [100 100 400 700]

页面树是一种树结构,通过这种结构可以使得在在数千页的文档中快速查找到指定页面。下面展示一个树结构以及对应的 pdf 协议内容,便于大家参考熟悉。

PageTreeDemo

% Kids表示当前节点的子节点信息;Count表示子节点数目;Parent表示了父节点

1 0 obj Root node
<< /Type /Pages /Kids [2 0 R 3 0 R 4 0 R] /Count 7 >>
endobj
2 0 obj Intermediate node
<< /Type /Pages /Kids [5 0 R 6 0 R 7 0 R] /Parent 1 0 R /Count 3 >> endobj
3 0 obj Intermediate node
<< /Type /Pages /Kids [8 0 R 9 0 R 10 0 R] /Parent 1 0 R /Count 3 >> endobj
4 0 obj Page 7
<< /Type /Page /Parent 1 0 R /MediaBox [0 0 500 500] /Resources << >> >> endobj
5 0 obj Page 1
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 500 500] /Resources << >> >> endobj
6 0 obj Page 2
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 500 500] /Resources << >> >> endobj
7 0 obj Page 3
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 500 500] /Resources << >> >> endobj
8 0 obj Page 4
<< /Type /Page /Parent 3 0 R /MediaBox [0 0 500 500] /Resources << >> >> endobj
9 0 obj Page 5
<< /Type /Page /Parent 3 0 R /MediaBox [0 0 500 500] /Resources << >> >> endobj
10 0 obj Page 6
<< /Type /Page /Parent 3 0 R /MediaBox [0 0 500 500] /Resources << >> >> endobj

五、Demo讲解2种结构

Demo文件,下载本地后通过 SublimeText 类似的软件工具打开文件查看内部文件结构内容(内部有注释),下面通过一张图讲解一下文件的逻辑结构。

DemoLogicalStructure

六、小结

本篇文章首先介绍了 PDF 文件以及对应版本信息,之后通过手写一个简单的 pdf 文件简单展示内部。之后详细的描述了 PDF 文件内部结构,以及 PDF 的逻辑结构。通过本篇文章可以快速的了解 PDF 原理,虽然后期开发可能通过一些完善的工具去解析 PDF,但是熟悉其结构也方便我们后续遇到工具无法处理的问题时快速上手解决。