CtrlK
BlogDocsLog inGet started
Tessl Logo
PluginPublic

testland/pdf-accessibility-checker

Test PDF accessibility (PDF/UA conformance) - tagged-PDF structure (StructTreeRoot), alternative text on images (Alt), reading-order, language metadata (Lang), document title, heading hierarchy. Use veraPDF / PAC (PDF Accessibility Checker) / pdfix / Adobe Acrobat Pro headless; map each finding back to WCAG 2.1 PDF Techniques (PDF1 - PDF23). Use when a product ships customer-facing PDFs into a context that mandates PDF/UA - US Section 508, EU Directive 2016/2102, or a public-sector tender - and each file must be proven tagged before release.

Quality

89%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

SKILL.md

name:
pdf-accessibility-checker
description:
Test PDF accessibility (PDF/UA conformance) - tagged-PDF structure (StructTreeRoot), alternative text on images (Alt), reading-order, language metadata (Lang), document title, heading hierarchy. Use veraPDF / PAC (PDF Accessibility Checker) / pdfix / Adobe Acrobat Pro headless; map each finding back to WCAG 2.1 PDF Techniques (PDF1 - PDF23). Use when a product ships customer-facing PDFs into a context that mandates PDF/UA - US Section 508, EU Directive 2016/2102, or a public-sector tender - and each file must be proven tagged before release.
metadata:
{"keywords":"pdf-accessibility, pdf-ua, tagged-pdf, wcag, verapdf","version":"1.1.8"}

pdf-accessibility-checker

Per the WCAG 2.1 spec, "Text alternatives" + "Structured information that can be programmatically determined" + "Language identification"

  • "Proper document organization" apply to PDFs. The PDF/UA standard (ISO 14289) operationalizes these into concrete file-level requirements: Tagged PDF, role mapping, Alt text, reading order, Lang metadata.

When to use

  • Producing PDFs in regulated industries (US Section 508, EU Directive 2016/2102, accessible-by-default policies).
  • Public-sector tenders that require PDF/UA compliance.
  • Pre-deployment gate: every customer-facing PDF passes the accessibility check before release.

Step 1 - Pick a checker

ToolStrength
veraPDF (open source)PDF/UA-1 + PDF/A validation; CI-friendly CLI
PAC (PDF Accessibility Checker) by axes4UI-first; comprehensive Matterhorn Protocol coverage
pdfixCommercial; auto-tagging fixes
Adobe Acrobat Pro Accessibility CheckIndustry default; manual-review-friendly

For CI, veraPDF is the default open-source choice.

Step 2 - Install veraPDF

# Download installer
curl -L -o verapdf.zip https://software.verapdf.org/releases/1.30/verapdf-greenfield-1.30.2-installer.zip
unzip verapdf.zip

# The zip ships an IzPack installer, not a ready-to-run binary
verapdf-greenfield-1.30.2/verapdf-install ./auto-install.xml
<install-dir>/bin/verapdf --version

Current release index (1.30.2 as of this writing): software.verapdf.org/releases. The headless verapdf-install ./auto-install.xml form and the bin/ layout are per docs.verapdf.org/install.

Step 3 - Run conformance check

verapdf --flavour ua1 --format json out.pdf > vera-report.json

Exit codes: 0 = conformant, 1 = non-conformant, 2 = parse error.

Step 4 - Parse and assert

import json, subprocess

def test_pdf_passes_pdf_ua_1():
    result = subprocess.run(
        ["verapdf", "--flavour", "ua1", "--format", "json", "out.pdf"],
        capture_output=True, text=True,
    )
    report = json.loads(result.stdout)

    # Iterate jobs[0].validationResult
    job = report["report"]["jobs"][0]
    failed = job["validationResult"]["details"]["failedRules"]

    assert failed == 0, f"PDF/UA validation failed: {job['validationResult']['details']}"

Step 5 - Verify tagged PDF (StructTreeRoot)

A PDF is "tagged" if its catalog has a StructTreeRoot and MarkInfo /Marked true. Without these, screen readers cannot read the document semantically.

import pikepdf

def test_pdf_is_tagged():
    pdf = pikepdf.open("out.pdf")
    catalog = pdf.Root
    assert "/StructTreeRoot" in catalog, "PDF lacks StructTreeRoot (untagged)"
    mark_info = catalog.get("/MarkInfo", {})
    # Use bool(), not `is True`: pikepdf can return a pikepdf.Object wrapper
    # rather than the singleton Python True, so `is True` is always False
    # even when /Marked is set (per [pikepdf objects]).
    assert bool(mark_info.get("/Marked", False)), "PDF MarkInfo /Marked is false"

Step 6 - Verify document title + language metadata

Per the WCAG 2.1 spec, language identification + descriptive title are required.

def test_pdf_has_title_and_lang():
    pdf = pikepdf.open("out.pdf")
    info = pdf.docinfo

    title = info.get("/Title")
    assert title and str(title).strip(), "PDF has no /Info /Title"

    lang = pdf.Root.get("/Lang")
    assert lang and str(lang) in ("en", "en-US", "en-GB", "de", "fr"), f"PDF /Lang invalid: {lang}"

