PDF 文件分析器(含 PDF 解析类)。(2022 版 VS 2022 .NET 6.0)






4.91/5 (75投票s)
PDF 文件分析器旨在读取、解析和显示 PDF 文件的内部结构。2.1 版本支持加密文件。
1. 引言
本项目允许您读取和解析 PDF 文件并显示其内部结构。PDF 文件规范文档可从 Adobe 获取。本项目基于 《PDF 参考第六版,Adobe 便携式文档格式版本 1.7 2006 年 11 月》。该文档长达 1310 页,令人望而生畏。本文档提供了规范的简洁概述。相关的项目定义了用于读取和解析 PDF 文件的 C# 类。为测试这些类,附带的测试程序 PdfFileAnalyzer
允许您读取 PDF 文件,对其进行分析,并显示和保存结果。该程序将 PDF 文件分解为单个页面描述、字体、图像和其他对象。
3.0 版本已升级到 VS 2022 和 .NET 6.0。软件分为 PDF 阅读器库和测试/演示程序。
2. 概述
PDF 文件结构的设计是为了让 Adobe Acrobat 能够在各种屏幕和打印机上显示和打印每一页。如果您用二进制编辑器打开该文件,您会发现大部分文件是不可读的。可读的小部分内容如下所示:
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 x 792 点。换句话说,8.5 英寸 x 11 英寸(1 英寸 = 72 点)。该页面使用两种字体 F1 和 F2。它们在对象 6 和 8 中定义。页面内容在对象编号 5 中描述。对象编号 5 有一个描述页面绘制的流。在示例中,我们用“...”作为此描述的占位符。如果您尝试使用二进制编辑器查看 PDF 文件,流看起来将是一长串不可读的随机数字。原因是您看到的是压缩数据。流使用 ZLib deflate 方法进行压缩。这在字典中由“/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”代表矩形。它前面的四个数字是位置和大小:“X Y 宽度 高度”。
这个简化的例子演示了 PDF 文件背后的基本思想。您从一个指向页面层次结构的根对象开始。每个页面定义资源,如字体、图像和内容流。内容流由绘制页面所需的运算符和参数组成。PdfFileAnalyzer
将生成一个对象摘要文件。该文件包含所有对象(不含流)。每个流将被解码并保存为单独的文件。页面描述保存为文本文件。图像流保存为 .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 文件结构和解析过程。以下各节将涵盖:对象定义、文件结构、文件解析、文件读取以及使用 PdfFileAnalyzer
程序。
3. 对象定义
PDF 文件由对象组成。每个 PDF 对象在 PdfFileAnalyzer
项目中都有一个对应的类。所有这些对象类都派生自 PdfBase
类。对象类定义的源代码是 BasicObjects.cs。确切的 PDF 对象定义可在 Adobe PDF 规范的第 3 章中找到。
3.1. 基本对象
- 布尔对象由
PdfBoolean
类实现。PDF 中布尔值的定义与 C# 中的相同。 - 整数对象由
PdfInt
类实现。PDF 中的定义与 C# 中的 Int32 相同。 - 实数对象由
PdfReal
类实现。PDF 中的定义与 C# 中的 Single 相同。 - 字符串对象由
PdfStr
类实现。PDF 中的定义与 C# 不同。字符串由字节组成,而非字符。它被括在括号 () 中。PdfFileAnalyzer
将 PDF 字符串保存为 C# 字符串,包括括号。PDF 字符串适用于 ASCII 编码。 - 十六进制字符串对象由
PdfHex
类实现。它是一个由每字节两个十六进制数字组成的字符字符串,并用尖括号 <> 括起来。PdfFileAnalyzer
将 PDF 十六进制字符串保存为 C# 字符串,包括尖括号。对于 PDF 阅读器而言,字符串和十六进制字符串对象具有相同的功能。字符串 (AB) 等同于 <4142>。PDF 十六进制字符串适用于任何编码。 - 名称对象由
PdfName
类实现。名称对象由正斜杠后跟一系列字符组成。例如 /Width。命名对象用作参数名称。PdfFileAnalyzer
将名称对象保存为 C# 字符串,包括前导 /。 - Null 对象由
PdfNull
类实现。PDF 中 null 的定义与 C# 中的基本相同。
3.2. 复合对象
- 数组对象由
PdfArray
类实现。PDF 数组是包含在方括号 [] 中的对象集合。一个数组中的对象可以是任何类型的混合,但不能是流。PdfFileAnalyzer
将对象保存为PdfBase
类的 C# 数组。由于所有对象都派生自PdfBase
,因此将不同类型的对象保存在此数组中没有问题。当数组对象转换为字符串(ToString() 方法)时,程序会添加前导和尾随方括号。数组可以为空。包含六个对象的数组示例:[120 9.56 true null (string) <414243>]。 - 字典对象由
PdfDict
类实现。PDF 字典是包含在双尖括号 <<>> 中的键值对集合。字典键是名称对象,值是任何对象(不能是流)。PdfFileAnalyzer
将一对键值对保存在 PdfPair 类中。键是 C# 字符串,值是PdfBase
。PdfDict
类有一个 PdfPair 类的数组。字典通过键访问。因此,对的顺序并不重要。PdfFileAnalyzer
按键值对字典进行排序。包含三个对的字典示例:<</CropBox [0 0 612 792] /Rotate 0 /Type /Page>>。 - 流对象由
PdfStream
实现。流用于存储页面描述语言、图像和字体。PDF 流由两部分组成:一个字典和一个字节流。字典定义流的参数。流字典条目之一是 /Filter。PDF 文档定义了 10 种过滤器。PdfFileAnalyzer
支持 4 种过滤器。这 4 种过滤器是我发现的唯一普遍使用的过滤器。压缩过滤器 FlateDecode 是当前 PDF 编写器最常用的过滤器。FlateDecode 支持 ZLib deflate 解压缩。LZWDecode 压缩过滤器几年前曾被使用。为了读取较旧的 PDF 文件,此程序支持此过滤器。ASCII85Decode 过滤器将可打印 ASCII 字符转换为二进制。DCTDecode 用于 JPEG 图像压缩。PdfFileAnalyzer
实现前三种的解压缩。DCTDecode 流按原样保存,文件扩展名为 .jpg。这是一个可以查看的图像文件。 - 对象流在 PDF 1.5 中引入。它是一个包含多个间接对象(如下文所述)的流。上面描述的流对象一次压缩一个流。对象流将所有包含的流压缩在一个压缩区域中。
- 交叉引用流在 PDF 1.5 中引入。它是一个包含本文后面将描述的交叉引用表的流。
- 内联图像对象由
PdfInlineImage
实现。它是一个嵌套在流中的流。内联图像是页面描述语言的一部分。它由三个运算符组成:BI-begin image(开始图像)、ID-image data(图像数据)和 EI-end image(结束图像)。BI 和 ID 之间的区域是图像字典,ID 和 EI 之间的区域是图像数据。
3.3. 间接对象
- 间接对象由
PdfIndirectObject
实现。它是 PDF 文档的主要构建块。间接对象是包含在“n 0 obj”和“endobj”之间的任何对象。其他对象可以通过指定“n 0 R”来引用间接对象。“n”是对象编号。“0”是代编号。本程序不支持代编号为 0 以外的值。PDF 规范允许其他数字。多代编号的目的是允许 PDF 修改,同时保留原始文件并附加更改。 - 对象引用是引用间接对象的一种方式。例如,/Pages 2 0 R 是目录对象中的一个字典条目。它是指向 /Pages 对象的指针。pages 对象是间接对象编号 2。
3.4. 运算符和关键字
- 运算符和关键字不被视为 PDF 对象。但是,
PdfFileAnalyzer
程序有一个PdfOp
类和一个PdfKeyword
类,它们都是PdfBase
的派生类。在解析过程中,解析器为每个有效字符序列创建一个PdfOp
或PdfKeyword
。Adobe PDF 文件规范的附录 A“运算符摘要”列出了所有运算符。该列表包含 73 个运算符。以下是一些运算符示例:BT-begin text object(开始文本对象)、G-set gray level for stroking operations(为描边操作设置灰度)、m-move to(移动到)、re-rectangle(矩形)和 Tc-set character spacing(设置字符间距)。关键字示例:stream、obj、endobj、xref。
4. 文件结构
PDF 文件由四部分组成:头部、正文、交叉引用和尾部签名。
- 头部:头部是文件签名。它必须是 %PDF-1.x,其中 x 为 0 到 7。
- 正文:正文区域包含所有间接对象。
- 交叉引用:交叉引用是所有间接对象的开始位置指针表。有两种类型的交叉引用表。原始样式由 ASCII 字符组成。新样式是间接对象中的一个流。信息被编码为二进制数字。交叉引用表的末尾有一个尾部字典。一个文件可以有多个交叉引用区域。
- 尾部签名:尾部签名由以下组成:“startxref”关键字,指向最后一个交叉引用表的字节偏移量,以及尾部签名 %%EOF。请注意:尾部字典是交叉引用区域的一部分。
5. 文件解析
PDF 文件是字节序列。其中一些字节具有特殊含义。
空格定义为:空字符、制表符、换行符、换页符、回车符和空格。
分隔符定义为:(、)、<、>、[、]、{、}、/、% 以及空格字符。
文件解析由 PdfParser
类完成。为了开始解析过程,程序将文件指针设置到要解析的区域。ParseNextItem()
是提取下一个对象的方法。
解析器会跳过空格和注释。如果下一个字节是“(”,则该对象是字符串。如果下一个字节是“[”,则该对象是数组。如果接下来的两个字节是“<<”,则该对象是字典。如果下一个字节是“<”,则该对象是十六进制字符串。如果下一个字节是“/”,则该对象是名称。如果下一个字节不是以上任何一种,解析器将累加接下来的字节,直到找到一个分隔符。分隔符不属于当前标记。该标记可以是整数、实数、运算符或关键字。对于整数,程序将进一步搜索对象引用“n 0 R”或间接对象“n 0 obj”,其中 n 是整数。从 ParseNextItem()
返回的值是第 4 节“对象定义”中的相应对象。对象类作为 PdfBase
类返回。
对于数组或字典,程序将递归调用 ParseNextItem()
来解析数组或字典的内部对象。
6. 文件读取
PdfReader 类是 PDF 文件分析的主要类。入口方法是 OpenPdfFile(String FileName, string Password = null)
。程序打开 PDF 文件以进行二进制读取(一次一个字节)。
文件分析从检查头部签名 %PDF-1.x(x 为 0 到 7)和尾部结束签名 %%EOF 开始。人们可能会认为所有 PDF 编写器都会将头部放在文件的第 0 个位置,将尾部放在文件的末尾。不幸的是,情况并非如此。程序必须在文件的两个末端搜索这两个签名。如果头部签名不在第 0 个位置,则所有间接对象的文件位置指针都必须进行调整。
在尾部签名正前面有一个指向最后一个交叉引用表开始位置的指针。
解析器将文件指针设置到交叉引用表。如果下一个对象是“xref”关键字,则我们拥有原始样式的交叉引用。否则,它是新的基于流的交叉引用。文件可以有多个交叉引用表。文件可以同时拥有新旧样式的表。每个表是对象编号和指向间接引用对象起始点的文件位置指针的列表。对于每个活动对象,程序会创建一个 PdfIndirectObject
对象并将其保存在 ObjectArray
中。该对象为空,仅包含对象编号和位置。对于原始交叉引用表,位置是相对于文件的。对于流类型交叉引用,位置是相对于父间接对象流的。
在此过程中,如果间接对象具有非零的代编号,程序将中止执行。PdfFileAnalyzer
不支持多代。
在交叉引用表末尾,我们有一个尾部字典。为了将此字典包含在分析中,我们创建一个具有负对象编号的虚拟间接对象,并将其保存在其中。
程序在尾部字典中查找四个特定条目。如果找到 /Encrypt,则文件将被解密。接下来,程序查找 /Root,即目录对象的对象编号。如果存在 /XRefStm 条目,则我们拥有两种类型的交叉引用。最后,如果存在 /Prev,则我们还有一个交叉引用表需要处理。
交叉引用处理完成后,我们得到一个包含所有间接对象的数组。此时可用的信息是对象编号和位置。接下来,程序遍历数组,读取和解析每个间接对象。此过程设置对象的值。如果对象是流,则仅解析字典部分。原因是此时可能不知道流的长度。除了对象本身,系统还会为字典和流对象设置对象类型和子类型成员(如果这两个值可用)。
接下来,程序遍历所有对象并处理流对象。流对象的类型为“/ObjStm”。程序读取与这些对象关联的流,并将流拆分为多个间接对象。
接下来,程序搜索所有字典对象和流字典对象中的对象引用。程序正在查找形如“/name n 0 R”的键值对。如果找到这样的对,程序会检查对象类型。如果对象类型在对象解析阶段未设置,则类型将被设置为 /name 值。
下一步是读取所有之前未读取的流。系统从文件中读取流。每个流都被解码并保存到相应的文件中。PdfFileAnalyzer
支持以下过滤器:/FlateDecode、/LZWDecode、/ASCII85Decode 和 /DCTDecode。文本文件将具有 .txt 扩展名,二进制文件为 .bin,图像文件为 .jpg 或 .bmp,字体文件为 .ttf,交叉引用文件为 .xref。/FlateDecode 是 ZLib Deflate 压缩方法。
下一步是构建页面内容。程序从根目录开始跟踪页面树。页面对象不是流对象。换句话说,页面描述命令不直接包含在页面对象中。页面对象的目录中有一个 /Contents 键值对。如果缺少此对,则该页面为空白。内容条目的值可以是单个引用,也可以是引用数组。程序将从一个或多个内容流创建一个虚拟内容流用于页面。页面内容虚拟流保存在 PageObj_xx.txt 和 PageSource_xx.txt 中。前一个文件是页面的实际页面描述内容。后一个文件是相同信息转换为伪 C# 源代码。第 2 节“概述”中有这些文件的示例。
页面内容流由参数和运算符组成。例如,矩形将是四个实数后跟 re。内联图像是此规则的例外。它在第 3 节“对象定义”中已述。
最后,程序生成对象摘要文件 ObjectSummary.txt。该文件显示所有间接对象信息(不包含流)。
7. TestPdfFileAnalyzer 程序
PdfFileAnalyzer 应用程序旨在测试 PDF 文件解析类。如果您想在开发环境之外测试可执行程序,请创建一个 PdfFileAnalyzer 目录,并将 TestPdfFileAnalyzer.exe 程序和 PdfFileAnalyser.dll 类库复制到该目录中并运行程序。如果您从 Visual C# 开发环境中运行项目,请确保在项目属性的“调试”选项卡中定义工作目录。此程序使用 Microsoft Visual C# 2019 开发。
启动程序。可用的选项有:打开 PDF 文件和最近使用的文件。
首次执行程序时,必须运行设置并定义项目目录。此目录将包含为正在分析的每个 PDF 文件创建的所有子目录。
“打开”按钮将显示一个标准的文件选择对话框。导航到要分析的 PDF 文件。
PdfFileAnalyzer 屏幕将变为对象摘要屏幕。
每一行代表一个间接 PDF 对象。每一列是:
- 对象编号。间接对象编号。对于尾部字典,对象编号是一个虚拟编号,它是负数,但在屏幕上显示为 TRn。
- 对象。对象的类型,根据第 4 节“对象定义”。
- 类型。如果对象是字典或流,则类型是 /Type 字典对的值。如果对象不是字典,或者字典不包含 /Type,则显示的值来自对此对象的间接引用。
- 子类型。如果对象是字典或流,并且字典包含 /Subtype 条目,则在此列中显示。
- 父对象编号。如果间接对象是对象流的一部分(参见第 3.2 节“复合对象”),则此列是对象流的对象编号。
- 父索引。如果间接对象是对象流的一部分,则此数字是父对象流中的索引号。
- 对象位置。对于非对象流类型的间接对象文件;这是对象在 PDF 文件中的位置。属于对象流的间接对象;这是相对于父对象的位置。位置以十进制和十六进制表示,供程序员在二进制编辑器中查看 PDF 文件。
- 流位置和流长度。流的位置和长度。位置与对象位置一样,相对于文件或父对象。
要查看 ObjectSummary.txt 文件,请按“摘要”按钮。下面是该文件开头的一个示例。
PDF file name: interactiveform_DATA.pdf Trailer Dictionary ------------------ <</DecodeParms<</Columns 5/Predictor 12>>/Filter/FlateDecode/ID[<f681c578264452c4ab65398fdc7c0daa><b4 25aedbd5c8c544a84d960c3f738458>]/Index[3 1 7 1 18 1 100 5 108 2 116 1 123 1 126 1 128 1 134 1 136 1 173 11]/Info 18 0 R/Length 71/Prev 116/Root 20 0 R/Size 184/Type/XRef/W[1 3 1]>> Indirect Objects ---------------- Object number: 1 Object Value Type: Stream File Position: 67126 Hex: 10636 Stream Position: 67201 Hex: 10681 Stream Length: 695 Hex: 2B7 Object Type: /ObjStm <</Filter/FlateDecode/First 22/Length 695/N 4/Type/ObjStm>> Object number: 2 Object Value Type: Stream File Position: 67915 Hex: 1094B Stream Position: 67990 Hex: 10996 Stream Length: 354 Hex: 162 Object Type: /ObjStm <</Filter/FlateDecode/First 33/Length 354/N 5/Type/ObjStm>> Object number: 3 Object Value Type: Stream File Position: 91134 Hex: 163FE Stream Position: 91193 Hex: 16439 Stream Length: 21616 Hex: 5470 Object Type: /Metadata Object Subtype: /XML <</Length 21616/Subtype/XML/Type/Metadata>>
要查看间接对象的详细信息,请选择一行并按“查看”按钮,或双击一行。将显示对象分析屏幕。
对于所有非流对象,前三个按钮是禁用的。唯一可用的信息是对象本身。您可以在文本或十六进制格式中查看它。
对于流对象,第一个按钮的名称是对象类型。前两个按钮“对象类型”和“流”允许您在查看对象或流之间切换。十六进制和文本允许您以二进制或文本格式查看。如果流是图像,则会显示图像而不是文本。如果流是交叉引用流,则文本格式显示四列:(1)对象编号,(2)类型(0-未使用,1-普通对象,2-流对象),(3)类型 1 的位置和类型 2 的父对象,(4)父对象索引号。如果流是二进制的(例如字体),则只能以十六进制查看。
页面对象被视为流对象。显示的文本是所有内容对象的串联。此外,“源”按钮允许您以类似 C# 代码的形式查看页面描述语言。
图像(.jpg 和 .bmp)可以旋转和缩放。
页面间接对象示例。
Object number: 22 Object Value Type: Dictionary File Position: 13810 Hex: 35F2 Object Type: /Page <</Annots 97 0 R/ArtBox[0 0 612 792]/BleedBox[0 0 612 792]/Contents 81 0 R/CropBox[0 0 612 792]/MediaBox [0 0 612 792]/Parent 16 0 R/Resources<</ColorSpace<</CS0 137 0 R>>/ExtGState<</GS0 138 0 R>>/Font<</C0_0 143 0 R/T1_0 146 0 R/T1_1 149 0 R/T1_2 151 0 R>>/ProcSet[/PDF/Text]/Properties<</MC0<</Metadata 91 0 R>>>>/Shading <</Sh0 153 0 R>>>>/Rotate 0/TrimBox[0 0 612 792]/Type/Page>>
内容流示例。
q 30 438 242.75 4.5 re W n q /GS0 gs 0 4.6875 4.6875 0 29.4979248 437.9062347 cm BX /Sh0 sh EX Q Q q 30 504 552 4.5 re W n q /GS0 gs 0 4.6875 4.6875 0 28.8583374 503.9062347 cm BX /Sh0 sh EX Q Q q 30 757.5 552 4.5 re W n q /GS0 gs 0 4.6875 4.6875 0 28.8583374 757.40625 cm BX /Sh0 sh EX Q Q /CS0 cs 0.39 0.34 0.33 scn /GS0 gs 339.25 442.714 242.75 60.143 re f 29.964 511.229 552 245.952 re f q
8. 历史
- 2012/08/25:版本 1.0,最初修订。
- 2013/04/10 版本 1.1。支持将逗号定义为小数点分隔符的世界地区。
- 2014/03/10 版本 1.2 修复了与带有交叉引用流的 PDF 文件相关的问题。
- 2015/04/02 版本 1.3 删除了与未实现流压缩过滤器相关的错误消息。
- 2019/06/14 版本 2.0 软件分为两个项目:库和测试程序。支持加密文件。
- 2019/06/19 版本 2.1 软件的微小更改。
- 2022/03/05 版本 3.0 升级到 VS 2022 和 .NET 6.0。