1. 介绍
这个项目让你可以去读取并解析一个PDF文件,并将其内部结构展示出来. PDF文件的格式标准文档可以从Adobe那儿获取到. 这个项目基于“PDF指南,第六版,Adobe便携文档格式1.7 2006年11月”. 它是一个恐怕有1310页的大部头. 本文提供了对这份文档的简洁概述. 与此相关的项目定义了用来读取和解析PDF文件的C#类. 为了测试这些类,附带的测试程序PdfFileAnalyzer让你可以去读取一个PDF文件,分析它并展示和保存结果. 程序将PDF文件分割成单独每页的描述,字体,图片和其它对象. 有两种类型的PDF文件不受此程序的支持: 加密文件和多代文件.
这个程序的1.1版本允许世界各地使用点符号作为小数分隔符的程序员来编译和运行程序.
1.2版本则修复了一个有关使用跨多个引用流来读取PDF文档的问题. 1.2之前的版本对此场景只会以一个对象数字重复的错误而终止运行.
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</div>
看上去,该文件是由嵌套在“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</div>
这是页面描述语言的一个小例子。 示例中, “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</div>
文章接下来的部分将对PDF文件的结构和解析过程进行更为详细的描述,接下来的章节包括:对象定义,文件结构,文件解析,文件读取,以及使用PDF文件分析器编程。
3. 免责声明
pdf 文件分析器能处理大量的文件,这是我在自己的系统上扫描众多PDF文件的经验。不过,该程序不支持加密文件或者多个代文件(在对象不为零之前的第二个数字)。在PDF规格文件之中可用功能的数量是非常显著的。这并不可能为一个单的个开发者系统地测试所有的功能。如果在整个文件分析期间该程序抛出一个异常,将显示一条错误信息,该信息显示源代码模块名和行号。
4.对象定义
PDF文件生成多个对象。在PDF文件分析器项目中每个PDF对象都有一个对应的类。所有这些对象类都派生于PDFbase类。对象类定义源代码是BasicObjects.cs.确却地PDF对象定义在Adobe pdf文件 规格第三章之中是有用的
4.1. 基础的对象
Boolean对象是靠PdfBoolean类来实现的. Boolean在PDF上的定义同C#上的是相同的.
Integer 对象是靠PdfInt类来实现的. PDF上的定义同C#上Int32的定义是相同的.
实数对象是靠PdfReal类来实现的. PDF上的定义同C#上的Single定义相同.
String 对象是靠PdfStr类来实现的. PDF上的定义同C#相比有所不同. String 是用字节构造出来的,而不是字符. 它被包在圆括号()里面. PdfFileAnalyzer会把包含在圆括号中的C#字符串保存成PDF的字符串. PDF的字符串对于ASCII编码非常有用.
十六进制字符串独享是靠PdfHex类来实现的. 它是由每字节两个十六进制数定义,并包在尖括号里面的字符串. PdfFileAnalyzer 将包含在尖括号中的C#字符串保存成PDF十六进制字符串. 对于 PDF 读取器,字符串和十六进制字符串对象可用于同种目的. 字符串 (AB) 等同于<4142>. PDF 十六进制字符串对于任意编码的场景非常有用.
Name 对象是靠PdfName类来实现的. Name 对象是由打头的正斜杠后面跟着一些字符组成的. 例如 /Width. Named 对象用作参数名称. PdfFileAnalyzer 将正斜杠开头的C#字符串保存成Name对象.
Null 对象是靠PdfNull类来实现的. PDF 对于null的定义基本上同C#中的是一样的.
4.2. 复合的对象
Array 对象是靠 PdfArray 类来实现的. PDF 数组是一个封装在一堆中括号中的对象的集合. 一个数组的对象可以是除了流之外的任何对象.PdfFileAnalyzer 将一个C#数组中的对象保存成PdfBase类
. 因为所有的对象都继承自PdfBase,所有在这个数组中保存多种类型的对象没有啥问题. 当数组对象被转换成一个字符串时(使用ToString()方法), 程序会在首位添加中括号. 数组可以是空的. 下面是一个有六个对象的数组示例: [120 9.56 true null (string) <414243>].
Dictionary 对象是靠PdfDict类实现的. PDF 字典是一组被包入一对双尖括号中的键值对集合. Dictionary 的键是一个对象的名称,而值则可以是除了流之外的任何对象. PdfFileAnalyzer 将一个键值对保存到PdfPair类中. 键是一个C#字符串,而值则是一个PdfBase.PdfDict 类有一个PdfPair类的数组. Dictionary 可以用键来访问. 因而键值对的顺序没有啥意义. PdfFileAnalyzer 用键来对键值对进行排序. 下面是一个有三个键值对的字典: <</CropBox [0 0 612 792] /Rotate 0 /Type /Page>>.
Stream 对象是靠PdfStream来实现的. Streams 被用来处理面熟语言,图形和字体. PDF Stream 由一个字典和一个字节流组成. 字典中定义了流的参数. 比如流对象中字典的一个键值对 /Filter. PDF 文档定义了10种类型的过滤器. PdfFileAnalyzer 支持了4种. 这是我发现在实际场景中只会被用到那4种. 压缩过滤器 FlateDecode 是现在的PDF写入器最长被用到的过滤器. FlateDecode支持ZLib解压缩. LZWDecode