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 中提取嵌入文件

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

  1. How does FileSpecification.GetFileName choose a name? 它会从嵌入文件的 Unicode 名称、常规文件名或集合键中解析名称,以先可用的为准。如果都不可用,则返回您传入的回退名称。该方法在 Aspose.PDF for .NET 26.9 中添加。

  2. 这适用于不是文档集的 PDF 吗? 是的,针对文档级别附加的文件。通过文件附件注释(通常显示为回形针图标)附加到特定页面的文件则存储在注释上,因此请从 FileAttachmentAnnotation.File 读取它们,如 Extract files attached to pages 所示。

  3. 为什么要在 Path.GetFileName 中包装 GetFileName 的结果?
    PDF 中存储的名称是由创建文件的人设置的,可能包含文件夹路径。Path.GetFileName 只保留文件名,因此每个附件都会写入您的输出文件夹。

  4. 如果两个附件具有相同的名称会怎样? 使用 FileMode.Create 时,第二个文件会覆盖第一个。检查路径是否已存在,并添加数字后缀,如在 处理重复和不安全的名称 中所示。

  5. 我可以在不将附件保存到磁盘的情况下处理它吗? 是的。Contents属性是一个Stream,因此您可以将其复制到MemoryStream,上传,或将其传递给其他库,而不是写入FileStream。