Skip to main content

Accessible PDFs (PDF/UA-1)

With pdf.ua, a render is a tagged PDF in the PDF/UA-1 format (ISO 14289-1), the format screen readers and other assistive technology read a PDF by. Formfeed writes the tags from your HTML, repairs what the browser's output gets wrong, checks every file against the machine-checkable rules of the standard before it is delivered, and reports what the check said.

A passed check does not make a document accessible. A validator can decide about two thirds of PDF/UA-1. Whether an alternative text describes its image, whether the reading order makes sense and whether the contrast is sufficient stays with the person who writes the template. This page says where that line runs.

What you get​

  • Tags from your HTML. Headings, paragraphs, lists, tables, links and images become the structure tree of the PDF. Semantic HTML is what makes a document readable: an h2 is a heading, a div in large type is not.
  • Repairs. The browser's tagged output breaks PDF/UA-1 in ways no template can fix. Formfeed fixes them in the finished file; the list is below.
  • Checked before delivery. The file is held against the PDF/UA-1 rules of veraPDF, the open-source validator of the PDF Association and the Open Preservation Foundation: 106 rules in the release Formfeed runs.
  • A report on every render. The render's accessibility field says whether the file passed, and for each rule that failed what to change in the template and in which elements.
  • The claim only with the proof. A file names itself PDF/UA-1 in its metadata only when it passed. One that failed is never labelled as conforming.

It works for the PDF output of HTML templates and of ad-hoc renders with html or url. See Limits for what it does not combine with.

What the check proves​

conformant: true means: the file passed every rule of PDF/UA-1 that a machine can check. That covers the structure: every piece of content is tagged or marked as decoration, headings do not skip levels, figures have an alternative text, tables are regular, the document has a title and a language, fonts are embedded.

It does not mean that the document is accessible. Nobody but a person can check:

  • whether an alternative text says what its image shows, or just that there is one;
  • whether the order in which the content is read makes sense. It is the order of your HTML, whatever CSS shows on the page;
  • whether the contrast and the type size are readable;
  • whether a heading says what its section is about, and a link where it leads;
  • whether a passage in another language is marked as such.

Look at a new template once with a screen reader, or with a tool that shows the reading order, before you rely on it. NVDA on Windows and VoiceOver on macOS read PDFs, and the free PDF Accessibility Checker (PAC) shows the structure of a file and checks it with rules of its own.

Formfeed is a tool that writes the format and checks it. It gives no legal advice: whether a document of yours has to be accessible, and under which law, is for you to find out.

Switching it on​

pdf.ua is a setting like any other: it is stored with the template version and merged with the request's settings, the request winning. When the template and the request both give an object, the two are merged key by key.

ValueMeaning
false or nullOff, the default.
trueOn, with the strict check: the same as { "check": "strict" }.
{ "check": "strict" } or {}A file that fails the check fails the render. Nothing is delivered.
{ "check": "report" }The file is delivered either way; the report says whether it passed.

Any other value in a request, such as "yes" or an object with another key, answers 400 invalid_request; stored in a template, such a value declares nothing, and formfeed validate reports it. pdf.ua makes the PDF a tagged PDF, so it implies pdf.tagged. It is part of every plan, needs no key scope beyond the one that renders, and applies to PDF output only.

In the template​

Open the Page setup in the editor, go to the Document section and turn on Accessible PDF (PDF/UA-1). When the file fails the check chooses between Fail the render and Report only. The same section holds the Title, which PDF/UA-1 requires, with Author, Subject and Keywords.

With the switch on, the Flow and Paged previews check the page they show. The Template check card in the section lists what they found, and the Problems list names each finding at its line. The status bar says that the template renders PDF/UA-1. Save and publish the version as for any other setting.

Image templates have no such switch, since PDF/UA is a format of PDF.

In a project, the declaration is in settings.json:

{
"pdf": {
"metadata": { "title": "Notice {{ notice.number }}" },
"ua": true
},
"locale": "en-GB"
}

