
Posted on
When the Department of Justice announced their accessibility ruling in April of 2024, it put many U.S. government organizations on notice that accessibility could no longer be an afterthought. I work with a number of government and higher education organizations that needed to be able to respond to this ruling. While that ruling was delayed, the need to improve accessibility is still present. We've continued to refine our accessibility audits and reporting tools for more than just PDFs, but PDFs are a huge area of risk that many organizations have no visibility into without manual scans and individual efforts.
While their are a lot of PDF-remediation solutions on the market, most are focused on correcting documents with an editor in the loop. This is great if you are remediating a large media library that already exists, but it doesn't address ongoing prevention and education. (These libraries also have a continued cost and vendor lock-in.)
One of my clients worked with me on an approach that would prevent inaccessible PDFs on upload by scanning the XML structure of the PDF and providing feedback to editors on what can be addressed. This resulted in PDFa11y, a module published to Drupal.org, free for anyone to try. I took heaving inspiration from a module called Editoria11y which gives immediate editor feedback when editing content.
Open source and free by design
Maybe this is a mistake, but at a time when so many companies are trying to create a product (preferably with recurring revenue), I just didn't see how a simple of scan of a PDF structure belonged behind any sort of a paywall. With that in mind, when I was starting this module, I searched for an open source, PDF-parsing library that would allow me to run the basic scan right on the Drupal host. In the end, I found smalot/phpparser, which is licensed under the GNU Lesser General Public License v3.0 and added to my module as a composer dependency.
How it works
It's function is pretty straightforward. The phpparser libraries allow you to read the headers, metadata, and basic structure of a PDF. If it has that information, the module can search for inaccessible patterns in your uploaded PDFs. Depending on the size of the PDF, this process typically takes seconds.
Currently, PDFa11y provides six checks, with a few more planned in the backlog.
Document language
Checks if the PDF declares its document language. Screen readers use the language to apply correct pronunciation rules; without it, content can be read with the wrong accent or phonetic interpretation.
Tagged PDF
Checks if the PDF is marked as tagged. Tags identify reading order, headings, lists, and other semantic elements that screen readers rely on. Works together with the Heading structure check.
Document title
Checks if the PDF has a title in its metadata. The title is announced by screen readers when the document opens and appears in browser tabs and bookmarks, helping users identify and return to documents.
Heading structure
Checks that the PDF contains a structure tree and that headings within it start at H1 without skipping levels. A missing structure tree, no headings at all, or a hierarchy that jumps from H1 to H3 prevents screen reader users from navigating the document.
PDF version
Checks the PDF version against a configurable minimum (default 1.4). Older PDF versions lack support for tagging and other accessibility features required by assistive technologies.
Document title is not a filename
Flags titles that end with a document file extension (e.g., "Report.docx") because they are usually leftover filenames rather than descriptive titles. Disable this check if your workflow intentionally uses filenames as canonical titles, such as for standardized forms.
I've thought through many more possible checks. In fact, a lot of the checks in Editoria11y that are focused on HTML structure and patterns could be rewritten for the XML format of PDFs. The major difference is that Editoria11y can show you the problem on output or directly in CKEditor. For instance, it will show you exactly where heading order starts to break, PDFa11y can't do that because PDFs are only rendered via a viewer and not as HTML themselves. There is no reliable way to show a javascript widget on top of every browsers' PDF preview. Especially, if you don't use a browser to preview the file. While you can put a link into a PDF, or even provide a link to a PDF, it is not HTML and should never be treated as a webpage. (I once helped a client with a massive migration where PDFs were sometimes their own little interlinked web worlds. It was infuriating to catch all the broken links when something changed.)
Error handling for PDFs that cannot or should not be scanned
One key finding as we rolled out PDFa11y on production data was that certain files just can't be scanned by smalot/pdfparser. They are too complex. We skip parsing PDFs for the following reasons:
- The file is missing. The media record points at a file that no longer exists in storage (usually a leftover from a deleted or migrated file). There's nothing to scan.
- The file can't be read right now. A slow or flaky connection to remote storage (like S3) timed out. This is treated as temporary — the scan can be retried later.
- The PDF is encrypted or has security restrictions. Password protection or copy/edit restrictions prevent the checker from opening the document. The fix is to re-export it without security (in Acrobat: File > Properties > Security > "No Security") and re-upload.
- The PDF is corrupt or malformed. The file doesn't follow the PDF specification well enough to be parsed — often the result of a broken export, a truncated upload, or a file that isn't really a PDF.
- The file is too large. It exceeds the configured size limit, so we don't attempt to parse it.
- The PDF is packed with images. Scanned documents, maps, and technical drawings can be small on disk but balloon to hundreds of megabytes when their images are decompressed during parsing. We estimate this up front and skip files that would exhaust server memory.
- Parsing took too long or overwhelmed the server. A rare file can hang the parser or run it out of memory. We cap the time and memory a single file can use, skip it, and move on so one bad file never derails a bulk scan of the whole library.
Reporting on inaccessible PDFs
The PDF accessibility report will should you if the PDF passed, failed, or error (skipped) with counts and a link to the per media or file entity accessibility tab. All of the needed views plugins are present for you to create your own views of this data.
Roadmap
While I'm always open to feature requests in the issue queue, there are some features I hope to address on the roadmap in the next few months.
- Integrate with Editoria11y. James Jameson and I have already had a couple of chats about these two ecosystems having a closer relationship and interactivity. Still not sure on the best path forward for this, but I might start with a custom Editoria11y check that shows PDF accessibility fails and errors on embedded media.
- Check for missing alt text. Simple check for the PDF concept of a
<figure>, which is similar to the HTML element, and whether it correctly declares alt text. - More settings for fine tuning. For example, you might want to be able to set the max-filesize parse error in the settings rather than only in config overrides. It's not even super clear you can override these settings just yet.
Need help auditing your website for accessibility? Want to implement PDFa11y on your site or fund a new feature? Connect with me on LinkedIn or reach out to me on Drupal Slack (@joshuami).