A PDF может содержать внутри другие файлы. Это может быть портфолио, объединяющее контракты и таблицы, или отчёт с прикреплёнными исходными данными. Когда вы извлекаете эти файлы программно, вы хотите, чтобы каждый из них сохранялся под именем, заданным его автором, а не как attachment1.bin. В этом руководстве показано, как извлечь вложенные файлы из PDF с оригинальными именами на C# с использованием Aspose.PDF for .NET. Вы откроете 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 or later, это релиз, в котором был представлен FileSpecification.GetFileName. Установите его из NuGet:
Install-Package Aspose.PDF

Или с помощью .NET CLI:

dotnet add package Aspose.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") возвращает сохранённое имя, или attachment.bin, если PDF его не предоставляет. Выберите запасной вариант, который имеет смысл для вашего приложения.

Удалить сегменты папки. Сохранённое имя может включать путь, например 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 вы видите значок скрепки, файл, вероятно, прикреплён через аннотацию вложения файла на странице. Аннотация содержит собственный FileSpecification в свойстве File, поэтому применяется то же разрешение имён и тот же вспомогательный метод. Этот метод проверяет каждую страницу и сохраняет каждый вложенный файл в папку вывода:

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 documentation. FileSpecification class reference перечисляет свойства other name и description, которые можно прочитать из каждого встроенного файла. Если PDF не извлекается как ожидается, разместите пример на Aspose.PDF support forum.

Часто задаваемые вопросы

  1. Как метод FileSpecification.GetFileName выбирает имя? Он определяет имя из Unicode‑имени вложенного файла, обычного имени файла или ключа коллекции, в зависимости от того, что доступно. Если ничего не доступно, он возвращает запасное имя, которое вы передаете. Метод был добавлен в Aspose.PDF for .NET 26.9.

  2. Работает ли это с PDF, которые не являются портфолио? Да, для файлов, вложенных на уровне документа. Файлы, вложенные в конкретную страницу через аннотацию вложения файла (обычно отображаемую как значок скрепки), хранятся в самой аннотации, поэтому их следует читать из FileAttachmentAnnotation.File, как показано в Извлечение файлов, вложенных в страницы.

  3. Зачем оборачивать результат GetFileName в Path.GetFileName? Имя, сохранённое в PDF, задаётся тем, кто создал файл, и может включать сегменты папок. Path.GetFileName оставляет только имя файла, поэтому каждое вложение записывается в вашу выходную папку.

  4. Что происходит, если у двух вложений одинаковое имя? При использовании FileMode.Create второй файл перезаписывает первый. Проверьте, существует ли уже путь, и добавьте числовой суффикс, как показано в Обработка дублирующих и небезопасных имен.

  5. Могу ли я обработать вложение, не сохраняя его на диск? Да. Свойство Contents является Stream, поэтому вы можете скопировать его в MemoryStream, загрузить его или передать другой библиотеке вместо записи в FileStream.