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:

  1. Confirm the plugin is installed in the same environment as MarkItDown.
  2. Inspect the installed-plugin list with markitdown --list-plugins.
  3. Confirm your application enables plugins and supplies the documented client and model configuration.
  4. 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

  1. versioned installation instructions
  2. v0.1.8 PDF converter source
  3. complete score file and metric definitions
  4. saved output
  5. rotated-header output
  6. markitdown-ocr plugin
  7. current API
  8. Content Understanding integration guide
  9. Marker
  10. equation response snapshot
  11. run log
  12. Markovo batch record
  13. full historical table