You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何不借助外部库开发读取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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 00:52:26