Step 7 - Verify image Alt text presence

def test_all_images_have_alt():
    pdf = pikepdf.open("out.pdf")
    structure_root = pdf.Root["/StructTreeRoot"]

    # The structure tree is NESTED: each element's /K can hold child
    # elements to arbitrary depth, so /Figure tags live below /Document,
    # /Sect, /P, etc. A top-level-only scan misses nested figures and
    # passes vacuously. Walk every /K recursively. Each /Figure must carry
    # an /Alt (or /ActualText) per [WCAG PDF1].
    untagged_images = []

    def walk(node):
        if isinstance(node, pikepdf.Array):
            for child in node:
                walk(child)
            return
        if not isinstance(node, pikepdf.Dictionary):
            return  # marked-content id (int) or other leaf
        if node.get("/S") == "/Figure":
            alt = node.get("/Alt")
            if not alt or not str(alt).strip():
                untagged_images.append(node)
        kids = node.get("/K")
        if kids is not None:
            walk(kids)

    walk(structure_root.get("/K"))
    assert untagged_images == [], f"Images without /Alt: {len(untagged_images)}"

Step 8 - Reading order verification

Reading order determined by structure-tree DFS. Verify the generated structure puts content in the order a human reader would expect:

def test_reading_order_matches_visual_order():
    # TEMPLATE: the title and section labels below are placeholders for an
    # invoice layout. Replace them with the expected reading-order landmarks
    # for your own document before using this test.
    text_per_page = extract_text_with_structure_order("out.pdf")

    # First page should start with the document's leading landmark.
    assert text_per_page[0].startswith("Invoice #")  # replace per document
    # Each expected landmark appears on its page, in reading order.
    expected = ["Bill To", "Items", "Total", "Footer"]  # replace per document
    for i, section in enumerate(expected):
        assert section in text_per_page[i] or i == 0

This is best done via PAC or visual inspection for high-stakes documents - automating reading-order verification is hard.

Step 9 - Map findings to WCAG 2.1 Techniques

Per the WCAG 2.1 spec, PDF Techniques document specific patterns:

TechniqueWhat it covers
PDF1Applying alt text to images
PDF2Creating bookmarks
PDF3Ensuring correct tab order
PDF4Hiding decorative images via Artifact tagging
PDF6Using table elements (<Table> / <TR> / <TH> / <TD>)
PDF9Heading structure (H1 / H2 / H3)
PDF15Form field accessibility
PDF18Document title in DocInfo
PDF19Lang in document or per-element

When a check fails, cite the technique in the failure message:

assert title, "PDF18: Document title required (WCAG 2.1)"

Step 10 - CI gate

- name: PDF/UA conformance
  run: |
    for pdf in out/*.pdf; do
      # jq -e sets the exit status from the result: false/null gives exit 1,
      # so the gate actually fails on violations. Plain jq exits 0 regardless
      # of the printed boolean (per [jq manual]), so `|| exit 1` never fired.
      verapdf --flavour ua1 --format json "$pdf" \
        | jq -e '.report.jobs[0].validationResult.details.failedRules == 0' > /dev/null || exit 1
    done

Anti-patterns

Anti-patternWhy it failsFix
Generate PDF with printBackground: true and skip tag checkVisually fine; screen-reader-brokenveraPDF flavour ua1 (Step 3)
Convert via tools that strip tags (some HTML→PDF engines)Pre-tag stripped during convertUse tagged-PDF-capable engines (WeasyPrint with tagged-pdf option)
Skip /LangScreen readers use wrong pronunciationStep 6
Auto-generate Alt = filename"logo.png" doesn't help screen readerManual Alt review for high-stakes
Only run in pre-prod, not CIInaccessible PDFs shipBlock PR (Step 10)

Limitations

  • veraPDF/UA1 catches structural issues; semantic correctness (helpful Alt text vs filename Alt) needs human review.
  • PAC is more complete than veraPDF for the Matterhorn Protocol but GUI-only.
  • WeasyPrint produces tagged PDFs; older versions of wkhtmltopdf do not. Check engine output capabilities.
  • WCAG 2.1 PDF Techniques are non-normative; consult the WCAG 2.1 spec + the per-technique test pages for current guidance.

References

  • WCAG 2.1 spec - accessibility principles applicable to PDFs
  • veraPDF (PDF/UA-1 + PDF/A validator) - verapdf.org
  • PAC by axes4 - pac.pdf-accessibility.org (Matterhorn Protocol)
  • ISO 14289-1 (PDF/UA-1) - cite by stable ID; consult ISO for spec text
  • pikepdf Python library - pikepdf.readthedocs.io
  • pikepdf objects - how pikepdf maps PDF scalars to Python types
  • WCAG PDF1 - applying /Alt text to /Figure structure elements
  • jq manual - the -e / --exit-status flag for CI gating
  • pdf-snapshot-tester - sister skill for visual-regression on the same PDFs

SKILL.md

tile.json