PDF files can contain more than text and images. In some cases, they also include embedded files such as Excel spreadsheets, Word documents, CSV files, or supporting reports.

For example, a project review PDF might contain an Excel workbook with detailed financial data and a CSV file attached to a chart. If you need those files for further processing, manually opening the PDF and saving each attachment can become tedious, especially when handling multiple documents.

In C#, you can extract these embedded files directly from the PDF and save them to a local directory. However, there's an important detail: PDF attachments can be stored at the document level or attached to specific pages as annotations.

This tutorial covers both types and shows how to extract them into separate folders without overwriting files that share the same name.

Prerequisites

Before getting started, install the required PDF processing library in your C# project.

In Visual Studio, open Tools > NuGet Package Manager > Package Manager Console and run:

Install-Package Spire.PDF 

Alternatively, use the .NET CLI:

dotnet add package Spire.PDF 

This example uses Spire.PDF for .NET, which supports reading PDF attachments without requiring Adobe Acrobat to be installed.

The examples assume you have an existing PDF named Project_Review.pdf containing one or more embedded files.

Understanding the Two Types of PDF Attachments

Before extracting files, it's useful to understand how attachments are stored in a PDF.

1. Document-Level Attachments

These files are attached to the PDF document as a whole. They usually appear in the Attachments panel of a PDF reader rather than at a specific position on a page.

For example, a project report might include Financial_Summary.xlsx and Meeting_Agenda.docx as document-level attachments.

In C#, these files can be accessed through the PdfDocument.Attachments collection.

2. Attachment Annotations

An attachment annotation is associated with a specific location on a PDF page. It may appear as a paperclip or another attachment icon.

For example, a chart on page 2 might have an attached CSV file containing its original data.

These attachments are stored within the page annotation collections rather than the document-level attachment collection.

This distinction matters because checking only PdfDocument.Attachments may miss files attached to individual pages.

Step 1: Load the PDF and Prepare the Output Folders

Start by loading the PDF document.

PdfDocument pdf = new PdfDocument(); pdf.LoadFromFile("Project_Review.pdf"); 

Next, create two output folders: one for document-level attachments and another for attachment annotations.

string outputRoot = "ExtractedFiles"; string documentFolder = Path.Combine( outputRoot, "DocumentAttachments"); string annotationFolder = Path.Combine( outputRoot, "AnnotationAttachments"); Directory.CreateDirectory(documentFolder); Directory.CreateDirectory(annotationFolder); 

Directory.CreateDirectory() creates a folder if it doesn't already exist. It also works when the directory is already present.

Keeping the two attachment types in separate folders makes the extracted files easier to organize and review.

Step 2: Extract Document-Level Attachments

Document-level attachments are available through the Attachments property of the PdfDocument object.

First, retrieve the attachment collection:

PdfAttachmentCollection attachments = pdf.Attachments; 

Then iterate through the collection and save each embedded file to disk.

for (int i = 0; i < attachments.Count; i++) { PdfAttachment attachment = attachments[i]; string outputPath = GetUniqueFilePath( documentFolder, attachment.FileName); File.WriteAllBytes(outputPath, attachment.Data); Console.WriteLine( $"Extracted document attachment: {outputPath}"); } 

Here's what happens in this code:

  • attachments.Count returns the number of document-level attachments.
  • attachment.FileName provides the filename stored in the PDF.
  • attachment.Data contains the embedded file's binary content.
  • File.WriteAllBytes() saves that content as a separate file.

The GetUniqueFilePath() helper, included in the complete example below, ensures that filenames are handled safely and existing files aren't accidentally overwritten.

For example, if the PDF contains Financial_Summary.xlsx, the program saves it as an Excel file in the output directory.

The extraction process doesn't convert the attachment to another format. It writes the embedded bytes to disk, preserving the file's original content.

Step 3: Extract Files Attached to PDF Pages

Document-level extraction is straightforward, but it doesn't cover attachment annotations.

To find these files, you need to inspect the annotations on each PDF page.

Start by iterating through all pages:

