MarkItDown for PDF: Extraction and OCR Routes
MarkItDown can extract useful Markdown from a PDF, but the result depends on which converter you run. The built-in local PDF path, an optional OCR plugin, and an Azure-backed conversion are different configurations. Installing PDF support does not mean you have configured OCR for scanned pages.
Start with the symptom: are words missing, is reading order wrong, or have table labels lost their relationship to values? Those problems need different checks. A nonempty Markdown file can still omit an entire scanned page.
This guide combines official documentation with an archived synthetic-PDF experiment dated October 6, 2026. We reviewed those artifacts for this revision; we did not rerun conversions. Markovo is the service we build, and its recorded results below include both successful extraction and a weaker equation result.
First, identify the failure
| What you see | What to check in the source and output | Next step |
|---|---|---|
| A dependency error before any output | Installed MarkItDown version and PDF extra in the active Python environment | Fix installation before changing extraction strategy |
| Empty output, or missing sentences on one page | Whether those words exist as selectable PDF text or only as pixels | Evaluate an OCR-enabled path |
| Paragraphs alternate between columns | Several complete sentences and paragraph boundaries, not isolated keywords | Test a layout-aware extraction path |
| A pipe table exists but labels are reversed or missing | Each column's full header, units, and corresponding values | Repair or change table extraction; syntax alone is insufficient |
| An equation looks plausible but differs from the PDF | Operators, superscripts, subscripts, and the rendered expression | Review the expression against the source |
The ability to select text is a useful first check, not a certificate of a good text layer. A PDF can contain incomplete or incorrect existing OCR. Check difficult pages individually, particularly in documents mixing digital text and scans.
Establish a reproducible local baseline
The historical local run used MarkItDown 0.1.8, Python 3.11.14, plugins disabled, and no Azure endpoint. To prepare a separate environment for that version:
python -m venv .venv
# Activate the virtual environment using the command for your shell.
python -m pip install 'markitdown[pdf]==0.1.8'
python -m pip show markitdown
These installation commands are setup instructions, not a new installation performed for this revision. The PDF extra installs format dependencies; other extras and integrations have separate requirements. See the versioned installation instructions.
The archived runner used this conversion call:
from pathlib import Path
from markitdown import MarkItDown
result = MarkItDown(enable_plugins=False).convert_local("input.pdf")
Path("output.md").write_text(result.markdown, encoding="utf-8")
Keep the unedited output. Record package versions, input identity, flags, and any errors before trying a different route. Otherwise, a successful second attempt cannot tell you whether a version, dependency, or extraction option made the difference.
What the built-in PDF converter does
In the v0.1.8 PDF converter source, a form-style helper analyzes word positions. When no page qualifies for that path, the converter uses pdfminer extraction for the document. When form-style pages are found, it combines the collected page results. Its exception path also falls back to pdfminer.
The form helper uses the first collected table row as a Markdown header. That matters when a real header was not recognized: the output may contain a syntactically valid table whose first data row has been promoted to a header. This source inspection explains a possible mechanism; it does not trace every observed failure to one branch.
What survived in the October 6 experiment
The corpus contained eight synthetic PDFs: single-column prose, a simple invoice table, merged headers, rotated headers, two columns, an image-only page, mixed text/table/scan pages, and linear textual equations.
| Fixture | MarkItDown 0.1.8 local result | What it establishes |
|---|---|---|
| Single-column prose | Prose recall 1.0; reading-order score 1.0 | The expected text and paragraph anchors survived this fixture |
| Simple invoice table | Row score 1.0; header score 1.0; pipe table detected | This table shape worked; not all PDF tables |
| Two-column prose | Prose recall 0.1111; reading-order score 0.8 | Some anchors survived even though complete sentences were damaged |
| Merged and rotated headers | Row score 1.0; header score 0.0 | Values being present did not establish usable headers |
| Image-only page | Scan-text recall 0.0 | The local baseline did not recover the expected scan sentences |
| Mixed document | Page-anchor coverage 0.6667; scan-text recall 0.0 | The text and table pages did not make the document complete |
| Linear equation fixture | Normalized expression-string recall 1.0 | Expected strings survived; spatial math reconstruction was not tested |
These values come from the complete score file and metric definitions. A score of 1.0 means all checks for that metric passed, not that the entire document was lossless.
Broken columns: compare sentences, not keywords
On the two-column fixture, the MarkItDown output alternated fragments from opposite columns. The raw pdfplumber text baseline had the same measured problem. PyMuPDF4LLM 1.28.2, using the recorded default call, achieved 1.0 for both prose recall and paragraph-anchor order on this fixture. Its saved output preserves the consecutive paragraphs.
That makes the tested PyMuPDF4LLM call a useful alternative to evaluate for a similar layout. It does not establish that it always handles columns, or that MarkItDown always fails on them. Use several source sentences crossing the suspected boundary to compare the outputs.
Broken headers: a table can pass the wrong test
The rotated-header output placed reversed label text outside the table and used a data row as its header. The row score still passed because the scorer only required the expected cell strings to appear on the same output line.
For a grouped header, verify the full meaning of each column. If “Price” spans “Unit” and “Total,” a downstream representation must keep that relationship, for example through explicit flattened labels or a table format that supports spans. Do not infer it from the presence of all four words somewhere in the output.
Missing image text: configure OCR explicitly
The optional markitdown-ocr plugin is separate from the built-in PDF converter. Its documented setup requires the plugin, enabled plugin loading, and an LLM client and vision model. The versioned documentation says that without an LLM client, OCR is skipped and the standard converter is used.
For troubleshooting:
- Confirm the plugin is installed in the same environment as MarkItDown.
- Inspect the installed-plugin list with
markitdown --list-plugins. - Confirm your application enables plugins and supplies the documented client and model configuration.
- Test one page with a known visible sentence, then verify that sentence in the output.
Enabling a flag alone is not proof that image text was read. An output containing only page labels is not recovered body text. A visible sentence must have corresponding words in the result.
The plugin was not run in the historical experiment or for this revision. There is no plugin result here to compare with the local baseline, and no evidence here that it fixes the merged-header or column fixtures. If the selected client calls a hosted model, account for document transmission and provider charges before running it.
The archived default PyMuPDF4LLM call also omitted the scan sentences. That is not a claim that PyMuPDF4LLM lacks OCR: its current API exposes OCR options, with behavior depending on the selected layout path and available dependencies. A configured OCR run was not evaluated in this corpus.
Damaged layout: distinguish the Azure backends
MarkItDown also supports Azure Document Intelligence and Azure Content Understanding. Both are service-backed configurations, not the local baseline with a different filename.
Microsoft's Content Understanding integration guide documents an Azure resource, endpoint configuration, the relevant dependency extra, and a backend that can return layout-aware Markdown and structured fields. Azure Document Intelligence is a separate integration. A PDF extra, an OCR plugin, a Document Intelligence endpoint, and a Content Understanding endpoint should not be treated as interchangeable settings.
Neither Azure path was tested here. Consider them candidates when you need their documented layout or field-extraction capabilities, then test the unresolved pages. Cloud processing does not remove the need to verify missing pages, column order, header assignments, or numbers. It also changes where the document is processed and introduces service charges.
If local model processing is preferable, Marker is another candidate with documented layout, OCR, and mathematical output. Marker was not run in this eight-PDF experiment. Choose a supported version and hardware configuration rather than treating a project's feature list as a result on your document.
Where Markovo fits, and where its output still needs review
The cached Markovo results recovered the expected scan sentences in both the image-only and mixed fixtures. They also passed the row and leaf-header checks on the special-header fixtures. Those PDFs produced HTML tables, not pipe tables. If your Markdown consumer removes HTML, useful table content can disappear downstream.
The equation result was weaker. Under the saved normalized expression-string metric, Markovo scored 0.5, compared with 1.0 for MarkItDown and raw pdfplumber and 0.75 for PyMuPDF4LLM. This was a small linear-expression fixture, not a general mathematical benchmark. The separate exact-string metric gave Markovo 0.0, illustrating how a format-sensitive matcher can penalize LaTeX output. Neither metric establishes mathematical equivalence.
The equation response snapshot reports quality_score=0.9, despite that weaker expression result. A service quality field cannot replace source comparison. It also reports billing_metrics.pagesOcr=0 and job.pages_ocr=1; these fields must not be used interchangeably or treated as proof of a per-page extraction route. The recorded fields alone do not identify the exact extraction method used on each page.
For a hosted workflow, try the PDF to Markdown converter on a representative document and inspect the downloaded result in your actual destination. Keep the original PDF and verify difficult pages before scaling up.
Evidence, dates, and limits
The historical environment was Python 3.11.14 with MarkItDown 0.1.8, PyMuPDF4LLM 1.28.2, and pdfplumber 0.11.10. The pdfplumber baseline used extract_text, not a tuned table-extraction workflow. The local calls, errors, and timings are in the run log.
The Markovo batch record is dated October 6, 2026. Its entries have from_cache=true and elapsed_s=0.0. Zero is a stored runner value, not instantaneous service conversion. The rate-card identifier describes billing, not a pinned converter implementation. We make no latency comparison.
The scorer checks normalized strings, paragraph anchors, same-line row cells, and same-line leaf headers. It does not certify column semantics, merged-cell spans, every word on a page, or mathematical correctness. The full historical table remains available rather than being presented as a new 2026-10-19 test.
For a broader choice of libraries, see the PDF converter selection guide. For implementation examples beyond MarkItDown, continue with PDF to Markdown in Python. The practical rule is the same: fix missing content at extraction time, and validate relationships before chunking or indexing the Markdown.
Sources
- versioned installation instructions
- v0.1.8 PDF converter source
- complete score file and metric definitions
- saved output
- rotated-header output
- markitdown-ocr plugin
- current API
- Content Understanding integration guide
- Marker
- equation response snapshot
- run log
- Markovo batch record
- full historical table


