---
title: "Create PDF/A in C#"
url: https://recursiveai.org/onepdf/articles/create-pdf-a-csharp/
description: "Create PDF/A-2 and PDF/A-3 files in C# from documents you generate or edit, attach XML data to PDF/A-3, and check any PDF. Limits first: it doesn’t convert arbitrary files."
date: 2026-10-26
author: OnePDF team
---

# Create PDF/A in C#

Create PDF/A-2 and PDF/A-3 files in C# from documents you generate or edit, attach XML data to PDF/A-3, and check any PDF. Limits first: it doesn’t convert arbitrary files.

October 26, 2026 · OnePDF team

To create PDF/A in C# with OnePDF, generate or edit the document and save it with `PdfSaveOptions.PdfA`. The save adds the color profile, the XMP metadata and the font embedding the standard asks for, checks the result, and writes PDF/A-2 or PDF/A-3 at level B, U or A. The limit first, because it decides whether this page is for you: **OnePDF doesn’t convert arbitrary PDFs to PDF/A.** It writes PDF/A for documents it made or edited and for files that already meet the requirements. A file from elsewhere that breaks a rule, say a font that isn’t embedded, is refused, with every violation listed. If your job is “take any PDF users upload and make it PDF/A”, you need a converter (see [converting existing PDFs](#convert)). If you generate the documents you archive, read on. OnePDF is pure C#, licensed MIT OR Apache-2.0. Every sample was compiled and run against OnePDF 1.11.0 on .NET 10, and every output passes the veraPDF validator.

## PDF/A-2 or PDF/A-3, level B, U or A

PDF/A (ISO 19005) is the archival profile of PDF: everything needed to show the pages is inside the file, with nothing that depends on the viewer, the machine or a network. OnePDF writes parts 2 and 3, which are both based on PDF 1.7:

