WatsonTsai/pdf-text-router
7 stars · Last commit 2026-08-29
A Claude Code hook that routes each Read on a PDF to the path that actually works: text layer to UTF-8 text, scans to vision, and neither one through poppler.
README preview
# pdf-text-router A Claude Code hook that routes each `Read` on a PDF to the path that actually works. English · [繁體中文](README.zh-TW.md) You ask Claude Code to read a PDF. Nothing errors. Claude quietly switches to some other method — a `Bash` one-liner, a guess from the filename — or tells you the file could not be opened. What actually happened underneath is `pdftoppm is not installed`, and you never saw it, because Read errors are not surfaced in the TUI ([#23699](https://github.com/anthropics/claude-code/issues/23699), closed as stale). Any PDF over ten pages, and any Read with a page range, goes through poppler's `pdftoppm`, and the official setup docs have never said so ([#23704](https://github.com/anthropics/claude-code/issues/23704), open). Even when poppler *is* there, a PDF with a real text layer is being read as pictures of words. **What you get instead.** For a PDF with a text layer, the hook rewrites the Read in place: one `Read`, and what comes back is a UTF-8 `.txt` with `[page N/M]` markers, so Claude can still cite pages, `Grep` the file, and never runs an OCR pass over text that was already text. No poppler involved. For a scan, the hook lets the native path through when it works, and when it would fail for want of poppler it renders the pages locally, points the same Read at the first PNG and lists the rest. **What it costs.** The extracted text is cached in plain text under `~/.claude/pdf-text-cache`. The PDF was already a readable file on the same machine, so the cache adds exposure in one situation only: the PDF lives on an encrypted volume, an external drive or a network share, and the cache lands in your home directory. For that case, `PDF_TEXT_ROUTER_CACHE=beside` keeps the cache next to the PDF, or point the variable at a directory of your own choosing; there is no encrypted mode (see [Cache](#design-notes)). Slide decks are the biggest saving and also the biggest trap: text drawn inside a figure — station names on a map, a scale bar, a whole slide exported as an image — is not in the text layer. The hook classes every page as text, blank, image or figure from its character count and the share of it covered by image objects, so it can flag a slide that is one big picture and a page that is a large figure with a caption; what it cannot flag is a vector drawing — a map or a chart drawn as paths has an image area near zero and reads as a sparse text page. The first Read of a large file pays for the extraction (the 1,039-page bill in the corpus takes 2.1–2.3 s with `pypdfium2`); every later Read is a cache hit that never opens the PDF (0.3 s on the same file). ## Is this for you? Two questions. 1. `which pdftoppm` (Unix) or `where pdftoppm` (Windows). If it prints nothing, Claude Code cannot currently read any PDF over ten pages, or any page range, and this hook is the difference between a document and an error message. 2. `python scripts/benchmark.py <a folder of your own PDFs>`. It prints, for each file, what Claude Code would spend on images versus what the text costs, and a median. Reading the result: