C# Parsing 类实现的 PDF 文件分析器

1. 介绍

这个项目让你可以去读取并解析一个PDF文件,并将其内部结构展示出来. PDF文件的格式标准文档可以从Adobe那儿获取到. 这个项目基于“PDF指南,第六版,Adobe便携文档格式1.7 2006年11月”. 它是一个恐怕有1310页的大部头. 本文提供了对这份文档的简洁概述. 与此相关的项目定义了用来读取和解析PDF文件的C#类. 为了测试这些类,附带的测试程序PdfFileAnalyzer让你可以去读取一个PDF文件,分析它并展示和保存结果. 程序将PDF文件分割成单独每页的描述,字体,图片和其它对象. 有两种类型的PDF文件不受此程序的支持: 加密文件和多代文件.

这个程序的1.1版本允许世界各地使用点符号作为小数分隔符的程序员来编译和运行程序.

1.2版本则修复了一个有关使用跨多个引用流来读取PDF文档的问题. 1.2之前的版本对此场景只会以一个对象数字重复的错误而终止运行.

如果你对将PDF文件写入器引入你的应用程序,那就请读一读 "PDF 文件写入程序 C# 类库" 这篇文章吧.

2. 概要

PDF格式的文件,借助Adobe Acrobat软件,可以在各种屏幕上显示查看,使用各种打印机打印。但是,如果使用二进制文件编辑器打开PDF文件,你会发现文件大部分是不可读的,有小部分是可读的,如下:

1 0 obj
<</Lang(en-CA)/MarkInfo<</Marked true>>/Pages 2 0 R
/StructTreeRoot 10 0 R/Type/Catalog>>
endobj
2 0 obj
<</Count 1/Kids[4 0 R]/Type/Pages>>
endobj 
4 0 obj
<</Contents 5 0 R/Group <</CS/DeviceRGB /S/Transparency /Type/Group>>
/MediaBox[0 0 612 792] /Parent 2 0 R
/Resources <</Font <</F1 6 0 R /F2 8 0 R>>
/ProcSet[/PDF/Text/ImageB/ImageC/ImageI]>>
/StructParents 0/Tabs/S/Type/Page>>
endobj
5 0 obj
<</Filter/FlateDecode/Length 2319>>
stream
. . .
endstream
endobj

看上去,该文件是由嵌套在“n 0 OBJ ”和“ endobj ”关键词之间的对象组成的,术语PDF也就是间接对象的意思。 “obj”前面的数字是对象编号和第几代对象标识, 双尖括号中的内容表示数据字典对象,中括号中的内容表示数组对象, 以斜杠/ 开始的内容表示参数名称 (例如: /Pages)。上例中的第一项 “1 0 obj” 表示文档的目录或者文档的根对象。文档目录的字典对象 “/Pages 2 0 R”,指向定义页码树对象的引用。按照这样推算,编号为2的对象包含指向 “/Kids[4 0 R]”的页面的引用,是一个页面文档。 编号为4的对象是唯一的一个页面定义, 页面大小为612*792点, 换句话说,也就是8.5” * 11” (1” 代表72 点)点。该页面使用了两种字体F1和F2,这两种字体分别在编号为6和8的对象中定义。该页面的内容在编号为5的对象中描述,该对象中包含页面绘图的流信息,示例中的 “. . .”代表这部分流信息。如果使用二进制文件编辑器打开PDF文件,会发现这部分流信息看起来是一长串不可读的随机数,原因是那是压缩数据。流数据采用Zlib方法压缩,压缩方式由字典对象“/Filter /FlateDecode”描述,被压缩流的大小为2319字节。解压这部分流信息,前面几行内容如下所示:

q
37.08 56.424 537.84 679.18 re
W* n
/P <</MCID 0>> BDC 0.753 g
36.6 465.43 537.96 24.84 re
f*
EMC  /P <</MCID 1/Lang (x-none)>> BDC BT
/F1 18 Tf
1 0 0 1 39.6 718.8 Tm
0 g
0 G
[(GRA)29(NOTECH LI)-3(MIT)-4(ED)] TJ
ET

这是页面描述语言的一个小例子。 示例中, “re” 代表矩形,“re” 前面的4个数字代表矩形的位置和大小,依次为:起点横坐标、起点纵坐标、宽度、高度。

这个简单的例子演示了PDF文件内部实现的总体思路。从页面层次结构的根对象开始, 每一页都定义了诸如字体、图片、内容流的资源,内容流由操作符和绘制页面所需要的参数构成。PDF文件分析器会产生一个对象汇总文件,该文件包含非流对象的其他所有对象。每个数据流会被解码并保存为一个单独的文件, 页面描述流保存为文本格式的文件, 图片流保存为.jpg或.bmp格式的文件,字体流保存为.ttf格式的文件,其他二进制流保存为.bin 格式的文件,文本流保存为.txt格式的文件。通过另一个解析过程,晦涩难懂的页面描述会被转换为伪C#代码,如上例中的页面描述被转为:

