PDF 可以在内部携带其他文件。它可以是一个将合同和电子表格打包在一起的组合文件,或是带有源数据附件的报告。当你在代码中提取这些文件时,你希望每个文件都保存为作者给出的名称,而不是 attachment1.bin。本教程展示了如何使用 Aspose.PDF for .NET 在 C# 中提取 PDF 中嵌入的文件并保留原始名称。你将打开一个 PDF,遍历其嵌入的文件,使用 FileSpecification.GetFileName 获取每个文件的名称,并将内容写入文件夹。
PDF 在何处存储嵌入文件的名称
每个嵌入的文件都由文件规范描述,且文件规范可以在多个位置记录名称。它包括 Unicode 文件名、标准文件名,以及文件在文档的嵌入文件集合中列出的键。PDF 生成器并未始终一致地填写这些信息,有些甚至完全省略名称。
Aspose.PDF for .NET 提供 FileSpecification.GetFileName 属性来处理此问题。它会从 Unicode 名称、常规文件名或集合键中解析名称(以可用的为准),因此即使缺少 Unicode 名称,仍能获得可用的名称。您可以为完全没有名称的文件传递一个后备名称。可选的第二个参数控制在没有文件名时是否允许空名称。这样您只需一次调用,而无需自行编写读取 PDF 字典的逻辑。
注意:本文中的示例仅使用 fallback 参数。
Document.EmbeddedFiles 保存文档级别的附件,包括 PDF 组合文档中的文件。通过文件附件注释(通常显示为回形针图标)附加到特定页面的文件则存储在该注释上;提取附加到页面的文件 介绍了这些内容。
使用 Aspose.PDF 从 PDF 中提取嵌入文件
- 一个 .NET 项目。
- Aspose.PDF for .NET 26.9 或更高版本,这是引入
FileSpecification.GetFileName的版本。从 NuGet 安装它:
Install-Package Aspose.PDF
或者使用 .NET CLI:
dotnet add package Aspense.PDF
- 包含嵌入文件的 PDF。示例使用名为
PDFPortfolio.pdf的作品集。
没有许可证时,Aspose.PDF 以评估模式运行。要在没有评估限制的情况下进行测试,请请求一个免费临时许可证。
提取每个嵌入文件及其原始名称
以下方法打开 PDF,将每个嵌入的文件保存到输出文件夹中,使用其解析后的名称,并打印每个写入文件的路径。
using System;
using System.IO;
using Aspose.Pdf;
public static class EmbeddedFileExtractor
{
public static void ExtractEmbeddedFiles(string inputPdf, string outputDir)
{
Directory.CreateDirectory(outputDir);
using (var document = new Document(inputPdf))
{
foreach (FileSpecification fileSpec in document.EmbeddedFiles)
{
// Resolve the stored name, then keep only the file name part.
string fileName = Path.GetFileName(fileSpec.GetFileName("attachment.bin"));
string outputPath = Path.Combine(outputDir, fileName);
using (var output = new FileStream(outputPath, FileMode.Create))
{
fileSpec.Contents.CopyTo(output);
}
Console.WriteLine($"Saved: {outputPath}");
}
}
}
}
使用 PDF 路径和您想要放置文件的文件夹来调用它:
EmbeddedFileExtractor.ExtractEmbeddedFiles("PDFPortfolio.pdf", "ExtractedFiles");
每个步骤的作用
打开文档。 new Document(inputPdf) 加载 PDF。using 块在处理完成时释放文件句柄,即使抛出异常也是如此。
遍历附件。 document.EmbeddedFiles 返回每个文档级嵌入文件的一个 FileSpecification。
解析名称。 fileSpec.GetFileName("attachment.bin") 返回存储的名称,如果 PDF 未提供,则返回 attachment.bin。为您的应用程序选择一个合理的后备方案。
剥离文件夹段。 存储的名称可能包含类似 reports/2026/q3.xlsx 的路径,或类似 ..\..\startup.bat 的伪造值。将结果通过 Path.GetFileName 处理后会缩减为 q3.xlsx 或 startup.bat,因此文件无法写入 outputDir 之外的路径。即使您信任今天处理的 PDF,也请保留此步骤。
写入内容。 fileSpec.Contents 是嵌入文件数据的流。CopyTo 将其写入新文件,且不进行任何转换。
预期结果
输出文件夹为每个文档级附件包含一个文件,文件名与其在 PDF 中存储的名称相同。对于包含电子表格和 Word 文档的组合文件,控制台输出类似如下:
Saved: ExtractedFiles\budget-2026.xlsx
Saved: ExtractedFiles\service-agreement.docx
文件名取决于您的 PDF。提取的文件包含相同的嵌入数据,因此每个文件都会在能够处理其格式的应用程序中打开。
处理重复和不安全的名称
基本示例有三个在实际 PDF 中会遇到的弱点:
- 两个附件可以共享同一个名称,且
FileMode.Create会悄悄覆盖第一个附件。当多个附件没有名称时,它们都会回退为attachment.bin,也会出现同样的情况。 - 在某个操作系统上有效的名称,在另一个操作系统上可能包含无效字符,例如 Windows 上的
:。 - 以路径分隔符结尾的存储名称会导致
Path.GetFileName返回空字符串。
此辅助程序处理所有三种情况。它会替换无效字符,在名称为空时回退,并在文件已存在时添加计数器:
private static string GetSafeOutputPath(string outputDir, string resolvedName, string fallbackName)
{
string fileName = Path.GetFileName(resolvedName);
foreach (char c in Path.GetInvalidFileNameChars())
{
fileName = fileName.Replace(c, '_');
}
if (string.IsNullOrWhiteSpace(fileName))
{
fileName = fallbackName;
}
string baseName = Path.GetFileNameWithoutExtension(fileName);
string extension = Path.GetExtension(fileName);
string outputPath = Path.Combine(outputDir, fileName);
int counter = 1;
while (File.Exists(outputPath))
{
outputPath = Path.Combine(outputDir, $"{baseName} ({counter}){extension}");
counter++;
}
return outputPath;
}
在提取循环中,将构建 fileName 和 outputPath 的两行代码替换为:
string outputPath = GetSafeOutputPath(outputDir, fileSpec.GetFileName("attachment.bin"), "attachment.bin");
因为该助手会检查已有文件,所以在同一文件夹中运行两次提取时会生成带编号的副本,而不是覆盖。如果希望每次运行都得到全新的结果,请先清空文件夹。
提取页面附加的文件
如果 EmbeddedFiles 为空,但在 PDF 查看器中看到回形针图标,文件可能是通过页面上的文件附件注释附加的。该注释在 File 属性中持有自己的 FileSpecification,因此相同的名称解析和相同的帮助程序适用。此方法检查每一页并将每个附加的文件保存到输出文件夹:
using Aspose.Pdf.Annotations;
public static void ExtractPageAttachments(string inputPdf, string outputDir)
{
Directory.CreateDirectory(outputDir);
using (var document = new Document(inputPdf))
{
foreach (Page page in document.Pages)
{
foreach (Annotation annotation in page.Annotations)
{
if (annotation is FileAttachmentAnnotation attachment && attachment.File != null)
{
string outputPath = GetSafeOutputPath(outputDir, attachment.File.GetFileName("attachment.bin"), "attachment.bin");
using (var output = new FileStream(outputPath, FileMode.Create))
{
attachment.File.Contents.CopyTo(output);
}
Console.WriteLine($"Saved: {outputPath}");
}
}
}
}
}
要收集 PDF 中的所有附件,请使用相同的输出文件夹运行这两种方法。如果 PDF 从两个位置引用了同一个文件,GetSafeOutputPath 会将第二个副本保存为带编号的名称,而不是覆盖第一个。
下一步
要进一步处理附件,例如在不提取的情况下添加、删除或列出它们,请参阅 Aspose.PDF for .NET 文档。FileSpecification 类参考 列出了每个嵌入文件可读取的其他名称和描述属性。如果 PDF 未按预期提取,请在 Aspose.PDF 支持论坛 中发布示例。
FAQs
How does FileSpecification.GetFileName choose a name? 它会从嵌入文件的 Unicode 名称、常规文件名或集合键中解析名称,以先可用的为准。如果都不可用,则返回您传入的回退名称。该方法在 Aspose.PDF for .NET 26.9 中添加。
这适用于不是文档集的 PDF 吗? 是的,针对文档级别附加的文件。通过文件附件注释(通常显示为回形针图标)附加到特定页面的文件则存储在注释上,因此请从
FileAttachmentAnnotation.File读取它们,如 Extract files attached to pages 所示。为什么要在 Path.GetFileName 中包装 GetFileName 的结果?
PDF 中存储的名称是由创建文件的人设置的,可能包含文件夹路径。Path.GetFileName只保留文件名,因此每个附件都会写入您的输出文件夹。如果两个附件具有相同的名称会怎样? 使用
FileMode.Create时,第二个文件会覆盖第一个。检查路径是否已存在,并添加数字后缀,如在 处理重复和不安全的名称 中所示。我可以在不将附件保存到磁盘的情况下处理它吗? 是的。
Contents属性是一个Stream,因此您可以将其复制到MemoryStream,上传,或将其传递给其他库,而不是写入FileStream。
