The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Pdffr listing page.
A PDF decompiler, not an image reader. PDF → Markdown in the browser or Node, in milliseconds for born-digital pages — with on-device OCR spent only on the pixels the text layer can't explain.
Try it in your browser → — drop any PDF; it never leaves the tab.
Every PDF→Markdown tool sits at one of two extremes:
| Approach | Speed | Quality | Problem |
|---|---|---|---|
Text-layer extraction (pdfminer, pdf.js getTextContent) | instant | poor | PDF has no paragraphs, headings, tables or reading order — you get a soup of positioned strings |
| Render + OCR / vision model (Textract, LlamaParse, VLMs) | slow, paid, cloud | high | Rasterizes a page that was already digital, then asks a model to re-read pixels the file could have told it exactly |
~80% of real-world PDFs are born-digital: every glyph's exact coordinates, size and font are already in the file. pdffr treats PDF as what it is — a drawing program — and decompiles the drawing back into structure:
**bold**/*italic*/<sup>, math fonts and sub/superscripts transliterated to $LaTeX$, rotated text re-framed upright, running header/footer stripping, hyphenation repair. No rasterization. Milliseconds per page.pdffr/node runs the identical pipeline on the server or the command line, with @napi-rs/canvas standing in for the DOM.pdfjs-dist and tesseract.js are peer dependencies; @napi-rs/canvas is an optional peer used only by the Node entry.
decompile(input, options?) → Promise<DecompileResult> — input is an ArrayBuffer, Uint8Array, Blob or File. Options: ocr, lang, concurrency, pool, onPage, onEvent, pdfWorkerSrc.decompileFile(path, options?) — Node only.warmOcr(lang?) / terminateOcr() — pre-load or shut down the shared tesseract pool.ocrPool(lang?) — the shared OcrPool; pass your own via options.pool to control worker count.runPipeline(buffer, emit, { ocr, concurrency, escalate }) — the streaming core, if you want raw events.blocksToMarkdown(blocks) — render typed blocks yourself.setPdfWorkerSrc(url) — configure pdf.js's worker.Types: Block, ListItem, Run, Region, Rules, PageState, Stats, PipelineEvent.
| Package | What it is |
|---|---|
pdffr-mcp | MCP server for Claude Desktop / Claude Code / Cursor / any agent: pdf_to_markdown, pdf_outline, pdf_tables — listed on the MCP Registry |
pdffr-langchain | LangChain.js document loader — one Markdown Document per page |
pdffr-llamaindex | LlamaIndex.TS reader — one Markdown Document per page |
langchain-pdffr (PyPI) | Python: pdffr.convert() and a LangChain PdffrLoader, driving the CLI (needs Node 20+) |
Structure pass: rotated runs re-framed upright (a dominant rotation turns the whole page; a minority is a sidebar group) → buildLines (math spans → LaTeX) → orderRuns (XY-cut: tall prose gutter → vertical cut; largest whitespace band → horizontal cut; ruled and aligned tables detected first as atomic boxes) → toBlocks (headings, lists with nesting, paragraphs by leading, display math, tables, furniture stripping) → markdown.
The playground in demo/ shows each page with the engine's decisions drawn on it — text it read straight from the file, regions it sent to OCR and what came back — beside the decompiled document. It opens on a sample report; drop any PDF onto it. Four canonical samples ship with it: a born-digital report (headings, bold runs, a list, a table, a two-column page, running header and page numbers), a full-page scan of the same report, a mixed document with a scanned insert inside native text, and one page each of a ruled table, a rotated sidebar and equations.
docs/benchmark.md compares pdffr with pdf-parse, raw pdf.js text and pdf2md on the four samples — time, and how much of the reference structure (headings, table rows, list items, reading order) each tool reproduces. Regenerate with npm run bench; add a cloud parser to bench/run.mjs if you have a key.
docs/benchmark.md — reproducible comparison against other open-source PDF tools.docs/architecture.md — the pipeline, the render-diff oracle, the shared IR, and every heuristic with its threshold and rationale.CONTRIBUTING.md — layout of the code, the one rule for new heuristics, how to add a test.CHANGELOG.mdCI runs typecheck, format check, tests and the build on every push.
Early. It is accurate on the documents it was built against (reports, Word exports with screenshots, scans, two-column layouts, ruled tables, rotated sidebars, simple equations) and will have gaps on others. What it handles today:
$…$ and display $$…$$ LaTeX.Also: letter-spaced headings, label columns (**KSA-UAE tension** — paragraph layouts become headings over their paragraphs), card/lane layouts, fractions drawn with a bar, multi-line display math, paragraphs cut by a page break, bold recovered from OCR stroke weight.
Known limitations:
aligned blocks are not reconstructed.x2 with no italic or math font) is not recognised as math.Bug reports with a PDF attached are the fastest way to improve it.
MIT © Amer Sarhan