PAYATHON 2026

如何使用 C# 和 iText 提取 PDF 中的 ZUGFeRD XML 附件?

数据小陈

你已经找到了附件入口,可以从匹配条目的 entry.Value 获取文件规范字典,读取 /EF 指向的附件流,再用 File.WriteAllBytes() 保存为 Factur1234.xml,不必遍历 PDF 中的所有 stream。

附件名称和内容如何对应

EmbeddedFiles 名称树中的条目包含名称和对应的文件规范对象。文件规范字典中的 /EF 字典引用了内嵌文件流,可以通过 GetAsStream(PdfName.F) 取得;下面的代码也会尝试读取 /UF 对应的流。iText 官方的 附件提取示例使用了相同的 /EF → /F → GetBytes() 读取方式。

PDF 中还有页面内容、图像和字体等 stream。遍历所有 stream 会失去附件名称与内容之间的直接对应关系,还会导出许多无关数据。

提取并保存 XML 的 C# 示例

这个示例适用于你已经能够在 EmbeddedFiles 名称树中匹配到目标名称的情况。使用时,将输入和输出路径替换为实际路径:

using System;
using System.IO;
using iText.Kernel.Pdf;

public static class InvoiceXmlExtractor
{
    public static void Extract(string sourcePdf, string destinationXml)
    {
        using (PdfDocument pdfDocument =
            new PdfDocument(new PdfReader(sourcePdf)))
        {
            PdfNameTree names = pdfDocument.GetCatalog()
                .GetNameTree(PdfName.EmbeddedFiles);

            foreach (var entry in names.GetNames())
            {
                string attachmentName = entry.Key.ToUnicodeString();

                if (!string.Equals(attachmentName, "zugferd-invoice.xml",
                        StringComparison.OrdinalIgnoreCase) &&
                    !string.Equals(attachmentName, "factur-x.xml",
                        StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }

                PdfObject fileSpecObject = entry.Value;
                if (fileSpecObject is PdfIndirectReference reference)
                {
                    fileSpecObject = reference.GetRefersTo();
                }

                PdfDictionary fileSpec = fileSpecObject as PdfDictionary;
                PdfDictionary embeddedFiles =
                    fileSpec?.GetAsDictionary(PdfName.EF);

                PdfStream xmlStream =
                    embeddedFiles?.GetAsStream(PdfName.UF) ??
                    embeddedFiles?.GetAsStream(PdfName.F);

                if (xmlStream == null)
                {
                    throw new InvalidDataException(
                        "已找到目标附件名称,但未找到对应的嵌入文件流。");
                }

                byte[] xmlBytes = xmlStream.GetBytes();
                if (xmlBytes == null)
                {
                    throw new InvalidDataException("无法读取 XML 附件内容。");
                }

                File.WriteAllBytes(destinationXml, xmlBytes);
                return;
            }

            throw new FileNotFoundException(
                "EmbeddedFiles 名称树中未找到目标 XML 附件。");
        }
    }
}

调用方式:

InvoiceXmlExtractor.Extract(
    @"C:\Invoices\invoice.pdf",
    @"C:\Invoices\Factur1234.xml");

代码找到第一个匹配的附件后,会保存文件并返回。输出名称由 destinationXml 决定:源附件名可以是 zugferd-invoice.xml 或 factur-x.xml,保存时统一使用 Factur1234.xml。

为什么应该使用 GetBytes()

PdfStream.GetBytes() 返回解码后的流字节,适合恢复嵌入的 XML 文件。GetBytes(false) 返回未经流过滤器解码的数据。如果附件流经过压缩,直接将这些字节写成 .xml 文件,得到的仍可能是压缩数据。两者的区别见 iText 9.5.0 的 PdfStream API 文档。

如果 GetBytes() 抛出异常,应保留异常并检查流解码问题,不要直接改用 GetBytes(false) 并将结果视为提取成功。直接保存解码后的字节也能避免不必要的字符串转换,保留 XML 原有的字符编码。

保存前需要注意什么

  • 输出目录必须已经存在,程序也需要有写入权限。File.WriteAllBytes() 会覆盖同路径的现有文件。
  • 示例按名称树的键匹配附件。如果其他 PDF 的名称树键与实际文件名不同,需要进一步检查文件规范字典中的 /UF、/F 文件名字段。
  • 如果名称树中没有匹配项,只能说明这条查找路径没有找到目标,不能据此断言 PDF 完全没有嵌入 XML;还需检查文件实际使用的附件关联结构。
  • 如果两个目标名称同时存在,示例只保存遍历时遇到的第一个。如需固定优先级,应分别查找并明确选择顺序。

备注:内容仅供参考。