SaveGraphicsState(); // q
Rectangle(37.08, 56.424, 537.84, 679.18); // re
ClippingPathEvenOddRule(); // W*
NoPaint(); // n
BeginMarkedContentPropList("/P", "<</MCID 0>>"); // BDC
GrayLevelForNonStroking(0.753); // g
Rectangle(36.6, 465.43, 537.96, 24.84); // re
FillEvenOddRule(); // f*
EndMarkedContent(); // EMC
BeginMarkedContentPropList("/P", "<</Lang(x-none)/MCID 1>>"); // BDC
BeginText(); // BT
SelectFontAndSize("/F1", 18); // Tf
TextMatrix(1, 0, 0, 1, 39.6, 718.8); // Tm
GrayLevelForNonStroking(0); // g
GrayLevelForStroking(0); // G
ShowTextWithGlyphPos("[(GRA)29(NOTECH LI)-3(MIT)-4(ED)]"); // TJ
EndTextObject(); // ET

文章接下来的部分将对PDF文件的结构和解析过程进行更为详细的描述,接下来的章节包括:对象定义,文件结构,文件解析,文件读取,以及使用PDF文件分析器编程。

3. 免责声明

pdf 文件分析器能处理大量的文件,这是我在自己的系统上扫描众多PDF文件的经验。不过,该程序不支持加密文件或者多个代文件(在对象不为零之前的第二个数字)。在PDF规格文件之中可用功能的数量是非常显著的。这并不可能为一个单的个开发者系统地测试所有的功能。如果在整个文件分析期间该程序抛出一个异常,将显示一条错误信息,该信息显示源代码模块名和行号。

4.对象定义

PDF文件生成多个对象。在PDF文件分析器项目中每个PDF对象都有一个对应的类。所有这些对象类都派生于PDFbase类。对象类定义源代码是BasicObjects.cs.确却地PDF对象定义在Adobe pdf文件 规格第三章之中是有用的



4.1. 基础的对象

4.2. 复合的对象

4.3. 间接对象

4.4. 操作符和关键词

5. 文件结构

PDF文件由四个部分构成: 头部Header , 主体body, 多引用cross-reference 和附带签名 trailer signature.

6. 文件转换

PDF 文件是一个字节的序列. 一些字节有特殊的意义.

空格被定义成: null, tab, 换行, 换页, 回车和间隔.

分隔符被定义成: (, ), <, >, [, ], {, }, /, %, 以及空格字符.

文件转换是由PdfParser 类来完成的. 开始进行转换过程是,程序会设置文件需要被转换区域的位置. ParseNextItem() 是提取下一个对象的方法.

