如何使用 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;还需检查文件实际使用的附件关联结构。
- 如果两个目标名称同时存在,示例只保存遍历时遇到的第一个。如需固定优先级,应分别查找并明确选择顺序。
备注:内容仅供参考。