| Choose | When |
|---|---|
| **PDF/A-2b** | The pages must look the same everywhere, years from now. The minimum. |
| **PDF/A-2u** | 2b plus a Unicode mapping for all text, so the archive can be searched and copied from. A sensible default for generated documents. |
| **PDF/A-2a** | 2u plus a tagged structure, for accessibility. See [PDF/UA in C#](https://recursiveai.org/onepdf/articles/pdf-ua-csharp/), which writes both in one file. |
| **PDF/A-3b, 3u, 3a** | The same levels, and the file may carry attachments of any type, such as the XML behind an invoice. |

PDF/A-1 (based on PDF 1.4) and PDF/A-4 (based on PDF 2.0) are not written.

## Before you start

*Terminal · NuGet*

```
dotnet new console -o ArchivePdf
cd ArchivePdf
dotnet add package RecursiveAI.OnePdf.Layout --version 1.11.0
```

The layout package brings in `RecursiveAI.OnePdf` and `RecursiveAI.OnePdf.Fonts`. OnePDF targets .NET 10. The samples use `photo.jpg` (any JPEG), `invoice.xml` (any XML file) and `report.pdf`, a six-page report made with OnePdf.Layout.

## Create a PDF/A document

*Program.cs · OnePdf.Layout 1.11.0 · .NET 10*

```
using OnePdf;
using OnePdf.Layout;
 
LayoutDocument minutes = LayoutDocument.Compose(doc =>
{
    doc.Title("Minutes, October meeting").Author("Harbor committee").Language("en-US");
    doc.Page(page =>
    {
        page.Size(PageSize.A4);
        page.Margin(54);
        page.Content().Column(col =>
        {
            col.Spacing(10);
            col.Item().Heading(1).Text("Minutes, October meeting").FontSize(20).Bold();
            col.Item().Text("Present: six members. The committee approved the harbor trial report and set the spring dates.");
            col.Item().Image(File.ReadAllBytes("photo.jpg")).AltText("The north quay at low tide");
        });
    });
});
 
using (FileStream file = File.Create("minutes-pdfa.pdf"))
    minutes.Save(file, new PdfSaveOptions { PdfA = new PdfAOptions { Part = PdfAPart.Part2, Conformance = PdfAConformance.U } });
 
using PdfDocument check = PdfDocument.Open("minutes-pdfa.pdf");
Console.WriteLine($"claims PDF/A-{check.Xmp?.PdfAPart}{check.Xmp?.PdfAConformance?.ToLowerInvariant()}, title \"{check.Xmp?.Title}\"");
```

*Output · Console*

```
claims PDF/A-2u, title "Minutes, October meeting"
```

veraPDF passes `minutes-pdfa.pdf` as PDF/A-2u. The save did four things on top of the layout:

- **Output intent.** It added an sRGB output intent with an ICC profile that OnePDF generates from the published sRGB parameters, so colors have a defined meaning.
- **XMP metadata.** It wrote the XMP packet with the PDF/A part and level and the title, author and dates from the document information, so the two agree.
- **Fonts.** Layout text is already in embedded subsets of real fonts, with a Unicode map for each.
- **The check.** It checked the finished file against the requirements OnePDF can verify, and would have thrown `PdfAException` with the violations instead of writing a file that claims PDF/A falsely.

The photo is embedded byte for byte as the JPEG it was: PDF/A allows JPEG, so nothing is decoded or recompressed.

## Create PDF/A-3 with an XML attachment

PDF/A-3 lets a file carry other files, each tied to the document with a relationship such as `Data`, `Source`, `Alternative` or `Supplement`. Invoices are the usual case: the PDF people read and the XML a system reads, in one archived file. Lay out the invoice, attach the data, save:

*Program.cs · OnePdf.Layout 1.11.0 · .NET 10*

```
using OnePdf;
using OnePdf.Layout;
 
// 1. Lay out the invoice into memory.
LayoutDocument invoice = LayoutDocument.Compose(doc =>
{
    doc.Title("Invoice 2026-0142").Author("Example Co.").Language("en-US");
    doc.Page(page =>
    {
        page.Size(PageSize.A4);
        page.Margin(54);
        page.Content().Column(col =>
        {
            col.Spacing(10);
            col.Item().Heading(1).Text("Invoice 2026-0142").FontSize(20).Bold();
            col.Item().Table(table =>
            {
                table.Columns(c => { c.Relative(); c.Constant(80); });
                table.CellPadding(4);
                table.HeaderRow(r => { r.Cell().Text("Item").Bold(); r.Cell().AlignEnd().Text("Amount").Bold(); });
                table.Row(r => { r.Cell().Text("Dredging survey"); r.Cell().AlignEnd().Text("1,200.00"); });
                table.Row(r => { r.Cell().Text("Quay inspection"); r.Cell().AlignEnd().Text("450.00"); });
            });
        });
    });
});
using var pdf = new MemoryStream();
invoice.Save(pdf);
 
// 2. Attach the machine-readable data and save as PDF/A-3u.
using PdfDocument doc = PdfDocument.Open(pdf.ToArray(), new PdfOpenOptions { AllowEditing = true });
doc.AttachFile("invoice.xml", File.ReadAllBytes("invoice.xml"), new PdfAttachmentOptions
{
    MimeType = "text/xml",
    Relationship = PdfAttachmentRelationship.Data,
    Description = "Invoice data",
    ModificationDate = File.GetLastWriteTimeUtc("invoice.xml"),   // PDF/A-3 needs a date on every attachment
});
doc.Save("invoice-pdfa3.pdf", new PdfSaveOptions { PdfA = new PdfAOptions { Part = PdfAPart.Part3, Conformance = PdfAConformance.U } });
 
// 3. Read it back.
using PdfDocument check = PdfDocument.Open("invoice-pdfa3.pdf");
foreach (PdfEmbeddedFile file in check.EmbeddedFiles)
    Console.WriteLine($"{file.Name}: {file.MimeType}, {file.Relationship}, {file.ReadContent().Length} bytes");
```

*Output · Console*

```
invoice.xml: text/xml, Data, 114 bytes
```

veraPDF passes `invoice-pdfa3.pdf` as PDF/A-3u. The attachment is listed in the document’s embedded files and in the catalog’s `/AF` array with its relationship, MIME type and modification date, which PDF/A-3 requires. Without `ModificationDate`, OnePDF uses the document’s own modification or creation date.

**ZUGFeRD and Factur-X** invoices use this same mechanism, plus their own XMP extension schema that names the invoice profile. OnePDF writes only the XMP properties defined by the XMP specification itself and replaces a packet that doesn’t already claim the requested PDF/A level, so it doesn’t produce those formats as shipped in 1.11.0.

## Edit a PDF and save it as PDF/A

The same save option works on a document you opened and changed. Here a report made earlier with OnePdf.Layout gets an “ARCHIVED” stamp in Helvetica, one of the 14 standard fonts that PDF viewers are expected to supply. PDF/A doesn’t allow relying on that, so the font must be embedded. Registering the `RecursiveAI.OnePdf.Fonts` package lets the save embed Liberation Sans Bold, a metric-compatible face, in its place:

*Program.cs · OnePDF 1.11.0 · .NET 10*

```
using OnePdf;
using OnePdf.Fonts;
 
FontsPackage.Register();   // Liberation faces stand in for Helvetica, Times and Courier
 
// report.pdf was made with OnePdf.Layout. Stamp it and archive it.
using PdfDocument doc = PdfDocument.Open("report.pdf", new PdfOpenOptions { AllowEditing = true });
doc.AddTextStamp("ARCHIVED", doc.AddStandardFont("Helvetica-Bold"), 72,
    new PdfStampOptions { Opacity = 0.2, RotationDegrees = 45 });
doc.Save("report-archived.pdf", new PdfSaveOptions { PdfA = new PdfAOptions { Part = PdfAPart.Part2, Conformance = PdfAConformance.B } });
 
Console.WriteLine($"{doc.PageCount} pages stamped and saved as PDF/A-2b");
```

*Output · Console*

```
6 pages stamped and saved as PDF/A-2b
```

veraPDF passes `report-archived.pdf` as PDF/A-2b, and `pdffonts` lists three fonts, all embedded subsets: the report’s two Liberation Sans faces and the stamp’s `LiberationSans-Bold`. The substitution applies only to standard fonts that OnePDF itself added, by `AddStandardFont` or in form-field appearances it generated. Fonts that came with a file are never replaced.

A PDF/A save also sets the print flag on annotations and generates missing form-field appearances. It rewrites the file, so existing signatures don’t survive it: [sign](https://recursiveai.org/onepdf/articles/sign-pdf-csharp/) after the PDF/A save, as an incremental update.

## Check whether a PDF is PDF/A

A file’s PDF/A claim is in its XMP metadata, and a claim isn’t proof. `doc.Xmp` reads the claim, and `CheckPdfA` checks the file against a target without changing it, read-only documents included. Here it reads the archived minutes from above and the IRS Form W-9 ([fw9.pdf](https://www.irs.gov/pub/irs-pdf/fw9.pdf)):

*Program.cs · OnePDF 1.11.0 · .NET 10*

```
using OnePdf;
 
(string Path, PdfAOptions Target)[] files =
[
    ("minutes-pdfa.pdf", new PdfAOptions { Part = PdfAPart.Part2, Conformance = PdfAConformance.U }),
    ("fw9.pdf", new PdfAOptions { Part = PdfAPart.Part2, Conformance = PdfAConformance.B }),
];
 
foreach (var (path, target) in files)
{
    using PdfDocument doc = PdfDocument.Open(path);
 
    // What the file claims, from its XMP metadata, and what a check against the target finds.
    string claim = doc.Xmp?.PdfAPart is { } part ? $"claims PDF/A-{part}{doc.Xmp.PdfAConformance?.ToLowerInvariant()}" : "no PDF/A claim";
    IReadOnlyList<PdfAViolation> problems = doc.CheckPdfA(target);
 
    Console.WriteLine($"{path}: {claim}, {problems.Count} violations of {Name(target)}");
    foreach (PdfAViolation v in problems.DistinctBy(v => v.Clause).Take(4))
        Console.WriteLine($"  {v.Clause}: {v.Message}");
}
 
static string Name(PdfAOptions o) => $"PDF/A-{(o.Part == PdfAPart.Part3 ? 3 : 2)}{o.Conformance.ToString().ToLowerInvariant()}";
```

*Output · Console*

```
minutes-pdfa.pdf: claims PDF/A-2u, 0 violations of PDF/A-2u
fw9.pdf: no PDF/A claim, 33 violations of PDF/A-2b
  6.3.3: The Widget annotation has no appearance dictionary.
  6.2.11.4.1: The font ZapfDingbats is not embedded.
  6.6.4: The XMP packet does not identify the file as PDF/A-2b (pdfaid:part=-, pdfaid:conformance=-).
  6.4.2: The interactive form has an /XFA entry.
```

Each violation carries the ISO 19005-2 clause, a message, the object number and the page. A list stops at twenty findings of one kind. OnePDF checks what it can verify from the file’s structure. It is not a validator, and a clean check doesn’t prove conformance. Before you rely on an archive, run [veraPDF](https://verapdf.org/software/), the open-source validator, over it. The tests behind OnePDF’s PDF/A writer use it for the same purpose.

## Converting existing PDFs to PDF/A

“PDF to PDF/A” searches usually mean any file, from anywhere. `PreparePdfA` shows how far OnePDF gets with one. It makes every repair that needs no judgement, in memory, and returns what is still wrong:

*Program.cs · OnePDF 1.11.0 · .NET 10*

```
using OnePdf;
using OnePdf.Fonts;
 
FontsPackage.Register();
var target = new PdfAOptions { Part = PdfAPart.Part2, Conformance = PdfAConformance.B };
 
using PdfDocument doc = PdfDocument.Open("fw9.pdf", new PdfOpenOptions { AllowEditing = true });
int before = doc.CheckPdfA(target).Count;
 
// Make every repair that needs no judgement, in memory, and see what is still wrong.
IReadOnlyList<PdfAViolation> left = doc.PreparePdfA(target);
Console.WriteLine($"fw9.pdf: {before} violations before, {left.Count} after the repairs");
foreach (var group in left.GroupBy(v => v.Clause))
    Console.WriteLine($"  {group.Key} ({group.Count()}): {group.First().Message}");
```

*Output · Console*

```
fw9.pdf: 33 violations before, 7 after the repairs
  6.2.11.4.1 (3): The font ZapfDingbats is not embedded.
  6.4.2 (1): The interactive form has an /XFA entry.
  6.5.1 (1): The document has a JavaScript name tree.
  6.1.13 (1): The integer 202302201302860 is outside the 32-bit range.
  6.8 (1): The file contains an embedded file; PDF/A-2 files written by OnePDF carry none. Use PDF/A-3 for attachments.
```

The repairs dealt with 26 findings: the output intent, the XMP claim and the missing form-field appearances. What remains needs decisions OnePDF doesn’t make for you. The W-9 refers to ZapfDingbats, a symbol font it doesn’t embed and for which no substitute ships. Its form also carries an XFA description and JavaScript, and it already has an embedded file, which PDF/A-2 written by OnePDF doesn’t carry. A converter would substitute a font, delete the scripts and the XFA, and rewrite whatever else breaks a rule.

That is what conversion products do. Aspose.PDF, for example, documents a `Convert` call that validates and converts, with a `ConvertErrorAction` to delete what can’t be converted and a font-substitution class for fonts that are neither embedded nor installed ([Aspose.PDF docs](https://docs.aspose.com/pdf/net/convert-pdf-to-pdfa/)). It is commercial software. Whatever you use, validate the result: conversion changes the file, and the archive should hold what you meant to keep.

With OnePDF, the dependable route is to archive the documents you generate: lay them out, or edit files OnePDF made, and save them as PDF/A, as in the samples above.

## What makes a PDF/A save fail

- **DeviceCMYK color.** The output intent OnePDF writes is RGB, so CMYK content is refused.
- **Fonts that aren’t embedded**, other than standard fonts OnePDF added itself.
- **Encryption.** PDF/A forbids it. Add `Encryption = PdfEncryptionAction.Remove` to the same save to write the file decrypted.
- **Attachments in PDF/A-2.** Use PDF/A-3.
- **JavaScript, XFA, multimedia annotations** and the other features the standard rules out.
- **A part that can’t be read**, such as a damaged font program. The save fails instead of quietly leaving the part out and claiming conformance.

A PDF/A save reads and rewrites the whole document, so it holds the file in memory. The full list of what is written, checked and not checked, clause by clause, is in [PDF/A](https://recursiveai.org/onepdf/docs/pdfa/).

## Where next

- [Create PDF/A](https://recursiveai.org/onepdf/cookbook/create-pdf-a/), the short cookbook recipe.
- [PDF/A](https://recursiveai.org/onepdf/docs/pdfa/): every repair and every check, clause by clause.
- [PDF/UA in C#](https://recursiveai.org/onepdf/articles/pdf-ua-csharp/) for accessible files, and PDF/A-2a or 3a in the same save.
- [Create a PDF in C#](https://recursiveai.org/onepdf/articles/create-pdf-csharp/) for invoices with tables, headers and page numbers.
- [Choosing a C# PDF library](https://recursiveai.org/onepdf/articles/csharp-pdf-library/) for licenses and features compared.
