# @mindmax/pdf-markdown: open-source PDF to Markdown for Node and the browser

URL: https://mindmax.me/blog/pdf-markdown/
Last verified: 2026-11-10

> @mindmax/pdf-markdown is an Apache-2.0 PDF to Markdown library with zero runtime dependencies, tested on 22 public documents. Fixes, refusals and open defects.

## We open-sourced our PDF-to-Markdown engine, and published what it still gets wrong

November 9, 2026 · 8 min read

[The MindMax team](https://mindmax.me/about/)

Building MindMax

**@mindmax/pdf-markdown** is an open-source PDF-to-Markdown library for Node 18+ and the browser,
published under Apache-2.0. It has no runtime dependencies, makes no network call, and is checked
against 22 public documents (612 pages) with 120 assertions that someone verified by hand against
the PDF. It also ships with a list of the cases it still gets wrong, and we would like your help
with them.

`

npm i @mindmax/pdf-markdown

`

The same package is what runs behind [the PDF to Markdown page](https://mindmax.me/tools/pdf-to-markdown/) and
inside the MindMax apps. It is not a demo build of something private.

## Why PDF to Markdown goes wrong[​](https://mindmax.me/#why-pdf-to-markdown-goes-wrong)

A PDF does not store paragraphs. It stores glyphs at coordinates. Everything a reader sees as
structure — columns, headings, tables, the order to read in — is something the converter has to
guess back. Most bad output comes from the same handful of guesses going wrong.

- **Columns read straight across.** A two- or three-column paper comes out as half a sentence from
the left column glued to half a sentence from the right.

- **Page furniture inside the text.** Running headers, footers and page numbers land in the middle
of paragraphs, every page.

- **Tables that are not tables, and tables that are.** A real table collapses into lines of
spaces; a displayed equation or a narrow column is read as a table.

- **Headings guessed from font size.** One large symbol becomes a `#` heading, and a wrapped
two-line title becomes two headings.

- **Your text gets reinterpreted.** A PDF that contains `$`, `*`, `_` or a line starting with `#`
produces Markdown that renders as something else.

If you are feeding PDFs to an LLM or a retrieval pipeline, these are not cosmetic. A sentence cut
across two columns is a sentence nobody can retrieve, and a stray heading splits a section in two.

## What it does about them[​](https://mindmax.me/#what-it-does-about-them)

The engine takes the text layer that pdf.js already gives you and rebuilds structure from it.

| Problem | What the engine does |
| --- | --- |
| Columns | Detects column gutters, including three-column pages, and reads the page column by column; a full-width block splits the page into bands. |
| Headers, footers, page numbers | Uses the PDF&#x27;s own /Artifact tags where they exist, and geometric repetition across pages where they don&#x27;t. |
| Paragraphs | Groups glyphs into lines by baseline and lines into paragraphs by spacing; rejoins words hyphenated across a break. |
| Headings | Decides from size relative to the page&#x27;s own body text, numbering and spacing, and merges a wrapped title back into one heading. |
| Tables | Draws them from geometry, and refuses to treat formulas or narrow columns as tables. |
| Links | Restores hyperlinks only where the PDF stores a link annotation. Nothing is invented. |
| Special characters | Escapes only characters that came from the PDF, never the Markdown the assembler wrote, so your $5 stays $5. |

Two other design choices matter more than they look.

**It is local by construction.** The package makes no network call. In a browser the file never
leaves the tab, which is the reason you can point it at contracts, papers under embargo or
internal reports.

**The output is addressable.** The library also exports `buildPdfTextMap`, which records where
every page and every text item landed in the final string. A reader can highlight a sentence in
the PDF and get the same sentence back by offset, instead of searching for text and hoping it
appears once. If you are building citation, highlighting or RAG-with-sources, that is the part
that is hard to retrofit.

The numbers: 25.1 KB gzipped, about 4,100 lines of TypeScript, 170 tests at 97.3% statement
coverage.

## What it refuses to do[​](https://mindmax.me/#what-it-refuses-to-do)

We would rather say where it stops than let you find out.

- **No OCR.** A scanned page has no text layer. The library reports the page as scanned or empty;
it does not quietly call a model.

- **No equation recovery.** A PDF stores positioned glyphs, not formulas, so `x²` arrives as
`x2`. The library detects pages where this happened and reports how many.

- **No images.** Figures stay in the PDF.

- **No upload.** Nothing leaves the machine.

## Tested in public, with the failures listed[​](https://mindmax.me/#tested-in-public-with-the-failures-listed)

Sixteen of the 22 corpus documents are CC BY, five are US federal works and one is an official
Chinese government document, so every one can be redistributed and the exact Markdown each
produced lives in the repository. Anyone can re-run the whole thing. Each document has a pass/fail
list of assertions, and 120 of 120 pass.

Alongside them sit **17 known failing checks across 10 documents**, expected to fail and marked as
such, so CI stays green while the defect stays visible. The [full results table and defect list](https://mindmax.me/open-source/pdf-markdown/benchmark/) is generated from the test run, with no hand-typed
figure.

The ones most likely to affect you:

- **Justified or letter-spaced Chinese text can get stray spaces** inserted between characters.

- **Fonts without a Unicode mapping** produce `�` for digits or `!` for full-width spaces, and the
health check does not always flag the page.

- **Magazine-style layouts**: a sidebar can be read into the body text or a list.

- **A text column next to a table** can be absorbed into the table, and a table header row can
merge into the paragraph beside it.

- **Heading levels can invert** when a cover page uses up levels one to five, and numbered
sections are not always recognised as headings.

Every one of these is a document with a failing assertion in the repository. That is also the
whole contribution model.

## Help us make it better[​](https://mindmax.me/#help-us-make-it-better)

If a PDF converts badly for you, the most useful thing you can send is **the document, or a
public one like it, and one sentence about what right would look like.** That becomes a corpus
case with a failing assertion; the test goes red, the code changes until it goes green, and the
case stays forever. A report with a PDF attached is worth more than a long description of one.

- **Report a bad conversion:** [open a corpus case](https://github.com/woodpeng/mindmax-pdf-markdown/issues/new?template=corpus_case.yml)

- **Fix one:** read [CONTRIBUTING](https://github.com/woodpeng/mindmax-pdf-markdown/blob/main/CONTRIBUTING.md), pick any of the five above, and send a pull request. The target is to grow the corpus to 40 documents.

- **Ask or propose:** [Discussions](https://github.com/woodpeng/mindmax-pdf-markdown/discussions)

- **Just try it:** [the browser playground](https://woodpeng.github.io/mindmax-pdf-markdown/) needs no install.

## What we are committing to[​](https://mindmax.me/#what-we-are-committing-to)

Published on the repository, in the repository&#x27;s own words rather than ours:

- **Security reports get a first response within 72 hours** ([SECURITY](https://github.com/woodpeng/mindmax-pdf-markdown/blob/main/SECURITY.md)).

- **Contributors are promoted.** Three merged pull requests, or accepted corpus cases, are enough
to apply for triager rights ([GOVERNANCE](https://github.com/woodpeng/mindmax-pdf-markdown/blob/main/GOVERNANCE.md)).

- **The project survives us.** If the owner is unreachable for 90 days, maintainers can fork and
carry on.

- **MindMax depends on the same package.** A fix for the app is a release here first.

Two honest caveats. The repository is still young, and the publishing credentials are currently
held by one owner, which is exactly what the 90-day rule is there to cover.

## What comes next[​](https://mindmax.me/#what-comes-next)

The engine is the first piece, not the last. The Word export engine behind our
[Markdown to Word](https://mindmax.me/tools/markdown-to-word/), LaTeX to Word and ChatGPT to Word tools is the next
candidate. It needs three things before we open it: no dependency on our internal core, its own
tests with round-trip samples, and a public API we are willing to keep. We are announcing it now
and giving no date. What is already open, and where its boundary is, is on the
[open source page](https://mindmax.me/open-source/).

## FAQ[​](https://mindmax.me/#faq)

### Is @mindmax/pdf-markdown free for commercial use?[​](https://mindmax.me/#is-mindmaxpdf-markdown-free-for-commercial-use)

Yes. It is Apache-2.0. The MindMax apps themselves are proprietary; the engine is not.

### Does it work on scanned PDFs?[​](https://mindmax.me/#does-it-work-on-scanned-pdfs)

No, and it says so. A page without a text layer is reported as scanned or empty instead of being
sent to an OCR model. MindMax&#x27;s desktop app has a separate, user-started OCR step.

### Does it send my PDF anywhere?[​](https://mindmax.me/#does-it-send-my-pdf-anywhere)

No. The package makes no network call, and in a browser the file stays in the tab.

### Does it run in Node and in the browser?[​](https://mindmax.me/#does-it-run-in-node-and-in-the-browser)

Yes, on Node 18 or newer and in browsers. You pass it an opened pdf.js document, so it never
bundles a second copy of pdf.js.

### Can I use the output for RAG or an LLM pipeline?[​](https://mindmax.me/#can-i-use-the-output-for-rag-or-an-llm-pipeline)

That is a main use. Columns are read in order, page furniture is removed, and `buildPdfTextMap`
gives you offsets back into the source pages for citations.

### How do I report a PDF that converts badly?[​](https://mindmax.me/#how-do-i-report-a-pdf-that-converts-badly)

Open a [corpus case](https://github.com/woodpeng/mindmax-pdf-markdown/issues/new?template=corpus_case.yml)
with the document or a public equivalent and a sentence describing the correct output.

If you would rather read the code than this post, start at
[the repository](https://github.com/woodpeng/mindmax-pdf-markdown). If a defect on that list is one
you have been fighting too, we would be glad to fix it with you.

**Tags:**

- [Notes](https://mindmax.me/blog/tags/notes/)
- [Local-first](https://mindmax.me/blog/tags/local-first/)
