Recursive AI

Create PDF/UA

All onepdf recipes

Generate a tagged PDF in C# that is PDF/UA-1 and PDF/A-2a in one file, and see what OnePDF refuses when a title or alternate text is missing.

Terminal
dotnet add package RecursiveAI.OnePdf.Layout --version 1.11.0
C#
using OnePdf;
using OnePdf.Layout;
 
byte[] logo = File.ReadAllBytes("logo.png");
 
LayoutDocument summary = LayoutDocument.Compose(doc =>
{
    doc.Title("Harbor trial summary").Author("Port office").Language("en-US");
    doc.Page(page =>
    {
        page.Size(PageSize.Letter);
        page.Margin(54);
        page.Header().Text("Port office").FontSize(9);              // headers and footers are tagged as artifacts
        page.Content().Column(col =>
        {
            col.Spacing(10);
            col.Item().Width(48).Image(logo).AltText("Port office logo");
            col.Item().Heading(1).Text("Harbor trial summary").FontSize(22).Bold();
            col.Item().Text("Six crews took part. Catches by quay:");
            col.Item().Table(table =>
            {
                table.Columns(c => { c.Relative(); c.Constant(100); });
                table.CellPadding(4);
                table.CellBorder(0.5, Colors.Grey);
                table.HeaderRow(r => { r.Cell().Text("Quay").Bold(); r.Cell().AlignEnd().Text("Catch (kg)").Bold(); });
                table.Row(r => { r.Cell().Text("North"); r.Cell().AlignEnd().Text("1,204"); });
                table.Row(r => { r.Cell().Text("South"); r.Cell().AlignEnd().Text("987"); });
            });
            col.Item().Heading(2).Text("Next steps").FontSize(14).Bold();
            col.Item().List(list =>
            {
                list.Bulleted();
                list.Item().Text("Repeat the trial in spring.");
                list.Item().Hyperlink("https://example.com/harbor").Text("Read the full report online.");
            });
        });
    });
});
 
// PDF/UA-1 and PDF/A-2a in one file.
using (FileStream file = File.Create("summary-ua.pdf"))
{
    summary.Save(file, new PdfSaveOptions
    {
        PdfUA = new PdfUAOptions(),
        PdfA = new PdfAOptions { Part = PdfAPart.Part2, Conformance = PdfAConformance.A },
    });
}
 
// What OnePDF cannot decide for you is refused, with every reason listed.
LayoutDocument untitled = LayoutDocument.Compose(doc =>
{
    doc.Language("en-US");                                          // no title
    doc.Page(page => page.Content().Column(col =>
    {
        col.Item().Width(48).Image(logo);                           // no alternate text
        col.Item().Heading(2).Text("Starts at level 2");            // heading levels must start at 1
    }));
});
try
{
    untitled.Save(new MemoryStream(), new PdfSaveOptions { PdfUA = new PdfUAOptions() });
}
catch (PdfUAException e)
{
    foreach (PdfUAViolation v in e.Violations)
        Console.WriteLine($"checkpoint {v.Checkpoint}: {v.Message}");
}
Text
checkpoint 06: The XMP packet has no dc:title: a PDF/UA file needs a title.
checkpoint 07: The catalog's /ViewerPreferences does not set /DisplayDocTitle true, so viewers would show the file name instead of the title.
checkpoint 13: A Figure structure element has neither alternate text (/Alt) nor replacement text (/ActualText).
checkpoint 14: The first heading is H2; the heading hierarchy has to start at H1.

summary-ua.pdf passes the veraPDF validator as PDF/UA-1 and as PDF/A-2a. The second document was refused and nothing was written.

How it works

OnePdf.Layout tags everything it lays out: headings become H1 to H6, tables Table with TH header cells, lists L with LI items, images Figure with your alternate text, links Link, and headers, footers, borders and backgrounds are marked as artifacts. A save with PdfSaveOptions.PdfUA then:

  • repairs the clerical parts: the document language (from PdfUAOptions.Language when the document has none), showing the title instead of the file name, tab order, link descriptions and the XMP identification;
  • refuses what only you can provide, listing every violation with its Matterhorn Protocol checkpoint: a title, alternate text for each image (or Artifact() for decoration), headings that start at level 1 without skipping levels, header cells in every table.

PdfUAOptions { Part = PdfUAPart.Part2 } writes PDF/UA-2 on PDF 2.0 instead. PDF/UA-2 can’t be combined with PDF/A-2 or PDF/A-3, which are PDF 1.7 formats.

Good to know

  • OnePDF never guesses alternate text, a title, a language or the role of content. Existing untagged files are refused. You can tag one with roles you supply through the tagging API: see Tagging.
  • A clean save is not proof of conformance. PDF/UA also asks for things only a person can judge: a sensible reading order, meaningful alternate text, enough contrast. Check with a validator and a screen reader.
  • CheckPdfUA() reports the violations of any document, including read-only ones, without changing it.
  • The PDF/UA save checks the finished file, so the whole document is held in memory while it is written. Plain Save(path) streams page by page.

The full lists of repairs and checks are in PDF/UA, and the tagging rules of the layout package in Layout.

A longer walk-through with PDF/UA-2 and tagging an existing file: PDF/UA in C#.