We open-sourced our PDF-to-Markdown engine, and published what it still gets wrong
@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 and inside the MindMax apps. It is not a demo build of something private.
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
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's own /Artifact tags where they exist, and geometric repetition across pages where they don'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'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
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 asx2. 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
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 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
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
- Fix one: read CONTRIBUTING, 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
- Just try it: the browser playground needs no install.
What we are committing to
Published on the repository, in the repository's own words rather than ours:
- Security reports get a first response within 72 hours (SECURITY).
- Contributors are promoted. Three merged pull requests, or accepted corpus cases, are enough to apply for triager rights (GOVERNANCE).
- 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
The engine is the first piece, not the last. The Word export engine behind our 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.
FAQ
Is @mindmax/pdf-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?
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's desktop app has a separate, user-started OCR step.
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?
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?
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?
Open a corpus case 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. If a defect on that list is one you have been fighting too, we would be glad to fix it with you.