Skip to content
Go back

Shipping horn v0.4.0 from my phone on a hike

Published: at 08:55 PM
This week I released version 0.4.0 of horn, my open source PDF accessibility checker, from my phone, on the way up the Little Matterhorn in Rocky Mountain National Park. Horn checks PDFs against something called the Matterhorn Protocol. I did not plan that, but I was not going to let it go to waste either. Me on the summit of the Little Matterhorn, smiling, in a cap, mirrored sunglasses and a yellow rain hood, with the rocky tundra and peaks of Rocky Mountain National Park behind me

How I shipped it from a phone

The setup is less exotic than it sounds. The Claude Code app on my phone connects to the horn repository on GitHub. When I start a session it gets its own checkout of the repo in the cloud, and I talk to it from the phone. The model was Fable 5.1. All the phone needs is a data connection.

Without the work that went into horn long before this hike, I would have had to wait until I was back at a laptop. Fable wrote the code, but I could only trust it from a phone because the repository already checks itself.

The veraPDF corpus and the pdfcheck fixtures are git submodules, so every session has hundreds of real PDF/UA files with known outcomes to test against, and the corpus tests assert the exact pass and fail counts for each set. Every push runs CI: rustfmt, clippy with warnings treated as errors, the unit tests, and a release build that runs the full corpus and uploads the results to code scanning.

Releasing was already automated too. Pushing a version tag starts a workflow that checks the tag against the version in every Cargo, Tauri and docs file, runs CI again, builds the CLI binaries for Linux, macOS and Windows along with the WASM package and the desktop app, and then publishes the GitHub release. If a version does not match or a check fails, nothing gets published.

On a phone I could not run anything locally, and a big diff is hard to review on that screen. I did not need to do either. If a new rule broke one that already passed, a corpus count would drop and CI would go red. So on the summit I read the numbers and the CI results, and released.

The hike

The Little Matterhorn is a rocky point of about 3,500 metres above Odessa Lake, in the Bear Lake corner of the park. I live at roughly sea level, so my lungs had opinions about it. Strava says 15.73 kilometres and 990 metres of climbing, five hours in total.

Strava summary of the hike: 15.73 km distance, 990 m elevation gain, 4:17:49 moving time, 16:23 average pace, 2682 calories, 5:09:09 total time

The views up there are amazing!

View from the Little Matterhorn: a deep forested valley with a small lake, grey rock walls in the foreground and Longs Peak on the skyline under a blue sky

The flat topped peak on the left of the skyline is Longs Peak, I sat here for a while, and shipped a release.

What horn is

Horn is an open source PDF/UA checker written in Rust. You point it at a PDF, or a folder full of them, and it tells you where the file breaks the PDF/UA standard, with the official Matterhorn Protocol index on every finding. So 28-010 in horn means exactly what 28-010 means in the protocol document or in any other checker.

The Matterhorn Protocol is the PDF Association’s list of every way a PDF can fail PDF/UA-1. There are 137 entries. Horn covers all of them: 87 can be tested by a machine and are automated, 48 need a human (is this alt text actually any good?) and get reported as manual review items, and two have no defined test and are marked not applicable. It scores 100% on the veraPDF PDF/UA-1 corpus of 297 files and passes the PDF Association’s reference suite.

I built it because the best known free checker, PAC 2024, is a Windows desktop app. Great tool, but you cannot put it in a pipeline. Horn is one binary for Linux, macOS and Windows, validates around 660 PDFs per second, and writes SARIF, JUnit or JSON. A PDF that is not accessible can fail your build like any other test. There is a GitHub Action, and a browser demo on horn.report if you just want to drop a file in and see.

What is new in 0.4.0

Until this release horn only really knew PDF/UA-1. If you gave it a PDF/UA-2 file (the 2024 revision of the standard, ISO 14289-2, built on PDF 2.0) it ran the PDF/UA-1 checks and stopped there. Everything that only exists in the newer standard went untested.

0.4.0 adds those 37 rules. Some of them are about identification: pdfuaid:rev has to be present and say 2024, the metadata stream needs its Type and Subtype, embedded files need a description. A bigger group is about structure: one Document root in the PDF 2.0 namespace, namespace role maps that do not loop back on themselves, the new FENote element, MathML under Formula, TOCI entries with a Ref, no private use code points in Alt or ActualText. Then annotations: invisible annotations have to be artifacts, Popup annotations do not belong in the structure tree, form widgets need a label, links with different targets cannot share one Link element. And structure destinations, so an outline or a link points at a structure element instead of a spot on a page.

The Matterhorn Protocol 2.0, the version for PDF/UA-2, is not out yet. Until it is, these rules carry interim ids like ua2:8.2.5.12-1 that follow the clause numbers of the standard, the same scheme veraPDF uses. Horn reads pdfuaid:part and picks the right rule set on its own, so the command does not change.

Fable did the heavy lifting. It took the veraPDF PDF/UA-2 profile as its reference, wrote the new check modules, wired PDF 2.0 namespace resolution into the existing role map check, ran the whole corpus, and came back with the numbers.

The numbers: 138 out of 138 files in the veraPDF PDF/UA-2 corpus, up from 122. Fable spotted that two of the old misses were false positives on horn’s side, not problems in the test files, and fixed them. PDF/UA-1 stayed at 297 out of 297. The corpus tests now treat these counts as a floor, so if a future change drops a single file, CI goes red.

What is next

Matterhorn Protocol 2.0. When the PDF Association publishes it, the ua2: ids get swapped for the official ones and horn’s PDF/UA-2 coverage gets a proper matrix, the same as horn coverage prints for PDF/UA-1 today.

If you want to try it: grab a binary from the releases page, or install from source.

cargo install --git https://github.com/focusring/horn.git
horn validate document.pdf

And if you are ever near Estes Park with a free day, go up the Little Matterhorn. Bring a hood.

Written by Tim’s agent.



Next Post
Scaling Digital Accessibility: My JSNation US 2024 Experience