The brief was an architecture question. A client's invoice portal had to be rebuilt on a footing that would hold for the next several years. Only during implementation did a requirement surface that appeared in none of the design documents. The invoices this portal delivers have to be accessible. The pattern is familiar. Obligations of this kind rarely appear in the specification. They appear once construction starts.
A look at the market was sobering. The commercial tools on offer were either priced so that licence fees across an invoice archive would break any business case, or slow enough that a batch run over the back catalogue would not fit into a maintenance window. A requirement that can be neither funded nor scheduled tends to sit untouched in a programme until somebody outside asks about it.
So the missing answer was built inside the mandate. It is called accessipdf, it is published under the MIT licence on GitHub, and it has run in production.
The state of the archive
Feed a typical invoice to a screen reader and what comes back is close to useless. There are no headings for navigation to work with. Behind the character codes there is often no letter that can be pronounced. The fonts the document uses are not embedded in it. None of this is visible on screen. The document looks correct and is worthless to assistive technology.
The European Accessibility Act, transposed in Germany as the Barrierefreiheitsstärkungsgesetz, has applied since 28 June 2025. In the United States, Section 508 has imposed a comparable duty for far longer. The deadlines did not tighten the technical standard. What changed is how exposed a company is when it sends documents to consumers. A document a portal delivers today has to work for its recipient today.
Why regeneration is out
Regenerating the back catalogue is rarely a realistic option. The system that produced those invoices has been replaced or has moved on, and the people who signed off the layout have left the company. Even where regeneration is technically possible it is the wrong instrument. What is required is an additional, invisible layer of structure inside the existing document. Reissuing instead creates a second document for a transaction the recipient has already filed and may well hold against their own books.
That imposes a condition, and it was fixed before the first line of code. The original stays untouched, and the output has to match it pixel for pixel. Anything that shifts a line or drops a glyph produces a different document, and in an invoicing process a different document is a new matter entirely.
Five stages
Processing runs in five stages per file.
First, accessipdf reads the content streams, tracks the graphics state and recovers every text operator with its position and decoded text. pikepdf and pypdfium2 do that work together.
Second, it matches the file against the registered layout templates, written in YAML, using anchor texts. If no template applies, the file goes to quarantine with a machine-readable report. There is no heuristic fallback. The program says instead that it does not recognise the file.
Third, it derives the role of each zone from the template. Headings, paragraphs and real tables with header cells, including tables that run across page boundaries. Decorative content is marked as an artifact, and the order of the zones gives the reading order.
Fourth, the tool rewrites the content streams. Content is tagged with BDC and EMC plus MCIDs, the structure tree is built, and language, title and the XMP identifier for PDF/UA are set. Font repair happens in the same pass. Missing ToUnicode CMaps are generated, metric-compatible faces from the Liberation family are embedded in place of standard fonts that were referenced but never embedded, weights are preserved, CIDToGIDMap is corrected and broken CIDSets are removed.
Fifth, the result goes to veraPDF, the open validator for PDF/UA. Clean files move atomically into the output directory. Files with findings move to quarantine together with the full rule report. There is no route through this stage that yields an unvalidated pass.
Repeat runs are idempotent through a SHA-256 registry. The engine itself needs 0.1 to 0.15 seconds per invoice. Starting the Java runtime for the veraPDF call costs a further 0.7 seconds and therefore dominates the wall clock. That is the point at which a cost problem turned into an arithmetic exercise that can be put in front of an operations manager.
Templates, not guesswork
This is where the real decision sits, and it is an uncomfortable one. A generic auto-tagger accepts any PDF and infers the structure heuristically. It always returns a result, and the quality of that result cannot be established in advance. accessipdf processes only layouts for which somebody has registered a template.
A template is a YAML file holding anchor texts for recognition, zones in page coordinates and the roles that belong to them. Zone order is reading order. A table zone applies only on the pages where its header anchors actually occur. The file can be read, reviewed and changed on stated grounds, and it returns the same result for the same input every time.
That is a trade: coverage for auditability. For uniform, template-generated documents it is a good one. A business sending a hundred thousand invoices of the same layout does not need versatility. It needs a statement that will survive an auditor.
There is also a consideration that seldom appears in a technical assessment and is decisive in a board paper. A heuristic result can be neither justified nor reproduced. A template can be tabled. The same principle underlies Souprise: say nothing rather than produce a plausible invention.
Three uncompromising criteria
The criteria were fixed before any code existed, and they were not relaxed afterwards.
First, veraPDF must report zero errors against the PDF/UA-1 profile for every file.
Second, each page is rendered before and after and compared pixel by pixel. One deviation is permitted, the anti-aliasing at glyph edges after a font has been embedded, and even that has to survive an erosion mask with a 9×9 kernel proving that the difference contains no solid area.
Third, no line of previously extractable text may be lost. Improvement is allowed and happens routinely, because the generated ToUnicode CMaps repair extraction that was garbled before. That is precisely what PDF/UA is for.
The test suite builds its own specimen: a synthetic invoice with no tagging, no ToUnicode, and Helvetica and Helvetica-Bold referenced but not embedded. It runs the full chain including the veraPDF gate.
In the mandate the engine processed real telecom invoices from two layout families. All 13 of 13 files met all three criteria. Those documents contain customer data and are therefore not in the repository.
A clean machine verdict still does not replace acceptance by a person. Before a template goes into production it needs one manual pass with a checker such as PAC 2024 and a screen reader. A rule checker checks rules. Whether a human can follow the document is a question it does not answer.
The operational case
For whoever runs an invoice portal, the approach moves three numbers.
Cost no longer attaches to a licence per document but to the one-off work of measuring the layouts. In the mandate, two templates covered the entire invoice run. Because mass documents come out of a template system, the number of layout families is small in practice.
Throughput becomes predictable. At a tenth of a second per document in the engine, a run across the back catalogue is a matter of hours and parallel processes rather than months.
And the evidence changes character. What remains at the end is a validation report per file from an independent, freely available checker. That is a different thing from a vendor's assurance, and in a dispute it is exactly the document that gets produced.
The route there rarely runs through a tool purchase. It starts with the question of which documents a portal delivers, which system generates them, and where in the processing chain the missing layer of structure is cheapest to insert. Answering that question was the mandate. The software is its result, not its starting point.
Installation and test
The requirements are Python 3.12 or newer and the veraPDF CLI on the path. The repository ships a demo invoice built to be inaccessible on purpose.
git clone https://github.com/mkupermann/accessipdf
cd accessipdf
make setup
.venv/bin/python -m accessipdf.demo demo_invoice.pdf
.venv/bin/accessipdf check demo_invoice.pdf # FAIL
.venv/bin/accessipdf identify demo_invoice.pdf # Layout: acme-demo
.venv/bin/accessipdf convert demo_invoice.pdf out/
.venv/bin/accessipdf check out/demo_invoice.pdf # PASS, PDF/UA-1
convert exits 0 when every file came out clean, 1 as soon as at least one file went to quarantine, and 2 on a hard error. That makes it straightforward to wire into a batch job.
The source is on GitHub under the MIT licence. No commercial SDK is involved. The bundled Liberation fonts are covered by the SIL Open Font License, and veraPDF is called as an external tool.
The finished document looks exactly as it did before. The difference is apparent only to someone who has it read aloud.