for (int pageIndex = 0; pageIndex < pdf.Pages.Count; pageIndex++) { PdfAnnotationCollection annotations = pdf.Pages[pageIndex].Annotations; // Process annotations on this page } 

Each page may contain different kinds of annotations, including comments, highlights, links, and file attachments.

We only want annotations that contain embedded files.

To identify them, check whether an annotation is a PdfAttachmentAnnotationWidget:

for (int pageIndex = 0; pageIndex < pdf.Pages.Count; pageIndex++) { PdfAnnotationCollection annotations = pdf.Pages[pageIndex].Annotations; for (int i = 0; i < annotations.Count; i++) { if (annotations[i] is PdfAttachmentAnnotationWidget fileAttachment) { string outputPath = GetUniqueFilePath( annotationFolder, fileAttachment.FileName, $"page_{pageIndex + 1}_" ); File.WriteAllBytes( outputPath, fileAttachment.Data); Console.WriteLine( $"Extracted page {pageIndex + 1} attachment: {outputPath}"); } } } 

This code checks each annotation's type before attempting to extract its data.

When an attachment annotation is found, the program gets its filename and binary content, then saves it to the annotation output folder.

Notice that the generated filename includes the page number.

For example, an attachment named Chart_Data.csv found on page 2 is saved as:

page_2_Chart_Data.csv 

This makes it easier to identify which PDF page the attachment came from, especially when several pages contain related files.

It also helps distinguish files with similar names.

Complete C# Example: Extract All PDF Attachments

The following program combines both extraction methods.

It reads document-level attachments, searches every page for attachment annotations, and saves the files into separate folders.

It also includes a helper method that removes directory information from attachment filenames and generates unique output paths.

using System; using System.IO; using Spire.Pdf; using Spire.Pdf.Attachments; using Spire.Pdf.Annotations; class Program { static void Main() { string inputFile = "Project_Review.pdf"; string outputRoot = "ExtractedFiles"; string documentFolder = Path.Combine( outputRoot, "DocumentAttachments"); string annotationFolder = Path.Combine( outputRoot, "AnnotationAttachments"); // Create output directories Directory.CreateDirectory(documentFolder); Directory.CreateDirectory(annotationFolder); PdfDocument pdf = new PdfDocument(); try { // Load the PDF document pdf.LoadFromFile(inputFile); int extractedCount = 0; // Extract document-level attachments PdfAttachmentCollection attachments = pdf.Attachments; for (int i = 0; i < attachments.Count; i++) { PdfAttachment attachment = attachments[i]; string outputPath = GetUniqueFilePath( documentFolder, attachment.FileName); File.WriteAllBytes( outputPath, attachment.Data); Console.WriteLine( $"Document attachment: {outputPath}"); extractedCount++; } // Extract page-level attachment annotations for (int pageIndex = 0; pageIndex < pdf.Pages.Count; pageIndex++) { PdfAnnotationCollection annotations = pdf.Pages[pageIndex].Annotations; for (int i = 0; i < annotations.Count; i++) { if (annotations[i] is PdfAttachmentAnnotationWidget fileAttachment) { string outputPath = GetUniqueFilePath( annotationFolder, fileAttachment.FileName, $"page_{pageIndex + 1}_" ); File.WriteAllBytes( outputPath, fileAttachment.Data); Console.WriteLine( $"Page {pageIndex + 1} attachment: {outputPath}"); extractedCount++; } } } Console.WriteLine( $"Extraction completed. Files extracted: {extractedCount}"); } finally { // Close the PDF document pdf.Close(); } } // Generate a safe, unique output file path static string GetUniqueFilePath( string folder, string originalName, string prefix = "") { // Remove directory information from the filename string fileName = Path.GetFileName( (originalName ?? "").Replace('\\', '/')); if (string.IsNullOrWhiteSpace(fileName) || fileName == "." || fileName == "..") { fileName = "attachment.bin"; } // Replace invalid filename characters foreach (char invalidChar in Path.GetInvalidFileNameChars()) { fileName = fileName.Replace(invalidChar, '_'); } fileName = prefix + fileName; string nameWithoutExtension = Path.GetFileNameWithoutExtension(fileName); string extension = Path.GetExtension(fileName); string outputPath = Path.Combine(folder, fileName); // Avoid overwriting files with the same name int counter = 2; while (File.Exists(outputPath) || Directory.Exists(outputPath)) { string newName = $"{nameWithoutExtension}_{counter}{extension}"; outputPath = Path.Combine(folder, newName); counter++; } return outputPath; } } 

Expected Output

Suppose Project_Review.pdf contains two document-level attachments and one attachment annotation on page 2.

After running the program, the output folder will look similar to this:

ExtractedFiles/ DocumentAttachments/ Financial_Summary.xlsx Meeting_Agenda.docx AnnotationAttachments/ page_2_Chart_Data.csv 

The console output will show the extracted filenames and the total number of files saved.

If two attachments share the same filename, the helper method generates a new name, such as Financial_Summary_2.xlsx, instead of overwriting the existing file.

This is particularly useful when extracting attachments from PDFs created by different systems, where filename conflicts may occur.

Extract Only Certain Types of Files

Sometimes you don't need every attachment. You may only want Excel spreadsheets, CSV files, or another particular file type.

For example, if you're processing financial reports, you might want to extract only Excel attachments.

You can check the attachment's filename extension before saving it:

for (int i = 0; i < attachments.Count; i++) { PdfAttachment attachment = attachments[i]; string extension = Path.GetExtension( attachment.FileName).ToLowerInvariant(); if (extension == ".xlsx" || extension == ".xls") { string outputPath = GetUniqueFilePath( documentFolder, attachment.FileName); File.WriteAllBytes( outputPath, attachment.Data); Console.WriteLine($"Extracted: {outputPath}"); } } 

This example filters document-level attachments by extension.

You can apply the same logic to attachment annotations by checking fileAttachment.FileName inside the page annotation loop.

Keep in mind that a filename extension doesn't guarantee the actual file type. If you process attachments from untrusted sources, validate their contents before importing or opening them.

Common Issues When Extracting PDF Attachments

1. The PDF Has Attachments, but None Are Extracted

One possible reason is that the files are attached as page annotations rather than document-level attachments.

If you only check:

pdf.Attachments 

you may miss embedded files associated with particular pages.

In that case, inspect the page annotation collections as demonstrated earlier.

Also, make sure the PDF actually contains embedded files. A hyperlink to an external document is not the same as an embedded PDF attachment.

2. Multiple Attachments Have the Same Filename

Different attachments may have identical filenames.

For example, a PDF might contain several files named data.csv, each associated with a different chart.

Saving them directly with File.WriteAllBytes() using the same output path would overwrite earlier files.

The complete example avoids this by checking whether the destination already exists and appending a numeric suffix when necessary.

For page-level attachments, including the page number in the filename also provides useful context.

3. An Extracted File Cannot Be Opened

Extracting an attachment preserves its embedded data, but it doesn't guarantee that the original file is valid.

If an extracted file cannot be opened, check whether the attachment was already damaged or contains data that doesn't match its filename extension.

For example, a file named report.xlsx may not actually contain a valid Excel workbook.

It is also worth checking whether the source PDF uses an unusual or unsupported attachment structure.

4. What About Password-Protected PDFs?

If a PDF is encrypted, you may need the correct password to open it before accessing its attachments.

Loading an encrypted PDF without the required credentials can prevent the extraction process from proceeding.

Make sure you have permission to access the document and its embedded files.

5. Are Extracted PDF Attachments Safe to Open?

Not necessarily.

PDF attachments can contain executable files, scripts, or other potentially unsafe content.

If you're processing documents uploaded by external users, consider storing extracted files in a controlled directory, scanning them, and validating their types before further processing.

Avoid automatically executing extracted files or opening them with privileged applications.

Conclusion

Extracting embedded files from a PDF in C# requires slightly different approaches depending on where the attachments are stored.

Document-level attachments can be retrieved directly from the PDF's attachment collection, while attachment annotations require iterating through the pages and identifying the appropriate annotation objects.

By handling both types, you can recover supporting files such as Excel workbooks, Word documents, and CSV datasets without manually opening each attachment in a PDF reader.

This approach is useful for document management systems, report processing, and other workflows where PDF files serve as containers for related documents.