PDFa11y

Review PDFs for accessibility on upload

Prevention, not just remediation

About Josh Mitchell

  • Founder of M6L
  • Full stack consultancy
  • Around 20 years Drupal experience
  • Maintainer of PDFa11y
  • Musician and outdoor enthusiast
  • Connect: LinkedIn or Drupal Slack (@joshuami)
Portrait of Josh Mitchell pointing, founder of M6L
The problem

PDFs are the accessibility blind spot

Thousands of documents, zero visibility, and a deadline

Why now

  • April 2024: DOJ finalized the ADA Title II web and app rule
  • State and local governments, and public universities, were put on notice
  • Deadlines have slipped, but the obligation has not
  • HTML pages get audited. PDF libraries mostly do not.

The remediation market

Great at

  • Fixing a large existing library
  • Editor-in-the-loop correction

Not built for

  • Preventing the next bad upload
  • Teaching editors why it failed
  • Your budget: recurring cost and vendor lock-in

Prevention over remediation

Scan the structure of a PDF at upload, tell the editor what is wrong, and let them fix the source before it ever publishes.

Heavily inspired by Editoria11y: immediate feedback, right where editors work.

Built with a client who wanted prevention and education, not another subscription.

Open source and free by design

  • A structural scan of a PDF does not belong behind a paywall
  • Published on Drupal.org: drupal.org/project/pdfa11y
  • Runs on your Drupal host, no third-party service
  • Parser: smalot/pdfparser (LGPL 3.0) as a Composer dependency
City seal of Mundane, Oregon

City of Mundane, Oregon

Numquam Notabilis, est. 1883. A demo site on Drupal CMS.
  1. Document library: every PDF wears a Pass or Fail
  2. A council meeting: agenda and minutes embedded with status
  3. Open a failing document's Accessibility tab
  1. Upload a new PDF through the media library and watch the warning
  2. PDF accessibility report for the whole library
  3. Terminal: drush pa:check --bundle=document
Open the demo site
Under the hood

How it works

Read the structure, look for known bad patterns, report back in seconds

Upload to result

UploadMedia form or Drush
Parsesmalot/pdfparser reads headers, metadata, structure tree
ChecksEach enabled plugin inspects the parsed document
ResultPass Fail Skipped
SurfaceEditor message, badge, Accessibility tab, report

Typical scan time is seconds, depending on file size. Results are stored per file and per media item.

Six checks today

Document language

document_language

Screen readers use the language to pick pronunciation rules.

Tagged PDF

tagged_pdf

Tags carry reading order, headings, lists. No tags, no navigation.

Document title

document_title

Announced when the file opens; shown in tabs and bookmarks.

Heading structure

heading_structure

Structure tree exists, starts at H1, and never skips a level.

PDF version

pdf_version

Configurable minimum (default 1.4). Older versions cannot be tagged.

Title is not a filename

document_title_filename

"Report.docx" is a leftover, not a title. Can be disabled for forms.

Extensible by design

Each check is a plugin. Add your own with an attribute and one method.


#[AccessibilityCheck(
  id: 'figure_alt_text',
  label: new TranslatableMarkup('Figures have alt text'),
)]
final class FigureAltTextCheck extends AccessibilityCheckBase {
  public function check(Document $document): CheckResult {
    // Walk the structure tree for /Figure nodes without /Alt.
  }
}
          

Views field and filter plugins ship with the module, so any report is just another view.

What it does not catch

A floor, not a ceiling

What a mechanical scan can and cannot tell you

PDFs are not web pages

  • Editoria11y can point at the broken heading on the page
  • A PDF only renders inside a viewer. There is no reliable way to overlay a widget on every viewer, and many people do not use a browser at all.
  • So PDFa11y reports next to the file, not on top of it

A PDF can contain links, and you can link to a PDF. It is still not HTML and should never be treated as a page.

Needs a human

  • Alt text quality
  • Whether the right text is tagged as a heading
  • Reading order on visually complex pages
  • Whether form fields are labeled meaningfully

Pair the module with editor training. The checks catch what can be caught mechanically.

Lessons from production

When we skip a file

Some PDFs cannot or should not be parsed. Say so, and move on.

Skipped, with a reason Skipped

  • Missing file. The media record points at nothing. Usually a leftover from a delete or migration.
  • Cannot be read right now. Remote storage such as S3 timed out. Temporary; retry later.
  • Encrypted or restricted. Fix: re-export with no security and re-upload.
  • Corrupt or malformed. Broken export, truncated upload, or not really a PDF.

Protecting the server

  • Too large. Over the configured size limit, so we never start.
  • Packed with images. Scanned plats and drawings can be small on disk and hundreds of MB decompressed. We estimate up front and skip what would exhaust memory.
  • Too slow or too hungry. Time and memory are capped per file, so one bad file never derails a bulk scan.

Rolling this out on real libraries is what produced this list.

Operations

Reporting and backfills

Existing libraries grow before the module is installed

Where results show up

  • Per media item: an Accessibility tab with every check and message
  • Site-wide: /admin/reports/pdf-accessibility with pass, fail, and error counts
  • Anywhere else: Views field and filter plugins for your own listings
  • Editors: a status badge on embedded documents, if the theme prints it

Two safe ways to scan a library

Queue mode for unattended backfills. Each file runs in its own worker, memory is reclaimed between items.


drush pa:check --bundle=document --missing-only --queue
drush queue:run pdfa11y_check
          

Chunked sync runs when you are watching and SSH sessions have a time cap.


drush pa:check --bundle=document --missing-only --limit=1500 --format=summary
          

Configuration

  • Editor instructions: link your own remediation guide
  • Warn or block: block failing uploads, with a bypass permission
  • Minimum PDF version and which checks run
  • Permissions: administer, run checks, view reports, bypass

/admin/config/media/pdf-accessibility

Roadmap

  • Editoria11y integration. A custom check that surfaces PDF fails and errors on embedded media, right in the editor.
  • Missing alt text. PDF figures have an alt concept too.
  • More settings. Size and memory limits in the UI, not just config overrides.
  • Many Editoria11y HTML checks have a PDF structure equivalent waiting to be written.

Get involved

  • Try it: drupal.org/project/pdfa11y
  • Feature requests and bugs: the issue queue
  • Need an accessibility audit, an implementation, or want to fund a check? Let's talk.

Thank you

Questions?

Josh Mitchell on LinkedIn
Drupal Slack: @joshuami

Slides: joshuami.com/presentations/pdfa11y