如何不借助外部库开发读取PDF嵌入文件的Visual Studio控制台应用
实现PDF嵌入文件读取的核心步骤
1. 先吃透PDF基础结构
要自己实现,必须先解析PDF的核心组成:
- 文件头:以
%PDF-1.x开头(x为版本号,如1.7),先确认文件合法性。 - 交叉引用表(XRef Table):记录所有PDF对象的字节偏移,是定位任意对象的关键入口。
- 目录(Catalog):PDF的根对象,嵌入式文件的入口就挂在它的
Names -> EmbeddedFiles节点下。
2. 定位嵌入式文件的名称树
PDF用**名称树(Name Tree)**组织嵌入文件,路径固定:Catalog -> Names -> EmbeddedFiles
- 如果节点下是
Kids,说明是分支节点,需要递归遍历所有子节点; - 如果是
Names,这是一个成对出现的数组:[文件名1, 文件对象1, 文件名2, 文件对象2,...],直接按组解析即可。
3. 解析文件规范与流对象
每个嵌入文件对应一个文件规范对象(File Specification),类型标记为/Filespec:
- 从对象的
/EF(Embedded File)字典里,取/F(或/UF,对应Unicode文件名)对应的流对象; - 流对象结构为:
<< /Length 字节数 /Filter 过滤方式... >> stream [二进制内容] endstream,如果有/Filter(比如/FlateDecode,即ZIP压缩),需要先解码流内容才能拿到原始文件数据。
4. 读取并保存嵌入文件
拿到解码后的二进制流后,直接写入本地文件即可,文件名从文件规范对象的/F(ASCII字符串)或/UF(UTF-16BE编码的Unicode字符串)中提取。
关键代码片段(C#控制台应用)
读取PDF并定位Catalog对象
// 读取PDF二进制内容 byte[] pdfBytes = File.ReadAllBytes("target.pdf"); string pdfContent = Encoding.ASCII.GetString(pdfBytes); // 定位交叉引用表起始位置 int startxrefPos = pdfContent.LastIndexOf("startxref"); int xrefOffset = int.Parse(pdfContent.Substring(startxrefPos + 9).Trim()); // 从Trailer中获取Root(Catalog)的对象编号 string trailer = pdfContent.Substring(pdfContent.IndexOf("trailer", startxrefPos)); int rootObjNum = int.Parse(ExtractDictValue(trailer, "/Root").Split(' ')[0]); // 读取Catalog对象内容 string catalogObj = ReadPdfObject(pdfBytes, rootObjNum);
遍历EmbeddedFiles名称树
// 从Catalog中获取Names字典 string namesDict = ExtractDictValue(catalogObj, "/Names"); // 获取EmbeddedFiles节点 string embeddedTree = ExtractDictValue(namesDict, "/EmbeddedFiles"); // 处理Names数组 if (embeddedTree.Contains("/Names")) { string namesArray = ExtractDictValue(embeddedTree, "/Names"); string[] entries = SplitPdfArray(namesArray); for (int i = 0; i < entries.Length; i += 2) { // 提取文件名 string fileName = entries[i].Trim('(', ')'); // 获取文件规范对象引用 string specRef = entries[i + 1]; string fileSpec = ReadPdfObject(pdfBytes, int.Parse(specRef.Split(' ')[0])); // 获取嵌入文件流引用 string efDict = ExtractDictValue(fileSpec, "/EF"); string streamRef = ExtractDictValue(efDict, "/F"); // 读取并解码流 byte[] fileBytes = ReadAndDecodeStream(pdfBytes, streamRef); // 保存文件 File.WriteAllBytes(fileName, fileBytes); } }
辅助方法:从PDF字典中提取值
static string ExtractDictValue(string dictContent, string key) { int keyPos = dictContent.IndexOf(key); if (keyPos == -1) return null; int valueStart = keyPos + key.Length; while (valueStart < dictContent.Length && char.IsWhiteSpace(dictContent[valueStart])) valueStart++; // 处理字符串、对象引用、字典三种常见值类型 if (dictContent[valueStart] == '(') { int end = dictContent.IndexOf(')', valueStart); return dictContent.Substring(valueStart, end - valueStart + 1); } else if (char.IsDigit(dictContent[valueStart])) { int end = valueStart; while (end < dictContent.Length && (char.IsDigit(dictContent[end]) || dictContent[end] == ' ' || dictContent[end] == 'R')) end++; return dictContent.Substring(valueStart, end - valueStart).Trim(); } else if (dictContent[valueStart] == '<') { int braceCount = 1; int end = valueStart + 1; while (end < dictContent.Length && braceCount > 0) { if (dictContent[end] == '<') braceCount++; else if (dictContent[end] == '>') braceCount--; end++; } return dictContent.Substring(valueStart, end - valueStart); } return null; }
注意事项
- 字符串编码:PDF字符串分ASCII(
(...))和十六进制(<ABCDEF>),Unicode文件名/UF是UTF-16BE编码,需要单独处理。 - 流解码:除了
/FlateDecode,可能遇到/ASCII85Decode、/ASCIIHexDecode,需要实现对应解码逻辑。 - 交叉引用流:部分PDF用XRef Stream代替传统XRef Table,需要先解码该流才能获取对象偏移。
- 间接引用:PDF对象可能是
n m R格式的间接引用,需通过XRef表找到对应字节偏移读取内容。
内容的提问来源于stack exchange,提问作者MilanHoesl
相关产品推荐
相关产品推荐

