Skip to content

The second most cited requirement, 20 of 26 clients

Python, because the library
only exists in Python.

Everything else on this site is TypeScript, because that was the right tool. This is a FastAPI service in a container on its own box, reading PDFs with PyMuPDF, which reads a text layer along with its coordinates and font metrics. There is no equivalent in Node without shelling out to a binary.

Point it at any public PDF. The useful part is not the word count, it is that it tells you which pages it could not read and why.

The ones in orange must be refused. It fetches whatever you type, so those are the interesting half.

What it does that a wrapper around a parser does not

01

It says when it could not read something

A PDF from a scanner is a stack of photographs with no text in it. Every extractor returns an empty string for those. One that returns an empty string alongside a success is how a pipeline silently processes nothing for a month, so this reports coverage per page, names the pages that need OCR, and says outright when the whole document is a scan.

02

The bytes decide what the file is, not the name

A URL ending in .pdf is a claim by whoever wrote the link, and the content-type is a claim by the server. Neither is trusted. The file has to begin with %PDF or it is refused before the parser sees it, because a parser is exactly where a hostile file wants to be.

03

The same address guard, rewritten in Python

This fetches an address a stranger typed, which is the same hostile surface the audit tool has. The guard is not a translation of the TypeScript code, it is a translation of what 74 tests taught it: never compare hostnames, judge every address a name resolves to, pin the socket to the address you checked, and walk redirects by hand. 72 tests here, including every spelling of a loopback address in IPv6 that could be thought of.

04

Not root, memory bounded, health checked

It runs as an unprivileged user in a container with half a gigabyte and one CPU, behind a health check the deploy script waits on rather than assumes. The whole job of this service is fetching things a stranger named, so it runs as somebody who can do very little if that ever goes wrong.

05

Two guards in two languages, not one and an assumption

The Node proxy in front of this does its own rate limiting, and the Python service does its own address checking rather than trusting the proxy to have done it. The day somebody calls the service directly is the day an assumption is worth nothing.

How it gets there

pytest services/pdf-reader -q          86 tests, 72 of them on the address guard
python scripts/deploy-pdf-reader.py    copy, build on the box, restart, wait for health

The deploy script prints every step and waits for the health check to answer before it says it is done, rather than starting a container and assuming. A deploy that says nothing is a deploy somebody has to go and check by hand.

What this is not

It does not do OCR. It tells you when OCR is what you need, which is a different job with a different cost and a different accuracy, and pretending otherwise is how somebody ends up paying for a pipeline that reads nothing.

Tables are counted, not extracted. Claiming to have parsed a table and handing back the wrong cells is worse than saying there is one there.

Headings are a guess from font size, because a PDF carries no structure to read. The output says so rather than presenting the guess as a fact.

Forty pages and twenty five megabytes, and it says when it truncated. Reading part of a document and reporting the totals as though they were the whole thing is the same defect as a skipped check improving a score.

The endpoint reports whether the Python service is actually up rather than describing one that might be dead. The whole list is 41 requirements from 114 job posts, with the gaps shown at the same size as the wins.