Skip to main content

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

· 8 min read
The MindMax team
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 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.

ProblemWhat the engine does
ColumnsDetects 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 numbersUses the PDF's own /Artifact tags where they exist, and geometric repetition across pages where they don't.
ParagraphsGroups glyphs into lines by baseline and lines into paragraphs by spacing; rejoins words hyphenated across a break.
HeadingsDecides from size relative to the page's own body text, numbering and spacing, and merges a wrapped title back into one heading.
TablesDraws them from geometry, and refuses to treat formulas or narrow columns as tables.
LinksRestores hyperlinks only where the PDF stores a link annotation. Nothing is invented.
Special charactersEscapes 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 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​

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:

  1. Justified or letter-spaced Chinese text can get stray spaces inserted between characters.
  2. Fonts without a Unicode mapping produce � for digits or ! for full-width spaces, and the health check does not always flag the page.
  3. Magazine-style layouts: a sidebar can be read into the body text or a list.
  4. 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.
  5. 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.

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.

Cite this page