The Problem

Converting PDFs to editable Markdown is usually lossy: tables break, formulas become images, and layout is discarded. Most tools use rule-based extraction that fails on complex documents. MarkPDFDown instead sends rendered PDF pages to a multimodal LLM and asks it to transcribe what it sees, which preserves structure at the cost of an API call per page.

What This Does

The package converts PDFs and images to Markdown using OpenAI or OpenRouter models through LiteLLM. Core logic lives in src/markpdfdown/core/: file_worker.py renders PDF pages to images, llm_client.py wraps the API call, and utils.py handles Markdown cleanup and input validation. The CLI in src/markpdfdown/cli.py supports both file arguments and stdin piping.

Configuration is environment-variable driven via src/markpdfdown/config.py — model name, temperature, max tokens, retry count. A .env.sample file documents the expected keys.

How It Is Wired

Execution starts at main in src/markpdfdown/cli.py:98, which parses arguments, validates them, then dispatches to convert_from_file or convert_from_stdin in src/markpdfdown/main.py. Those call convert_to_markdown, which uses FileWorker to render pages to images, then LLMClient.completion to get the transcription. The shortest path to an external effect is main -> convert_from_file -> convert_to_markdown -> os.makedirs — two hops from entry to filesystem write.

LLMClient is the hub: called from 14 places. create_parser, detect_file_type, and validate_page_range are each called from 10+ places, so changes to their signatures break a wide surface. The import graph is acyclic with 12 edges across 17 modules — no circular dependencies, which keeps refactoring straightforward.

The module graph shows core/utils.py as a stable leaf (3 callers, 0 dependencies) and core/llm_client.py similarly stable. main.py sits in the middle with instability 0.67, meaning it both depends on and is depended on — the natural place to add features like new output formats.

How To Use It

# Install with uv (recommended, uv.lock is present)
uv sync
uv pip install -e .

# Or with pip
pip install -e .

# Configure
cp .env.sample .env
# Edit .env: MODEL_NAME, OPENAI_API_KEY (or OPENROUTER_API_KEY)

# Convert a PDF
markpdfdown --input document.pdf --output output.md

# Convert specific pages
markpdfdown --input document.pdf --output output.md --start 1 --end 10

# Pipe mode
markpdfdown < document.pdf > output.md

A Dockerfile is present for containerized use.

Real-World Use

Batch-converting a document archive: loop over PDFs, call markpdfdown per file, store the Markdown in a content repository. The pipe mode makes this scriptable without temp files: cat report.pdf | markpdfdown > report.md. Page-range selection lets you handle large documents in chunks to stay within context windows.

Code Health & Issues

Static analysis found 8 issues (0 critical, 2 high, 5 medium, 1 low):

  • High - Third-party GitHub Actions pinned to tags, not commit SHAs — .github/workflows — a moved tag can run arbitrary code with your CI secrets
  • High - CI has 16 test files but no workflow runs them — .github/workflows — the green check proves nothing
  • Medium - No permissions block on workflows that reference secrets — .github/workflows/changelog.yml
  • Medium - No Dependabot/Renovate configured — dependencies go unpatchable
  • Medium - Docker base image python:3.9-slim not pinned by digest — Dockerfile
  • Medium - No dependency vulnerability scan in CI — .github/workflows
  • Medium - No non-root USER in the Dockerfile — container runs as root
  • Low - No timeout-minutes on workflow jobs — a wedged run blocks the queue

One code-quality finding: src/markpdfdown/core/llm_client.py has nesting depth of 6 — extract guard clauses to flatten.

A uv.lock file is present, so builds are reproducible despite no lockfile warning in the analysis.

The Bottom Line

A clean, small codebase (17 Python files) with a sensible architecture and actual tests — though CI doesn't run them. The LLM-based approach is genuinely better than rule-based PDF extraction for complex documents. The main risks are supply-chain: unpinned CI actions and base image. Suitable for anyone needing high-fidelity PDF-to-Markdown conversion and willing to pay API costs per page.