pdf-accessibility-checker

Use when customer-facing PDFs must meet PDF/UA (Section 508, EU accessibility rules, public tenders) and each file must be proven tagged before release. Checks structure tags, alt text, reading order, language and title with veraPDF, PAC or similar, mapped to WCAG PDF techniques.

/plugin install qa-pdf-print@testland-qa

First time in Claude Code? Add the marketplace with /plugin marketplace add testland/qa

View source (opens in a new tab)
Skill
Version
v1.2.3

Per the WCAG 2.1 spec (opens in new window), "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 (opens in new window). The headless verapdf-install ./auto-install.xml form and the bin/ layout are per docs.verapdf.org/install (opens in new window).

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 (opens in new window), 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 (opens in new window), 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 (opens in new window) + the per-technique test pages for current guidance.

References