In the request​

curl https://api-eu.formfeed.dev/v1/renders \
-H "Authorization: Bearer $FORMFEED_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"template": "notice",
"data": { "notice": { "number": "2026-0042" } },
"settings": { "pdf": { "ua": true } }
}'

"ua": false in a request switches off what the template declares, for that request.

Strict or report​

strict (the default)report
The file passesDelivered, with the PDF/UA identifier.Delivered, with the PDF/UA identifier.
The file fails422 pdfua_validation_failed, no file.Delivered without the identifier; conformant: false.
The validator is down503 conformance_unavailable.The same: a file is never delivered unchecked.

Strict is what true means, because a customer who asked for a conforming file and silently got another would find out from their own customer. Use report while you bring a template into shape, or where a document must go out either way and you follow the reports.

The editor's True render always uses report, so you see the file and the verdict together.

Writing a template that passes​

The template check finds these in the page before it is printed: in the editor's Flow and Paged previews, in formfeed dev, and in every render with pdf.ua, where it names the elements behind the rules that failed. An error is a fault the validator fails the file for (the column Rule names the rule of veraPDF's PDF/UA-1 profile, by clause and test number); a warning is one no rule catches and a reader hears all the same. Nothing that is not painted (display: none, visibility: hidden) is checked, and images, figures, headings, tables and links inside aria-hidden="true" are not either, because the browser leaves them out of the tags.

CodeSeverityRuleWhat it findsWhat to do
document-titleerror7.1-9, 7.1-10No title. A template takes it from pdf.metadata.title or a <title> in its head; an html or url source only from its own <title> element, since pdf.metadata.title is not used for them.Set Title in the Document section. It may contain expressions: Invoice {{ invoice.number }}. In an html source, write a <title>.
image-alterror7.3-1An <img> with none of alt, aria-label, aria-labelledby and title, unless it is role="presentation" or role="none".Say what it shows. alt="" is a decision too: it marks decoration, which a screen reader passes over.
figure-nameerror7.3-1A canvas, or any element other than img with role="img" (a chart, an svg), without a name: no aria-label, aria-labelledby, title attribute or <title> child.Give the chart helper an alt; give other figures an aria-label.
heading-ordererror7.4.2-1The first heading is not an h1, or a level is skipped on the way down (an h3 after an h1). Elements with role="heading" count with their aria-level. Going back up skips nothing.Choose the level by structure and the size by CSS.
table-rowserror7.2-43In a table with header cells, rows that span different numbers of columns, colspan and rowspan counted.Make every row add up.
table-cell-emptyerror7.2-43A td without content in a table that has header cells. One finding per table, at its first such cell, with the number of empty cells.The browser writes no cell for it, so its row comes out shorter than the others. Put a non-breaking space (&nbsp;) or a dash into it. A decoration or an image with an empty alt does not count as content.
masked-texterror7.20-2CSS mask or mask-image on an element with text.Mask a picture, not the text. No repair reaches this.
filtered-textwarningCSS filter on an element with text.The browser turns such text into a picture nobody can read. Filter the picture, not the text.
document-languagewarningNo lang attribute on <html> and no locale in the template's or the request's settings. A template always has one: Formfeed writes its locale into lang, en-GB when the template sets none. So this comes from an html document or a url page.Set lang on <html>, or locale in the settings: with pdf.ua, Formfeed then writes it onto a document that has no lang. Without either, the file says en-US, whatever the language. Set the locale of every template too, or its file says British English.
table-headerswarningA table of two or more rows without a th (or an element with role="columnheader" or role="rowheader").Mark the header cells as th. Without them, cells are read without their column, and the table is not tagged as one.
link-namewarningA link without text, aria-label, title or an image with alt inside.Give it text. Otherwise its address is read out.
generated-textwarningLetters or digits from CSS ::before or ::after, counters included.Write them into the template. Generated content is not read out.
fixed-textwarningText in a position: fixed element, on a document of several pages.It is printed on every page and read once per page. Put it in the header or footer instead.
running-textwarningWords or numbers in the header or footer that the body does not contain. Page numbers and the words around them (Page, of) do not count; at most ten are listed.A screen reader skips page headers and footers. Say in the body what a reader needs: bank details, a case number.