解析器跳过空格符和注释。如果下一个字节是“(”,判断对象为一个字符串。如果下一个字节是“[”,判断对象是一个数组。如果接下来的两个字节是“<<”,判断对象是一个字典。如果下一个字节是“<”,判断对象是一个十六进制字符串。如果下一个字节是“/”,判断对象是一个名称。如果下一个字节不是上述任何一种,解析器会采集随后的字节直到发现定界符。定界符不是当前标记符的一部分。标记符可以是整数,实数,操作符或关键词。在整数的情况下,程序将进一步搜索对象引用“n 0 R”或间接对象“n 0 obj”中 n 为该整数的对象。从 ParseNextItem() 返回的值是第4节“对象的定义”中所述的适当对象。对象的类作为 PdfBase 类返回。

在数组或字典的情况下,程序将执行递归调用 ParseNextItem() 来解析数组或字典的内部对象。

7. 文件读取

PdfDocument 类是 PDF 文件分析的主要类。入口方法是 ReadPdfFile(String FileName)。程序以二进制读取的方式打开 PDF 文件(一次一个字节)。

文件分析开始于检查头部签名 %PDF-1.x(x为0到7)和结尾签名%%EOF。有人会认为,所有的 PDF 生成器会把头部签名放在文件的零位置,结尾签名放在文件的最后。不幸的是,实际并非如此。程序必须在文件的两端搜索这两个签名。如果头部签名不在零位置,所有间接对象的文件位置的指针也必须调整。

就在结尾签名的前面有一个指向最后一个交叉引用表开始位置的指针。

解析器为多引用表设置文件位置. 如果下一个对象是“xref” 关键词,我们就有了原来类型的多引用. 否则,它就是新的基于流的多引用. 文件可以有多个多引用表. 文件也可以同时拥有新的和旧的风格的表. 每一个表都有一个对象数目和指向间接引用开头的指针的列表. 对于每一个活动对象程序都会创建一个PdfIndirectObject 对象并将其保存在 ObjectArray中. 除了对象的数字和位置,这个对象的其它东西都是空的. 对于原来的多引用表,其位置是相对于文件而言的. 对于流类型的多引用,位置是相对于一个父间接对象流而言的.

在处理过程中,如果间接对象生成了0之外的数字, 程序的执行就会被终止. PdfFileAnalyzer 不支持多代的形式.

附件字典在交叉引用表的末尾处。分析PDF文件的时候,我们创建了一个带负对象号的虚拟间接对象用于保存附件字典。

程序在附件字典中寻找四个特定的入口。如果找到/Encrypt入口,表示PDF文件是被加密的,程序的将结束分析,因为程序不支持分析加密格式的PDF文件。接着程序寻找/Root目录对象的对象号。如果找到/XRefStm入口,我们就有了两种交叉引用的类型。最后如果存在/Prev入口,我们有了另一个用于处理的交叉引用表。

交叉引用的处理完成后,我们拥有所有的间接对象的数组。  在处理阶段,可用信息是对象号和对象位置。下一步,程序遍历数组,读取并解析每一个间接对象,并设置对象的值。如果对象是流,仅字典部分被解析,因为在这个时候还不知道流的长度。除了上述对象,如果字典和流对象的对象类型和子类型成员是可用的,系统将为字典和流对象设置这两个值。

接下来程序遍历所有的对象,并处理流对象。流对象的对象类型是"/ObjStm"。程序读取和对象相关联的流,并分解流到多个间接对象上。

接下来程序搜索所有的字典对象和流对象引用的对象字典对象。程序查找键值对,例如“/name n 0 R”。加入键值对被找到,程序检查对象类型。如果再对象解析阶段没有设置对象类型,对象类型将设置为/name值。

下一步,读取所有前面没有读取的流。系统读取从文件读取流。流被解码并保存到对应的文件中。PdfFileAnalyzer支持如下的过滤:/FlateDecode,/LZWDecode, /ASCII85Decode和/DCTDecode。文本文件的扩展名是.txt,二进制文件的扩展名是.bin,图片文件的扩展名是.jpg和.bmp,字体文件的扩展名是.ttf,交叉引用文件的扩展名是.xref。/FlateDecode是ZLib Deflate压缩算法。解压缩源代码取自发布在CodeProject.com网站的文章《用C#压缩和解压类处理彼标准的ZIP文件》,点击这儿查看

下一步是构建页的内容。程序跟随从根开始的页面树。页对象不是流对象。换句话说,页描述命令是不能直接在也对象中的。页对象字典有/Contents的键值对。如果不存在这个键值对,那么页面就是空的。内容入口值可以是一个单独的引用或者是一个应用数组。程序将为来自于一个或多个内容流的页面创建虚拟的内容流。页内容虚拟流保存在PageObj_xx.txt和PageSource_xx.txt中。PageObj_xx.txt是页面的实际描述内容。PageSource_xx.txt是将页面的描述内容转换为伪C#源代码。在第二节概要中,有这两个文件的例子。

页内容流是由参数和操作符组成的。例如矩形由四个实数描述的,内嵌的图片不遵循这个规则。它的描述是在第三节对象定义中。

最后,程序产生对象汇总文件ObjectSummary.txt。文件显示所有简介对象的信息不包含流。

8. PdfFileAnalyzer 程序

开发应用程序 PdfFileAnalyzer 的目的是用来测试这个 PDF 文件解析类。如果你想在开发环境之外测试它的可执行程序,需创建一个名为 PdfFileAnalyzer 的目录并复制 PdfFileAnalyzer.exe 到这个目录中,然后运行这个程序。如果你想从 Visual C# 开发环境中运行这个项目,请确保你在“项目属性”的“Debug”标签栏中定义了一个工作目录。此程序是使用 Microsoft Visual C# 2012 开发的。

运行程序,可用的操作项有: Open, Setup 和 Exit.

程序首次执行时你必须使用 Setup 定义工程目录。这个目录盛放所有被分析的 PDF 文件所产生的对应子目录。

Open 按钮会显示一个标准的文件选择对话框,你可以在其中找到你要进行分析的 PDF 文件。

PDF文件分析器界面将切换到类的汇总界面:

每行代表一个间接的PDF对象。每列是:

点击Summary按钮,查看ObjectSummary.txt 文件。

选择一行并点击View按钮或者双击一行后将显示对象分析界面,用于查看间接对象的详情。


对于所有的非流对象,前面的三个按钮是不能点击的。仅仅显示对象自身的信息。你能用文本方式或者十六进制格式查看这些信息。

对于流对象,第一个按钮的名字是object type。前两个按钮object type和Stream允许你在查看对象和流之间切换。Hex和Text按钮允许你采用二进制格式或者文本格式查看。如果是图片流,文本格式显示为四列:(1) 对象号,(2) 类型 (0-未使用,1-普通对象,2-流对象),(3)普通对象的位置和流对象的父对象,(4) 父对象的索引号。如果是二进制流(例如:字体),则仅能用十六进制格式查看。

页面对象按照流对象来处理。所有内容对象的文本显示是关联的。另外,Source按钮允许你查看页面在C#代码中的描述语言。

JPG图片和BMP图片可以旋转方向和调整大小。

9. 参照

Adobe PDF 文件规格文档:“PDF参考,第六版,Adobe 便携文档格式版本 1.7 2006年11月”。点击 http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/pdf_reference_1-7.pdf 可以查看 Adobe 网站上的该内容。

“用 C# 的 压缩/解压 类处理标准的 Zip 文件”(由 Uzi Granot 发表在 CodeProject.com 网站上。地址: http://www.codeproject.com/Articles/359758/Processing-Standard-Zip-Files-with-Csharp-compress

10. 本作者的其它开源软件

11. 历史