Skip to main content

The PDF → Markdown engine, in the open

The converter on the PDF to Markdown page is not a service that happens to be free. It is a TypeScript library running in your browser, published as @mindmax/pdf-markdown under Apache-2.0, and it is the same code that runs inside MindMax when you open a PDF there. This page says what it does, what it deliberately does not do, and what stops it from quietly getting worse.

Package@mindmax/pdf-markdown v0.1.0
Sourcegithub.com/woodpeng/mindmax-pdf-markdown
LicenceApache-2.0 — the engine, not the MindMax app; see where the licence stops
Runtime dependencies0
Shipped size25.1 KB gzipped
Runs onNode 18+ and any browser; no fs, no window
Guards22 documents, 170 tests, 97.3% statements

What it rebuilds​

Copying text out of a PDF gets you the words in the order the producer wrote them to the file, which is rarely the order a person reads them. This library works from the other direction: it takes the text items pdf.js reports, with their positions, and treats the layout as the source of structure.

  • Reading order. Two-column papers are read left column first. Text that wraps across a page break stays one paragraph, and a paragraph broken for a column is joined back together.
  • Headings. Levels come from type size, weight and numbering (3.1, 第X章, Chapter 4), refined by the PDF's own bookmark outline when it has one, and by the tagged structure tree when it does not.
  • Tables. Rows and columns are recovered from where the text sits — gutters, alignment, repeated column starts — and written as Markdown tables rather than shredded prose.
  • Lists and links. Bullets become -, numbered items keep their numbers, and a link that a word points to inside the PDF is restored. Where the PDF stores no link annotation, nothing is invented.
  • Page furniture. Running headers, footers, page numbers and watermarks are removed. A tagged PDF marks these as /Artifact and that is used first; geometry is the fallback for the majority of PDFs that ship no tags at all.

The string that comes out is also the string everything anchors into. buildPdfTextMap records where every page starts and where every text item landed in it, so a reader can highlight a sentence in the PDF and get the same sentence back — by offset, not by searching for the text again and hoping it appears once.

What it refuses to do​

RefusesWhy
OCR a scanned pageA scan has no text layer. Anything called OCR here would be a model call, a bill and a different product. The library reports that the page is empty instead.
Recover equationsA PDF stores positioned glyphs, not formulas. x² arrives as x2. The library detects pages where this happened and says how many, rather than pretending the LaTeX is recoverable without a model.
Carry images acrossFigures stay in the PDF. Extracting them is a different job with a different failure mode.
Upload anythingThe package makes no network call. In a browser the file does not leave the tab.

These are the four things people ask for most, so they are the four things stated plainly. A converter that claims two of them and does neither is worse than one that says where it stops.

How a page becomes Markdown​

StepWhat happens
1. CollectOne pdf.js pass per page gathers text items, their transforms, and the /Artifact flags on tagged PDFs.
2. Drop furnitureArtifact-tagged runs are blanked, then geometric repeats across pages (headers, footers, page numbers) are removed. Items keep their indices, so offsets stay valid.
3. Rebuild linesGlyphs are grouped into lines by baseline, and lines into paragraphs by spacing and indentation. Hyphenated words across a break are rejoined.
4. Order the pageColumn gutters are detected and the page is read column by column; a full-width block splits the page into bands.
5. Classify blocksHeadings from size and numbering, lists from markers, tables from geometry, footnotes from size and rules.
6. EmitMarkdown, plus the map of what came from the PDF and what the assembler wrote. Nothing here reformats text to look tidier, because that would move every offset.

Two ways to call it​

Most callers want a document. Some want the structural text and the mapping, and those two should not share one API that does both badly.

import { convertPdfToMarkdown } from '@mindmax/pdf-markdown';

// You already have an opened pdf.js document.
const result = convertPdfToMarkdown(pdfDocument);

if (result.ok) {
result.markdown; // the .md file
result.links; // how many hyperlinks were restored
result.textLayerIssues; // pages with equations or unmapped glyphs, or null
} else {
result.reason; // 'scanned' | 'empty'
}
import { buildPdfTextMap } from '@mindmax/pdf-markdown';

// You want the canonical text, not a file.
const map = buildPdfTextMap(pagesOfTextItems);
map.text; // the Markdown-shaped text every anchor addresses
map.pages; // per page: where it starts, and each item's range inside it

CANONICAL_TEXT_VERSION is exported for the same reason those offsets exist: if you store them, you need to know when a version bump could move them. It changes only when map.text changes for the same input — Markdown rendering can change without bumping it.

Nothing above imports pdf.js. The Pdf*Like types are structural, so a browser build, a Node legacy build and a test stand-in all satisfy them, and the package never pulls a second copy of pdf.js into your bundle.

Why the corpus matters​

The repository carries 22 real documents and the exact Markdown each one produced. Sixteen are CC-BY, five are US federal works, one is an official Chinese government document — every one is redistributable, so the snapshots can live in the repository and the tests run anywhere.

When a PDF converts badly, the fix is not "adjust the heuristic and hope". The document joins the corpus with a failing assertion describing what is wrong; the test is red; the code changes until it is green; the case stays forever. That is the whole contribution model, and it is why a bug report with a document attached is worth more than a long description of one.

The results table is that corpus with its numbers: what each document produced, how many assertions check it, and the defects still open. It is rendered from the JSON the test run writes, so there is no hand-typed figure to go stale.

Who maintains it​

The MindMax team maintains the engine in public, and MindMax itself depends on the published package rather than a private copy, so a fix for the app is a release here first. The licence covers this engine, not the app around it; the open-source page states that line once, with the commitments the team makes on security, governance and continuity.

Trying it, and contributing​

The shortest path to seeing the output is the converter itself: it runs this library in your browser, with no install and no account. A bare playground takes a PDF and prints the Markdown, with nothing else around it; its source is the repository's examples/browser directory.

Two contribution rules matter more than the rest. A change that moves the output of an existing corpus document needs a reason written in the diff, and a change that fixes one document must not break another — CI runs every document on every pull request. Everything else is in CONTRIBUTING.md.

If you only want to report a bad conversion, the corpus case template asks for the one thing that makes it fixable: the document, or a page from it you have the right to share.

Cite this page