Reading order is not on the list. It is the order of your HTML, and no check can tell whether CSS that moves content around shows what you meant.

Two of them, the missing title and an image written without alt, show in the source alone. formfeed validate and POST /templates/{id}/validate report those for a template that declares pdf.ua: as errors where the check is strict, since every render of the template would then fail, and as warnings where it is report.

Page headers and footers deserve a second look. In PDF/UA they are artifacts: page furniture that is not part of the content, like the page number. That is right for a page number and wrong for an IBAN that stands only in the footer. Formfeed marks them as the standard asks and warns you about text that stands nowhere else.

What Formfeed repairs​

You do not have to do anything for these. They are what stands between the browser's tagged output and the standard:

  • Metadata. The file gets the XMP metadata PDF/UA-1 requires, with the title, and its viewer is told to show the title instead of the file name.
  • Decoration. Everything the browser draws without a tag is marked as an artifact: borders, backgrounds, rules, and the header and footer of each page, as pagination artifacts.
  • Tags of the browser's own. Structure types the standard does not know are mapped to the ones it does.
  • Links. Each link gets a description, its text as a screen reader would say it, and the links of headers and footers get their place in the structure.
  • Lists. A list item's content gets the body element the standard asks for.
  • Figures. A <figure> that only groups content is no longer a figure that would need an alternative text of its own.
  • Table headers that repeat. When a table runs over several pages, the browser repeats its header row. The copies are marked as pagination artifacts, so the header is read once.
  • Stamps. The mark on test renders and a text watermark are written in an embedded font and marked as artifacts.
  • Language. A document without a lang attribute gets the locale your settings state, so the file names its language.

The repairs of the structure run on every tagged PDF, also one that sets only pdf.tagged or pdf.outline. Only pdf.ua adds the language, the check and the PDF/UA identifier.

The report​

A render that set pdf.ua carries accessibility:

"accessibility": {
"standard": "PDF/UA-1",
"check": "strict",
"conformant": true,
"validator": "veraPDF 1.30.2",
"rules": { "passed": 106, "failed": 0 },
"failures": [],
"warnings": [
{ "code": "running-text", "count": 1, "elements": ["DE36 0000 0000 0000 0000 00"] }
]
}
FieldMeaning
standardPDF/UA-1.
checkstrict or report, as the render took it.
conformantThe file passed every rule the validator checks.
validatorThe validator and its version. Its rule set moves between releases, so the report says which one judged.
rulesHow many rules passed and failed.
failuresThe rules that failed, at most 20: rule (clause and test number), count, message, and elements, the start tags concerned, at most five. A code from the table above where the template check knows the cause.
warningsWhat the template check found that no rule fails for: code, count, elements.
truncatedPresent and true when more rules failed than failures holds.

accessibility is null on a render that did not set pdf.ua. A strict render that failed keeps it, and its problem carries the same block.

Where you see it:

  • In the API: the answer to the render, GET /v1/renders/{id}, the render list and the render.completed and render.failed webhook events.
  • With ?output=binary the answer is the file, and the header X-Formfeed-PDF-UA says conformant or not-conformant.
  • In the app, a render's sheet in the render log has an Accessibility section with the verdict and the failed rules. Report (JSON) downloads the report. A True render in the editor shows the same section below its result.
  • On the command line, formfeed render prints it.

Keep the report with a document you have to answer for. It records which rules the file was held to and that it passed, on the day it was made.

With an e-invoice​

pdf.ua and an e-invoice go together. An e-invoice is a PDF/A-3b that carries the invoice as XML; with pdf.ua the same file is also a PDF/UA-1 document. Declare both in the template's settings, or one in the template and the other in the request:

