Recursive AI

Accessible PDF: PDF/UA-1 and PDF/A-2a

All onerender recipes

Write HTML to a PDF that declares PDF/UA-1, PDF/A-2a or both with OneRender, and see how a document that breaks a rule is refused with the reasons.

C#
using OneRender.Pdf;
 
string html = """
<!DOCTYPE html>
<html lang="en">
<head><title>Return policy</title></head>
<body style="font: 12pt/1.5 serif">
  <h1>Return policy</h1>
  <p>Items can be returned within 30 days. <a href="https://example.com/returns">Start a return</a>.</p>
  <table>
    <thead><tr><th>Item type</th><th>Window</th></tr></thead>
    <tbody><tr><td>Books</td><td>30 days</td></tr><tr><td>Gift cards</td><td>Not returnable</td></tr></tbody>
  </table>
  <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" alt="Store logo" width="40" height="40">
</body>
</html>
""";
 
foreach (PdfProfile profile in new[] { PdfProfile.PdfUA1, PdfProfile.PdfA2a, PdfProfile.PdfA2aAndUA1 })
{
    var options = new HtmlPdfOptions { Output = new PdfOutputOptions { Profile = profile, Author = "Harbor Street Books" } };
    HtmlPdfResult r = HtmlPdf.ToPdf(html, File.Create($"policy-{profile}.pdf"), options);
    Console.WriteLine($"{profile}: {r.PageCount} page, {new FileInfo($"policy-{profile}.pdf").Length:N0} bytes");
}
 
// A document that breaks a rule is refused, with the rules it breaks.
try
{
    HtmlPdf.ToPdf("<p>No title, no language</p>", new HtmlPdfOptions { Output = new PdfOutputOptions { Profile = PdfProfile.PdfUA1 } });
}
catch (PdfRenderException e)
{
    Console.WriteLine($"refused: {e.Message}");
}
Text
PdfUA1: 1 page, 17,082 bytes
PdfA2a: 1 page, 19,746 bytes
PdfA2aAndUA1: 1 page, 20,918 bytes
refused: The document does not meet PDF/UA-1: 7.2: The catalog has no /Lang entry naming the document's natural language.; 7.1: The XMP packet has no dc:title: a PDF/UA file needs a title.; 7.1: The catalog's /ViewerPreferences does not set /DisplayDocTitle true, so viewers would show the file name instead of the title.

What makes it pass

  • Structure from the HTML. The heading, paragraph, link, the table with column header scopes, and the image with its alt become the structure tree. An image with an empty alt, or role="presentation", is marked as an artifact, and so are backgrounds and borders.
  • A title and a language, from <title> and <html lang> or from Title and Language in PdfOutputOptions. The title is shown in the viewer’s title bar.
  • Text that maps to Unicode, through /ToUnicode and /ActualText, so assistive technology and extraction read the source text.
  • A missing glyph isn’t drawn as a box. With a profile set, a character no font has keeps its space and stays in the text.

How it is written

A profile other than None builds the file in memory, then saves it again with the XMP identification and checks it against the rules OnePDF knows. If any fail, ToPdf throws PdfRenderException listing them, so you never ship a file that claims a standard it breaks. Without a profile the file streams and is still tagged; the profile adds the identification and the check.

Good to know

  • PdfA2aAndUA1 writes one file that is both.
  • In the upstream test suite, the six document templates and the paged sample pass veraPDF’s PDF/UA-1 validation with zero failed rules. veraPDF runs as a separate program and isn’t part of the packages.
  • Whole sentences in complex scripts (Arabic, Devanagari, Thai) can fail veraPDF rule 7.21.7 in 1.0.0, because glyphs inside multi-glyph clusters have no ToUnicode entry. The text still extracts correctly.
  • To check the HTML itself before rendering, see Check accessibility with rendered facts.

The tag mapping and the checks are in PDF output.