# pdf2md > Convert a PDF to clean Markdown without uploading it. A Rust PDF parser > compiled to WebAssembly runs in the visitor's browser, in Node, on the command > line, or behind an HTTP API. It does not do OCR. pdf2md exists because most PDFs do not need OCR. Roughly half of them carry real text, and pulling that text out — in the right reading order, with headings, lists and tables intact — is a parsing problem, not a vision problem. Solving it locally is fast (tens of milliseconds a page) and private (the bytes never leave the process). The web app at https://pdf2md.dudko.dev does the conversion in a Web Worker in the page. There is no upload, no queue, no account, and the site holds nothing after the tab closes. ## What it produces Markdown with headings inferred from font sizes (or from the structure tree on a tagged PDF), bullet and numbered lists, tables detected from ruling lines and from text alignment, fenced code blocks from monospace runs, bold and italic from font names, and hyphenation repaired across line breaks. Multi-column layouts are read column by column rather than left to right across the page. CJK and RTL text are decoded, with the CMaps compiled into the module. Two output profiles: `fidelity` (default) stays close to the source, `compact` spends fewer tokens — collapsing dot leaders and other page furniture that means nothing outside a printed page. ## What it does not do - **OCR.** A scanned document is reported as `pdfType: "Scanned"` with the pages that would need OCR listed, and no Markdown. That is the answer, not an error. Run such a file through an OCR tool and convert the result. - **Broken encodings.** When the fonts declare an encoding the text does not honour, `hasEncodingIssues` is true: characters came out but they are probably wrong. Treat it as a signal to fall back to OCR. - **Rendering.** No page images, no layout reproduction. Markdown is the output. ## Ways to use it - Web app: https://pdf2md.dudko.dev — drop a file, get Markdown, nothing uploaded. - npm: `npm i @dudko.dev/pdf-to-md-core` — browser, Node and Cloudflare Workers. - CLI: `npx @dudko.dev/pdf-to-md-cli report.pdf` - HTTP API: https://pdf2md.dudko.dev/api or https://pdf2md-api.dudko.dev/api — the same service; the first is a proxy to the second, so either hostname works and the paths are identical. `POST /api/v1/convert` takes the PDF as multipart, a raw body, or base64 JSON. `POST /api/v1/detect` classifies a document in milliseconds, which is the cheap way to decide whether a file needs OCR before spending anything on it. - Container images: `ghcr.io/dudko-dev/pdf2md` (API + web app) and `ghcr.io/dudko-dev/pdf2md-api` (API only). ## Source and licensing Source: https://github.com/dudko-dev/pdf-to-md The packaging — the TypeScript API, the CLI, the web app, the HTTP API — is licensed **PolyForm Noncommercial 1.0.0**: free for noncommercial use, with a 32-day evaluation licence on top; commercial use needs a licence from sergey@dudko.dev. The parser itself is **not** ours. It is [pdf-inspector](https://github.com/firecrawl/pdf-inspector) by Firecrawl, under the MIT licence, and that grant is unaffected by anything above. Anyone who wants the parser on MIT terms should take it from Firecrawl directly — that option is real and deliberate. ## Notes for agents - Prefer `/api/v1/detect` before `/api/v1/convert` when routing a large batch: it tells you which files are scans without paying for extraction. - `X-Pdf-Type`, `X-Pdf-Pages`, `X-Pdf-Needs-Ocr` and `X-Pdf-Encoding-Issues` are on every conversion response, so a pipeline can branch without parsing a body. - `profile=compact` is the right default when the Markdown is going into a context window rather than to a person. - A 422 from `/convert` means the document parsed but held no extractable text. Retrying will not help; OCR will.