settings.json
{
"pdf": {
"ua": true,
"metadata": { "title": "Invoice {{ _invoice.number }}" }
},
"einvoice": { "profile": "en16931", "flavour": "zugferd" }
}
  • One file, checked as both. The page is rendered, tagged and repaired as for any PDF/UA-1 file. The conversion to PDF/A-3 keeps the tags, and the finished e-invoice, which is the file you receive, is held to both standards. The verdict on PDF/UA-1 is about the file that is delivered, not about the PDF before it was converted.
  • Two reports. The render carries einvoice and accessibility. The first says what the rules of EN 16931 and of PDF/A-3b found, the second what the rules of PDF/UA-1 found. When the file passed both, its metadata names both standards.
  • Strict. An e-invoice whose file fails PDF/UA-1 answers 422 pdfua_validation_failed and delivers nothing. The problem carries both reports: accessibility with the rules that failed, and einvoice, which says that the invoice itself was in order.
  • Report. With { "check": "report" } the valid e-invoice is delivered, and it does not name itself PDF/UA-1.
  • What comes first. A block that breaks a rule of EN 16931, or a file that cannot be made a PDF/A-3b, fails the render with the e-invoice's own error, and then there is no file to hold to PDF/UA-1. A strict display check that fails carries accessibility too.
  • Plans and units. The e-invoice costs what it always costs: one unit on top of the render, live from the Starter plan on (Plans and units). pdf.ua adds nothing.

The site's e-invoice example declares both.

Limits​

  • No merge. pdf.ua with post.merge_after answers 400 invalid_request: the tags of merged documents do not survive the merge, and the validator would pass the file all the same.
  • Not for Word and PowerPoint templates. Their PDF comes from another converter, whose tags Formfeed has not measured. A PDF render of such a template with pdf.ua answers 400 invalid_request, and formfeed validate reports the declaration as an error.
  • Images. A template that declares pdf.ua renders images as ever. Asking for it in a request for an image answers 400 invalid_request.
  • PDF tools. Merging, watermarking and protecting an existing file does not check it again, and a merge drops the tags of the files it joins.
  • Watermark text. A text watermark is set in an embedded font, as PDF/UA requires, which covers Latin, Greek and Cyrillic letters. A text with other characters is refused with invalid_request; use an image watermark for other scripts.
  • A password goes last. post.password encrypts the file after the check. With an e-invoice, a password is refused, because PDF/A forbids encryption.

Plans, units and cost​

pdf.ua is part of every plan and costs no extra unit. Test renders and the editor's true renders are checked like every other. A failed render costs nothing. A deduplicated render answers with the report of the file it reuses.

Errors​

StatusCodeWhen
422pdfua_validation_failedStrict, and the file failed. accessibility names the rules and their elements.
503conformance_unavailableThe validator is busy or not reachable. Retry after Retry-After.
504render_timeoutThe validator did not answer within 20 seconds on top of the render. Retry.
400invalid_requestpdf.ua with a merge, an image output, or a Word or PowerPoint template, or a value that is not one of those above.

On the command line​

formfeed validate notice

reports, for a template whose settings.json declares pdf.ua, what the source already shows: a missing title, an image without alt, and a declaration the API refuses. With the strict check the title and the image are errors and the command exits with 1; with report they are warnings. A declaration the API refuses (a value it does not take, or pdf.ua on a Word or PowerPoint template) is always an error.

formfeed render notice

prints the verdict of the render (here with a test key, which costs no unit):

PDF/UA-1 conformant, 106 rule(s) passed (veraPDF 1.30.2)
warn Only in the header or footer: DE36 0000 0000 0000 0000 00
rnd_7da09ab6cd9946b798c187c2a9350852: 1 page(s), 0 unit(s) -> notice.pdf

A strict render that fails prints the rules with their elements and exits with 1. formfeed dev checks the page it previews and lists the findings beside it.