Recursive AI

Convert a PDF to PDF/A in C#

All onepdf recipes

Open the PDF, embed the fonts it is missing, let PreparePdfA make the repairs that need no judgement, and save with PdfSaveOptions.PdfA when nothing is left in the way. The program below converts a one-page document exported from a word processor, a public test file from the W3C, and lists what stands in the way for the IRS Form W-9, which can’t be converted yet.

OnePDF doesn’t yet convert files that break the rules in ways that need content rewritten, such as CMYK color, JavaScript or an XFA form. For those it lists every problem instead of writing a file that claims PDF/A falsely. This page will be updated when conversion ships.

Terminal
dotnet new console -o PdfToPdfA && cd PdfToPdfA
dotnet add package RecursiveAI.OnePdf --version 1.14.0
dotnet add package RecursiveAI.OnePdf.Fonts --version 1.14.0
curl -LO https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf
curl -LO https://www.irs.gov/pub/irs-pdf/fw9.pdf

Put this in Program.cs:

C#
using OnePdf;
using OnePdf.Fonts;
 
// metric-compatible faces for fonts a file names but doesn't embed
FontsPackage.Register();
 
string input = args.Length > 0 ? args[0] : "dummy.pdf";
string output = Path.GetFileNameWithoutExtension(input) + "-pdfa.pdf";
var pdfa = new PdfAOptions { Part = PdfAPart.Part2, Conformance = PdfAConformance.B };
 
using PdfDocument doc = PdfDocument.Open(input,
    new PdfOpenOptions { AllowEditing = true });
Console.WriteLine($"{input}: {doc.CheckPdfA(pdfa).Count} PDF/A-2b problems as it is");
 
// 1. Embed the fonts the file names but doesn't carry.
PdfSubstituteEmbedReport fonts = doc.EmbedSubstituteFonts();
foreach (PdfSubstituteEntry e in fonts.Entries)
    if (e.Outcome != PdfSubstituteOutcome.AlreadyEmbedded)
        Console.WriteLine($"  font {e.BaseFont}: {e.Outcome}");
 
// 2. Make the repairs that need no judgement, in memory, and see what is left.
IReadOnlyList<PdfAViolation> left = doc.PreparePdfA(pdfa);
if (left.Count == 0)
{
    doc.Save(output, new PdfSaveOptions { PdfA = pdfa });
    Console.WriteLine($"Saved {output} as PDF/A-2b");
}
else
{
    Console.WriteLine($"Not converted: {left.Count} problems OnePDF can't fix yet");
    foreach (var group in left.GroupBy(v => v.Clause))
        Console.WriteLine($"  {group.Key}: {group.First().Message}");
}

Run it on the W3C file:

Terminal
dotnet run
Text
dummy.pdf: 2 PDF/A-2b problems as it is
Saved dummy-pdfa.pdf as PDF/A-2b

Its fonts were embedded already, so the repairs added an sRGB output intent and the XMP metadata that names the file PDF/A-2b. The veraPDF validator passes dummy-pdfa.pdf as PDF/A-2b.

Then on the W-9:

Terminal
dotnet run -- fw9.pdf
Text
fw9.pdf: 33 PDF/A-2b problems as it is
  font ZapfDingbats: Embedded
  font Helvetica: Embedded
  font HelveticaLTStd-Bold: NoSubstitute
Not converted: 5 problems OnePDF can't fix yet
  6.4.2: The interactive form has an /XFA entry.
  6.5.1: The document has a JavaScript name tree.
  6.2.11.4.1: The font HelveticaLTStd-Bold is not embedded.
  6.1.13: The integer 202302201302860 is outside the 32-bit range.
  6.8: The file contains an embedded file; PDF/A-2 files written by OnePDF carry none. Use PDF/A-3 for attachments.

The fonts step embedded substitutes for two of the W-9’s three missing fonts. No metric-compatible face ships for HelveticaLTStd-Bold, so it stays. The rest needs content removed or rewritten: the XFA form, the JavaScript, an attachment, and a number too large for PDF/A. Nothing was saved.

What the steps do

  • EmbedSubstituteFonts embeds a subset of a metric-compatible face for each font the file names without embedding: Liberation Sans for Arial and Helvetica, Liberation Serif for Times, Liberation Mono for Courier, Noto faces for Symbol and ZapfDingbats. Each glyph keeps its width, so no text moves. The letter shapes are the substitute’s.
  • PreparePdfA adds the sRGB output intent and the XMP metadata, sets print flags on annotations, generates missing form-field appearances, and returns every problem left, each with its ISO 19005 clause.
  • The save checks again and throws PdfAException with the same list rather than write a file that fails.

Limits

  • No conversion yet. OnePDF doesn’t convert CMYK color (its output intent is RGB), remove JavaScript or XFA, or flatten transparency. Many real files hit one of these: of the public IRS publications we tried, most used CMYK. Commercial converters such as Aspose.PDF rewrite such content. Whatever you use, validate the result.
  • Parts 2 and 3 only. PDF/A-2 and PDF/A-3 at levels B, U and A; not PDF/A-1 or PDF/A-4. A file with attachments needs PDF/A-3: set Part = PdfAPart.Part3.
  • Fonts without a substitute, such as Verdana, Calibri or HelveticaLTStd-Bold, stay unembedded unless you supply a face you are allowed to embed, through FaceForFont in PdfSubstituteEmbedOptions.
  • Encryption is forbidden in PDF/A: add Encryption = PdfEncryptionAction.Remove to the save options.
  • Signatures don’t survive the save, which rewrites the file. Sign afterwards.
  • OnePDF isn’t a validator. It checks what it can verify from the file’s structure, and some files pass its check but not veraPDF. IRS Publications 5 and 1345 convert without complaint, yet veraPDF fails both: their images carry XMP metadata from a stock photo library and Adobe tools that PDF/A doesn’t allow, and Publication 5’s fonts have incomplete CIDSets. OnePDF neither checks nor fixes either. Run veraPDF before you rely on an archive.

For documents you generate, writing PDF/A directly always works: see Create a PDF/A file. The fonts step on its own is in Embed missing fonts. Every repair and check, clause by clause, is in PDF/A.