PDF Accessibility skill for Codex

Tagged PDFs that pass PDF/UA and real readers. Codex makes PDFs accessible at the source instead of patching exports: it probes tagging, language, title, fonts and figure alt text with pikepdf, picks the right export settings for Word, LibreOffice, Typst, Chromium or WeasyPrint, validates against the PDF/UA profile in veraPDF, and records the manual checks validators cannot do, such as reading order and alt text quality.

  • Documents & Office
  • $pdf-accessibility
  • 181 lines
  • 2 files
  • ~2,150 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- pdf-accessibility

Installs to ~/.agents/skills/pdf-accessibility/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $pdf-accessibility, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Produces and audits accessible, tagged PDFs that meet PDF/UA-1 (ISO 14289-1) and WCAG 2.2, covering structure tags, reading order, alt text, table headers, language, title, bookmarks and embedded fonts, generated from Word, LibreOffice, Typst, WeasyPrint or Chromium and checked with veraPDF, PAC and a pikepdf probe. Use when asked to make a PDF accessible or screen-reader friendly, meet Section 508, EN 301 549 or the European Accessibility Act, fix PDF/UA validation failures, or decide whether to remediate or regenerate an untagged PDF.

Use it when

  • A PDF must meet PDF/UA, Section 508, EN 301 549 or the European Accessibility Act
  • veraPDF, PAC or Acrobat reports accessibility failures
  • Generated PDFs (HTML, Typst, Word exports) need proper tags and alt text
  • You must decide between remediating an untagged PDF and regenerating it

Use something else when

  • The PDF is a scan with no text at all: use ocr-documents first
  • You only need to render HTML to PDF without an accessibility target: use html-to-pdf

How to check the result

veraPDF with the ua1 flavour reports no failed rules, the pikepdf probe shows tags, language, title display and embedded fonts, and a manual pass confirms reading order and alt text quality.

The mistake it prevents

Running the file through Ghostscript compression or print-to-PDF after tagging, which strips the structure tree and silently undoes the work.

~/.agents/skills/pdf-accessibility/SKILL.md

--- name: pdf-accessibility description: Produces and audits accessible, tagged PDFs that meet PDF/UA-1 (ISO 14289-1) and WCAG 2.2, covering structure tags, reading order, alt text, table… license: MIT ---

An accessible PDF is done when veraPDF’s PDF/UA profile reports no failures, PAC (or Acrobat’s checker) agrees, and a manual pass confirms what machines cannot: alt text says something useful, the reading order matches the visual order, and headings and tables make sense to a screen-reader user.

The rule that saves the most time

Fix accessibility at the source and regenerate. Remediating an untagged PDF by hand (or by script) is slow, fragile and lost on the next export. Only remediate when the source is gone, and say so in the report.

Requirements checklist (PDF/UA-1 plus WCAG essentials)

RequirementWhere it lives in the file
Tagged, with all real content inside the structure tree/MarkInfo << /Marked true >>, /StructTreeRoot
Decorative content marked as artifacts (headers, footers, page numbers, rules)Artifact marked content in page streams
Logical heading hierarchy, no skipped levelsH1..H6 (or H in PDF 2.0) structure elements
Alt text on every informative figure, none on decorative ones (artifact instead)/Alt on Figure
Tables with header cells and scopeTable / TR / TH with /Scope / TD
Lists as listsL / LI / Lbl / LBody
Document languageCatalog /Lang (and on spans in another language)
Title shown in the window, from metadataXMP dc:title plus /ViewerPreferences << /DisplayDocTitle true >>
PDF/UA identifierXMP pdfuaid:part = 1
Fonts embedded with Unicode mappingEvery font embedded, ToUnicode present
Bookmarks for documents longer than a few pages/Outlines
Links and form fields labelledLink elements with /Contents, field /TU tooltips
Colour not the only cue, contrast at least 4.5:1 for body textDesign, checked visually
Tab order follows structurePage /Tabs /S

Workflow

