{
  "markdown": "# pdffr\n\n[![CI](https://github.com/AmerSarhan/PDFFR/actions/workflows/ci.yml/badge.svg)](https://github.com/AmerSarhan/PDFFR/actions/workflows/ci.yml)\n[![npm](https://img.shields.io/npm/v/pdffr.svg)](https://www.npmjs.com/package/pdffr)\n[![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n\n**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.\n\n**[Try it in your browser →](https://amersarhan.github.io/PDFFR/)** — drop any PDF; it never leaves the tab.\n\n[![pdffr decompiling a mixed native + scanned document: the page on the left with native text tinted and the OCR region outlined, the markdown beside it](docs/demo.png)](https://amersarhan.github.io/PDFFR/)\n\n```ts\nimport { decompile } from 'pdffr';\n\nconst { markdown, stats } = await decompile(file);\n// stats.firstOutputMs ≈ 150ms for a typical report; the file never leaves the tab\n```\n\n```bash\nnpx pdffr report.pdf -o report.md\n```\n\n## Why this exists\n\nEvery PDF→Markdown tool sits at one of two extremes:\n\n| Approach                                                  | Speed             | Quality | Problem                                                                                                               |\n| --------------------------------------------------------- | ----------------- | ------- | --------------------------------------------------------------------------------------------------------------------- |\n| 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                       |\n| 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 |\n\n~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:\n\n- **Geometry-native decompilation.** Glyph runs → lines → an XY-cut reading-order tree over whitespace → headings (font-size clustering), paragraphs (leading analysis), nested lists (marker glyph + indent), tables (ruling lines from the content stream, or column x-alignment), inline `**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.\n- **Render-diff oracle.** When a page carries bitmaps or thin text coverage, the page is rendered once, the raster's ink mask is computed, and the dilated boxes of every native glyph are _erased_ from it. What's left is ink the text layer cannot explain — scans, stamps, screenshots with burned-in text. Exact image rectangles from the content stream (CTM-tracked) sharpen the regions further. **Only those regions** go to OCR.\n- **One IR for both sources.** OCR words come back with boxes and confidence, get gated by a text-plausibility test (confidence alone lies on icons and charts), and enter the _same_ geometry engine as native glyphs. Structure recovery is source-agnostic.\n- **Parallel and optimistic.** Pages decompile concurrently; markdown streams out immediately with placeholders; a pool of tesseract workers fills them in place. Whole-page scans are split along their own ink into chunks so the pool works in parallel.\n- **Private by construction.** pdf.js and tesseract.js run in web workers in the user's tab. Nothing is uploaded.\n- **Same engine in Node.** `pdffr/node` runs the identical pipeline on the server or the command line, with `@napi-rs/canvas` standing in for the DOM.\n\n## Install\n\n```bash\nnpm install pdffr pdfjs-dist tesseract.js\n# Node / CLI additionally:\nnpm install @napi-rs/canvas\n```\n\n`pdfjs-dist` and `tesseract.js` are peer dependencies; `@napi-rs/canvas` is an optional peer used only by the Node entry.\n\n## Usage\n\n### Browser\n\n```ts\nimport { decompile, warmOcr, setPdfWorkerSrc } from 'pdffr';\n\n// Bundled apps: point pdf.js at its worker. Without this, pdffr falls back to the jsdelivr build.\nsetPdfWorkerSrc(new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).href);\n\n// Optional: pre-load OCR workers while the user is still choosing a file.\nwarmOcr();\n\nconst result = await decompile(file, {\n  ocr: true, // escalate unexplained ink to on-device OCR (default true)\n  lang: 'eng', // tesseract language(s): 'deu', 'eng+ara', 'chi_sim', …\n  concurrency: 4, // pages decompiled in parallel\n  onPage(page, md) {\n    // streams: first the native pass, then again as OCR regions land\n    render(page, md);\n  },\n  onEvent(e) {\n    // every trace line, page (re)emit, and stats update\n    if (e.type === 'trace') console.log(e.kind, e.msg);\n  },\n});\n\nresult.markdown; // the whole document\nresult.pages[0].blocks; // typed blocks: heading | para | math | list | table\nresult.stats; // firstOutputMs, nativeDoneMs, totalMs, ocrRegions, nativeChars, ...\n```\n\n### Node\n\n```ts\nimport { decompileFile, terminateOcr } from 'pdffr/node';\n\nconst { markdown } = await decompileFile('invoice.pdf', { lang: 'deu' });\nawait terminateOcr(); // let the process exit once the tesseract workers are done\n```\n\n### Command line\n\n```bash\npdffr scan.pdf                    # markdown on stdout, progress on stderr\npdffr scan.pdf -o scan.md --lang eng+fra\npdffr paper.pdf --no-ocr -q       # native text only, silent\npdffr paper.pdf --json --pages 1-3  # per-page markdown, typed blocks and stats as JSON\n```\n\n### API\n\n- `decompile(input, options?) → Promise<DecompileResult>` — `input` is an `ArrayBuffer`, `Uint8Array`, `Blob` or `File`. Options: `ocr`, `lang`, `concurrency`, `pool`, `onPage`, `onEvent`, `pdfWorkerSrc`.\n- `decompileFile(path, options?)` — Node only.\n- `warmOcr(lang?)` / `terminateOcr()` — pre-load or shut down the shared tesseract pool.\n- `ocrPool(lang?)` — the shared `OcrPool`; pass your own via `options.pool` to control worker count.\n- `runPipeline(buffer, emit, { ocr, concurrency, escalate })` — the streaming core, if you want raw events.\n- `blocksToMarkdown(blocks)` — render typed blocks yourself.\n- `setPdfWorkerSrc(url)` — configure pdf.js's worker.\n\nTypes: `Block`, `ListItem`, `Run`, `Region`, `Rules`, `PageState`, `Stats`, `PipelineEvent`.\n\n## Integrations\n\n| Package                                            | What it is                                                                                                                                                                                |\n| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [`pdffr-mcp`](packages/mcp)                        | MCP server for Claude Desktop / Claude Code / Cursor / any agent: `pdf_to_markdown`, `pdf_outline`, `pdf_tables` — listed on the [MCP Registry](https://registry.modelcontextprotocol.io) |\n| [`pdffr-langchain`](packages/langchain)            | LangChain.js document loader — one Markdown `Document` per page                                                                                                                           |\n| [`pdffr-llamaindex`](packages/llamaindex)          | LlamaIndex.TS reader — one Markdown `Document` per page                                                                                                                                   |\n| [`langchain-pdffr`](python/langchain-pdffr) (PyPI) | Python: `pdffr.convert()` and a LangChain `PdffrLoader`, driving the CLI (needs Node 20+)                                                                                                 |\n\n```python\nfrom langchain_pdffr import PdffrLoader\ndocs = PdffrLoader(\"report.pdf\").load()   # one Markdown Document per page\n```\n\n```json\n{ \"mcpServers\": { \"pdffr\": { \"command\": \"npx\", \"args\": [\"-y\", \"pdffr-mcp\"] } } }\n```\n\n## How a page flows through\n\n```\ngetTextContent ─► runs (x, y, w, h, size, bold, italic, math font, rotation)\n                   │\ngetOperatorList ─► exact bitmap rects + ruling lines (CTM walk), font resolution\n                   │\n        suspicious? (bitmaps, or thin coverage)\n             │ no                          │ yes\n             ▼                             ▼\n     structure pass                render page once (print intent)\n                                   ink mask − native glyph boxes = residual\n                                   regions = bitmap rects ∪ residual components\n                                   large regions split along their ink\n                                   ─► OCR pool (2×/3× upsampling for small crops,\n                                      second read of doubtful words,\n                                      text-plausibility gate)\n                                   ─► OCR runs join the same structure pass\n```\n\nStructure 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.\n\n## Demo\n\n```bash\nnpm install\nnpm run dev\n```\n\nThe 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.\n\n## Benchmark\n\n[`docs/benchmark.md`](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.\n\n## Documentation\n\n- [`docs/benchmark.md`](docs/benchmark.md) — reproducible comparison against other open-source PDF tools.\n- [`docs/architecture.md`](docs/architecture.md) — the pipeline, the render-diff oracle, the shared IR, and every heuristic with its threshold and rationale.\n- [`CONTRIBUTING.md`](CONTRIBUTING.md) — layout of the code, the one rule for new heuristics, how to add a test.\n- [`CHANGELOG.md`](CHANGELOG.md)\n\n## Development\n\n```bash\nnpm test             # vitest: unit tests + Node end-to-end runs on the sample PDFs\nnpm run typecheck\nnpm run format\nnpm run build        # library to dist/, demo to dist-demo/\n```\n\nCI runs typecheck, format check, tests and the build on every push.\n\n## Status and roadmap\n\nEarly. 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:\n\n- Born-digital text with headings, nested lists, tables (ruled or aligned), inline styles, two-column reading order, running headers/footers, hyphenation.\n- Scans and figures with burned-in text via on-device OCR, any tesseract language, with icons and chart glyphs rejected by shape and colour rather than trusted on confidence.\n- Rotated pages and sidebars (multiples of 90°); skewed watermarks are dropped, not reordered.\n- Math set in math fonts (Symbol, Computer Modern, STIX, Cambria Math…): Greek, operators, sub/superscripts → inline `$…$` and display `$$…$$` LaTeX.\n- Browser, Node and CLI.\n\nAlso: 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.\n\nKnown limitations:\n\n- Radicals with an argument bar, matrices and `aligned` blocks are not reconstructed.\n- Math typed in an ordinary upright text font (a bare `x2` with no italic or math font) is not recognised as math.\n- Tables whose cells span rows or columns are flattened.\n- Icons that are neither solid nor coloured (a thin grey outline) can still OCR into a character.\n- OCR output carries bold (from stroke weight) but no italic.\n\nBug reports with a PDF attached are the fastest way to improve it.\n\n## License\n\nMIT © Amer Sarhan\n",
  "bytes": 12930,
  "sha": "b36f6d29b9d6264fe99d2e5cadd5e076e00e9a6b59c3776e7e98fd4d1b5c0ca0",
  "repo_slug": "amersarhan/pdffr",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_amersarhan_pdffr_mcp_e457a3da/readme"
}