Use accessible HTML to tag and update a PDF.
Iris turns page images into accessible HTML. This tool takes that HTML and the original PDF, and gives back the same PDF, tagged: a structure tree a screen reader can follow, with form fields filled in if you give it values. The page looks exactly as it did.
It runs offline. It makes no network or model calls.
Node 24 or later. Tesseract 5 is optional; it is needed only for scanned pages.
git clone https://github.com/EqualifyEverything/equalify-iris-pdf && cd equalify-iris-pdf
npm ci
npm test
npm link # puts iris-pdf on your PATHThe sources are TypeScript, run directly by Node. Node does not do that inside node_modules, so use a clone (as above), not npm install from another project.
iris-pdf tag --pdf in.pdf --pages pages.json --out out.pdf --report report.json
iris-pdf fields --pdf in.pdf [--json]
iris-pdf check --pdf out.pdf # runs veraPDF's PDF/UA-1 check, if installedtag options:
| Option | Meaning |
|---|---|
--values values.json |
Fill form fields (below). |
--lang, --title |
Used when pages.json has none. A language is required. |
--ocr auto|off|required |
Use Tesseract for pages with no text layer. Default auto. |
--verify pixels,text|off |
The checks below. On by default. --verify-dpi sets the render resolution (36–600, default 150). |
--flatten |
Draw the field values into the page and remove the fields. |
--password |
Open an encrypted PDF. The output keeps its encryption. |
--allow-signed |
Tag a signed PDF. This breaks the signature, and the report says so. |
--partial |
Leave a page untagged, instead of failing, when it has no way to place text. |
--strict |
Fail on any warning that means content went untagged or unmatched, or that the file was repaired. |
Iris's HTML, one entry per source page:
{ "lang": "en", "title": "Parking permit",
"pages": [{ "sourcePage": 1, "html": "<h1>Parking Permit</h1><p>…</p>" }] }Field name to value. Names are the ones iris-pdf fields prints.
{ "applicant.name": "Ada Lovelace", "applicant.consent": true, "contact": "phone", "state": "WI" }Text fields take strings, checkboxes true/false, radio groups and lists one of their options. A wrong type, an unknown option or a value over the field's length limit stops the run before anything is written. Read-only fields are skipped and counted. Unchecking always writes /Off, whatever the source used for "off".
- The page's original drawing is kept byte for byte and marked as an artifact.
- Iris's words are matched to the words on the page (from the text layer, or from Tesseract on a scan).
- An invisible text layer is added with Iris's words at those positions, tagged with the structure from the HTML: headings, lists, tables with their headers, links, figures with alt text, form fields.
- The file is saved incrementally: the original bytes are the start of the output. A damaged file is instead rewritten from mupdf's repair of it, with warning
repaired.
Then two checks run, and if either fails nothing is written (exit 2):
- Pixels. Every page renders the same as before, except inside fields whose values changed.
- Text. Every word the source had is still there, and every character of the added layer can be read back.
--report writes JSON: per page, where the text came from and how many words matched; the structure written; fields set and skipped; the check results; and every warning. Warnings name what could not be done, for example unmatched_text (page text missing from the HTML, kept as a paragraph), missing_alt, field_not_in_html, unmatched_link, duplicate_text_layer, page_not_in_html and page_not_tagged (the page is left as it was; a blank page needs no HTML and is not warned), no_title, font_not_embedded (a source font has no embedded program, which PDF/UA-1 requires; the source drawing is not changed), source_marked_content (the page drawing has marked-content ids left from an earlier tag tree), alignment_incomplete (the page and the HTML differ too much to match every word in time; the rest is kept as unmatched text).
The output declares PDF/UA-1 only when it has a title, every page is tagged, every source font is embedded, and no page drawing has leftover marked content. The tests check each such claim with veraPDF.
| Exit | When |
|---|---|
| 0 | Done. |
| 1 | Refused: encrypted (no or wrong password), permissions_denied, too_many_pages (over 25), too_many_words (over 4000 on a page), already_tagged, xfa (dynamic form), signed, no_acroform_field, no_text_positions, strict. |
| 2 | A check failed: pixels_changed, text_lost. |
| 3 | Bad input: unreadable, bad_pages, no_document_language, bad_value, field_not_settable, bad_arguments. |
Errors print one line: iris-pdf: <code>: <message>.
Form values are personal data. They are never printed, logged, or put in the report or an error message; only field names are.
- The text exists twice on a page that already had a text layer: the original, now an artifact, and ours. Screen readers use ours. Plain copy-and-paste tools may show the text doubled. The report warns
duplicate_text_layer. - A table that continues onto the next page is tagged as two tables.
checkneeds veraPDF installed.- A form with no fields (a flat form) cannot be filled.