- [ ] 1. Probe the existing PDF (or the generator's output)
- [ ] 2. Decide: regenerate from source, or remediate
- [ ] 3. Fix the source and export with tagging/PDF-UA options
- [ ] 4. Validate: veraPDF PDF/UA, then PAC or Acrobat
- [ ] 5. Manual checks a validator cannot do
- [ ] 6. Report with residual issues

1. Probe

Quick structural probe with pikepdf (pip install pikepdf, needs network):

import sys
from collections import Counter
import pikepdf

pdf = pikepdf.open(sys.argv[1])
root = pdf.Root
report = {
    "tagged": "/StructTreeRoot" in root and bool(root.get("/MarkInfo", {}).get("/Marked", False)),
    "lang": str(root.get("/Lang", "")) or None,
    "display_doc_title": bool(root.get("/ViewerPreferences", {}).get("/DisplayDocTitle", False)),
    "pdfua_id": b"pdfuaid:part" in (root.Metadata.read_bytes() if "/Metadata" in root else b""),
    "outlines": "/Outlines" in root,
}
with pdf.open_metadata() as meta:
    report["xmp_title"] = meta.get("dc:title")

roles, missing_alt = Counter(), 0
def walk(node, depth=0):
    global missing_alt
    if depth > 200:
        return
    if isinstance(node, pikepdf.Array):
        for k in node:
            walk(k, depth + 1)
    elif isinstance(node, pikepdf.Dictionary):
        s = node.get("/S")
        if s is not None:
            roles[str(s)] += 1
            if str(s) == "/Figure" and "/Alt" not in node and "/ActualText" not in node:
                missing_alt += 1
        if "/K" in node:
            walk(node["/K"], depth + 1)

if "/StructTreeRoot" in root:
    walk(root.StructTreeRoot.get("/K"))
print(report, dict(roles.most_common(15)), "figures without alt:", missing_alt)

Read the result:

  • tagged: False means a screen reader guesses the order from the content stream. Regenerate if at all possible.
  • Roles only P and Span, no H1..H6: the source used manual formatting instead of heading styles.
  • Custom role names (e.g. /Heading1) are fine only if /RoleMap maps them to standard types.
  • fonts: also run pdffonts file.pdf (poppler); every row needs emb yes and uni yes.

2. Decide

SituationAction
Source available (DOCX, ODT, HTML, Typst, LaTeX)Fix source, regenerate (step 3)
Scanned PDFOCR first (ocrmypdf), then remediate; a scan has no structure
Source lost, short documentRemediate in Acrobat Pro or similar by hand; scripts cannot infer reading order reliably
Source lost, long documentRebuild the source (convert to DOCX/HTML, fix, re-export); usually faster than remediation
Fillable formFix field tooltips (/TU), tab order, and tag form fields (Form elements)

3. Generate tagged PDFs

SourceExport
Word (Microsoft 365)Save As PDF, Options: “Document structure tags for accessibility” on, “Create bookmarks using headings”; run Check Accessibility first
LibreOfficesoffice --headless --convert-to 'pdf:writer_pdf_Export:{"UseTaggedPDF":{"type":"boolean","value":"true"},"PDFUACompliance":{"type":"boolean","value":"true"}}' doc.odt (LibreOffice 7.4+ JSON filter options)
Typst 0.14+Tagged by default; typst compile --pdf-standard ua-1 doc.typ adds PDF/UA checks and fails on violations; set alt: on images and set document(title: ...)
HTML via Chromium (Playwright)page.pdf({ tagged: true, outline: true, ... }); semantic HTML drives the tags
HTML via WeasyPrintweasyprint --pdf-variant pdf/ua-1 in.html out.pdf (check weasyprint --help for variants in the installed version)
LaTeXKernel tagging via \DocumentMetadata{lang=en, pdfversion=2.0, pdfstandard=ua-2, testphase=latest} (recent TeX Live, LuaLaTeX preferred); still maturing, so verify with veraPDF before claiming conformance

Source fixes that matter more than export flags:

  • Use real heading styles in order (one H1 or title, then H2, H3 without gaps).
  • Write alt text that states the point of the image (“Revenue doubled from 2023 to 2025”), not its appearance (“bar chart”). Decorative images: mark as decorative.
  • Mark table header rows (Word: “Repeat as header row”; HTML: <th scope="col">). Avoid merged cells and nested tables; split complex tables.
  • Set the document title in properties/metadata, and the document language.
  • In HTML, use <h1>..<h6>, <ul>/<ol>, <table> with <th>, <figure> with <img alt>, lang on <html>. Avoid div-soup; tags follow the DOM.
  • Text in images is invisible to screen readers; use real text.

4. Validate

verapdf --flavour ua1 --format text doc.pdf       # veraPDF CLI, needs Java; install needs network
verapdf --flavour ua1 --format html doc.pdf > verapdf-report.html

veraPDF checks the machine-checkable PDF/UA rules (Matterhorn Protocol failure conditions). Then run PAC (free, Windows) or Acrobat Pro’s “Full Check” if the user has them; ask the user to run them when you cannot.

5. Manual checks

Machines cannot judge these; do them and record the result:

  • Read the tag tree order (Acrobat Tags panel, PAC’s logical structure view, or the probe’s role walk) against the visual order on 3 representative pages.
  • Read every alt text: meaningful, concise, no “image of”.
  • Check that headers, footers, page numbers and watermarks are artifacts, not content.
  • Check table header association on the most complex table.
  • Zoom to 400% (reflow) and check contrast on coloured backgrounds.
  • If available, listen to two pages with NVDA, JAWS or VoiceOver.

Gotchas

  • A PDF can be “tagged” and still useless: auto-tagging often marks everything as P and reads multi-column pages across columns.
  • Post-processing tools (Ghostscript compression, some merge tools, printing to PDF) strip tags. Merge with qpdf or pypdf and recheck; never “print to PDF”.
  • PDF/A and PDF/UA are different; PDF/A-1a/2a requires tagging, but PDF/A-2b does not. Claim only what you validated.
  • Adding pdfuaid:part to metadata does not make a file conformant; it only claims it.
  • Mathematical formulas need Formula tags with alt text (or MathML in PDF 2.0 workflows).
  • Links need meaningful text in context; “click here” passes validators and fails users.

Output

## PDF accessibility: annual-report-2026.pdf
- Route: regenerated from annual-report.docx (Word heading styles fixed, 12 alt texts added)
- Probe: tagged yes, lang en-GB, DisplayDocTitle yes, pdfuaid 1, bookmarks yes, fonts embedded
- veraPDF ua1: 0 failed rules (was 143); PAC: passed (run by user)
- Manual: reading order ok on p.3/7/22; 2 decorative images artifacted; table 4 split into two
- Residual: chart on p.18 has a summary alt text; full data table added in appendix

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "PDF Accessibility"
  short_description: "Tagged PDFs that pass PDF/UA and real readers"
  default_prompt: "$pdf-accessibility Make our annual report PDF pass PDF/UA; the source Word file is in docs/"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc