{
  "markdown": "# pdfmux\n\n[![CI](https://github.com/NameetP/pdfmux/actions/workflows/ci.yml/badge.svg)](https://github.com/NameetP/pdfmux/actions/workflows/ci.yml)\n[![PyPI](https://img.shields.io/pypi/v/pdfmux)](https://pypi.org/project/pdfmux/)\n[![Python 3.11+](https://img.shields.io/pypi/pyversions/pdfmux)](https://pypi.org/project/pdfmux/)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)\n[![Downloads](https://img.shields.io/pypi/dm/pdfmux)](https://pypi.org/project/pdfmux/)\n\n**Self-healing PDF extraction that flags the pages it can't read instead of dropping them — and now certifies any extractor's output for silent drops.** Open-source LlamaParse alternative for RAG pipelines, MCP server for Claude Desktop, LangChain + LlamaIndex loaders.\n\n> pdfmux extracts PDFs and checks its own work — and now certifies any extractor's, telling you which pages it silently dropped. Free, MIT. Patent-pending method. `pip install pdfmux`.\n\n**Two jobs, one tool:**\n\n- **Self-healing extraction.** The only PDF extractor that audits its own output. Catches blank pages, scrambled columns, broken tables — re-extracts them with a stronger backend, and flags what it still can't read instead of silently dropping it. So your LLM gets clean data, not silent garbage. Routes each page to the best of 7 built-in extraction backends + BYOK LLM fallback (Gemini / Claude / GPT-4o / Ollama). One CLI. One API. Zero config.\n- **[Certify Anything](#certify-anything) — new in v1.8.1.** `pdfmux verify` audits *any* extraction engine's output against the source PDF — Reducto, Mistral OCR, LlamaParse, Docling, your in-house parser — and tells you which pages it silently dropped. Free, MIT, patent-clean.\n\n<p align=\"center\">\n  <img src=\"demo.svg\" alt=\"pdfmux terminal demo\" width=\"700\" />\n</p>\n\n```\nPDF ──> pdfmux router ──> best extractor per page ──> audit ──> re-extract failures ──> Markdown / JSON / chunks\n            |\n            ├─ PyMuPDF         (digital text, 0.01s/page)\n            ├─ OpenDataLoader  (complex layouts, 0.05s/page)\n            ├─ RapidOCR        (scanned pages, CPU-only)\n            ├─ Docling         (tables, 97.9% TEDS)\n            ├─ Surya           (heavy OCR fallback)\n            ├─ Marker          (academic papers, neural)\n            ├─ Mistral OCR     ($0.002/page, 96.6% tables)\n            └─ YOUR LLM        (Gemini / Gemma 3 / Claude / GPT-4o / Ollama / Mistral — BYOK via YAML)\n```\n\n## Install\n\n```bash\npip install pdfmux\n```\n\nThat handles digital PDFs. **For any real-world batch, install `pdfmux[ocr]` too** — almost every directory of PDFs has at least one scan, and without OCR those pages return empty text:\n\n```bash\npip install \"pdfmux[ocr]\"             # ⭐ recommended — RapidOCR for scanned pages (~200MB, CPU)\n```\n\nOther backends, by document type:\n\n```bash\npip install \"pdfmux[tables]\"          # Docling — table-heavy docs (~500MB)\npip install \"pdfmux[opendataloader]\"  # OpenDataLoader — complex layouts (Java 11+)\npip install \"pdfmux[marker]\"          # Marker — neural extraction for academic papers\npip install \"pdfmux[llm]\"             # Gemini fallback (default LLM)\npip install \"pdfmux[llm-claude]\"      # Claude (Sonnet / Opus)\npip install \"pdfmux[llm-openai]\"      # GPT-4o family\npip install \"pdfmux[llm-ollama]\"      # Ollama (any local model)\npip install \"pdfmux[llm-mistral]\"     # Mistral OCR API ($0.002/page)\npip install \"pdfmux[llm-all]\"         # all LLM providers (incl. Gemma via Gemini key)\npip install \"pdfmux[watch]\"           # `pdfmux watch <dir>` auto-convert on change\npip install \"pdfmux[all]\"             # everything\n```\n\nRequires Python 3.11+.\n\n## Quick Start\n\n### CLI\n\n```bash\n# zero config — just works\npdfmux convert invoice.pdf\n# invoice.pdf -> invoice.md (2 pages, 95% confidence, via pymupdf4llm)\n\n# RAG-ready chunks with token limits\npdfmux convert report.pdf --chunk --max-tokens 500\n\n# cost-aware extraction with budget cap\npdfmux convert report.pdf --mode economy --budget 0.50\n\n# schema-guided structured extraction (5 built-in presets)\npdfmux convert invoice.pdf --schema invoice\n\n# BYOK any LLM for hardest pages\npdfmux convert scan.pdf --llm-provider claude\n\n# use a built-in or saved profile (invoices, receipts, papers, contracts, bulk-rag)\npdfmux convert invoice.pdf --profile invoices\n\n# predict cost before running anything\npdfmux estimate big-report.pdf --llm-provider gemini\n\n# stream pages as NDJSON as they finish (great for long documents)\npdfmux stream report.pdf --quality high\n\n# auto-convert any new PDFs that land in a folder\npdfmux watch ./inbox/ -o ./output/\n\n# diff two extractions side-by-side\npdfmux diff old.pdf new.pdf\n\n# batch a directory — writes manifest.json with per-doc confidence\npdfmux convert ./docs/ -o ./output/\n\n# CI mode: fail the run if any document is below 0.20 confidence\npdfmux convert ./docs/ -o ./output/ --strict --min-confidence 0.20\n\n# pre-flight a directory: which extras do you actually need for THIS batch?\npdfmux doctor --check ./docs/\n\n# results are cached by file hash — re-runs are instant; bypass with --no-cache\npdfmux convert report.pdf --no-cache\npdfmux convert report.pdf --clear-cache\n```\n\n### Python\n\nFor batch processing, use `batch_extract()` — not a `subprocess.run(['pdfmux', ...])` loop. Same pipeline, no per-file process spawn, handles non-ASCII filenames:\n\n```python\nimport pdfmux\nfrom pathlib import Path\n\n# Batch extract — yields (path, result) tuples as each PDF completes.\npdfs = list(Path(\"./inbox\").glob(\"*.pdf\"))\nfor path, result in pdfmux.batch_extract(pdfs, quality=\"standard\"):\n    if isinstance(result, Exception):\n        print(f\"FAILED {path.name}: {result}\")\n        continue\n    if result.confidence < 0.50:\n        print(f\"REVIEW {path.name} ({result.confidence:.2f})\")\n    else:\n        print(f\"OK     {path.name} ({result.confidence:.2f})\")\n\n# Single-file helpers.\ntext   = pdfmux.extract_text(\"report.pdf\")             # markdown string\ndata   = pdfmux.extract_json(\"report.pdf\")             # locked schema dict\nchunks = pdfmux.chunk(\"report.pdf\", max_tokens=500)    # RAG-ready chunks\n```\n\n> **Don't wrap pdfmux with your own pypdf/pdfplumber fallback.** pdfmux already routes per page through PyMuPDF → RapidOCR → vision LLM. PyMuPDF tolerates malformed PDFs that pypdf rejects (\"Stream has ended unexpectedly\"), so a downstream pypdf fallback turns recoverable PDFs into failures. Trust the router; check the confidence score on the result.\n\n## Certify Anything\n\n`pdfmux verify` audits **any extraction engine's output** against the source PDF and tells you which pages it silently dropped — not just pdfmux's own extraction. Point it at the output of Reducto, Mistral OCR, LlamaParse, Docling, or your in-house parser and it re-derives the source text with pdfmux's own audit pass, aligns the extraction to it, and scores every page.\n\n**The failure it catches:** a page where the source has real text but the engine returned nothing — while reporting success. That \"silent drop\" is the exact failure that poisons a RAG index without a single error in the logs.\n\n```bash\n# Certify pdfmux's own extraction of a document\npdfmux verify --source report.pdf --engine pdfmux\n\n# Certify ANOTHER engine's output (JSON / Markdown / text)\npdfmux verify --source report.pdf --extracted reducto.json --engine-name reducto\n\n# Batch a whole directory — the \"M pages silently dropped across N docs\" report\npdfmux verify --source ./pdfs/ --extracted ./engine-outputs/ -o certification.json\n\n# CI gate: exit non-zero unless the overall verdict is PASS\npdfmux verify --source report.pdf --extracted out.json --strict\n```\n\nEvery run prints a `PASS` / `REVIEW` / `FAIL` verdict, overall confidence and coverage, and — when it finds them — the silently dropped pages by number:\n\n```\npdfmux verify — report.pdf · engine: reducto\n  FAIL   confidence 71% · coverage 68%\n  reducto: FAIL; 3 page(s) SILENTLY DROPPED (pages 7, 12, 31); overall\n  confidence 71%, coverage 68% across 40 page(s).\n\n❌ 3 page(s) SILENTLY DROPPED: 7, 12, 31\n```\n\nPer page you get a verdict (`pass` / `review` / `fail`), confidence, coverage, alignment, hallucination-risk, and table/heading integrity. Batch mode rolls that up into a single **\"N pages silently dropped across M documents\"** line — the report you run on 100 of your own PDFs to find the silent failures already in your pipeline.\n\n### It works on any engine's output\n\n`--extracted` accepts JSON, Markdown, or plain text (`--extracted-format auto | json | markdown | text`). When the extraction exposes real per-page structure, pdfmux compares page-by-page; when it's a single blob, it falls back to content-presence checks so it never fabricates a \"silent drop\" from a pagination mismatch.\n\n### Python API\n\n```python\nfrom pdfmux import verify_extraction, verify_batch\n\n# Single document → a CertificationManifest\nmanifest = verify_extraction(\"report.pdf\", \"reducto.json\", engine=\"reducto\")\nprint(manifest.verdict)        # \"PASS\" | \"REVIEW\" | \"FAIL\"\nprint(manifest.silent_drops)   # e.g. (7, 12, 31)  — 1-indexed page numbers\nprint(manifest.coverage)       # 0.0–1.0\n\n# Many documents → a BatchCertification (\"M pages dropped across N docs\")\nbatch = verify_batch([(\"a.pdf\", \"a.json\"), (\"b.pdf\", \"b.json\")], engine=\"llamaparse\")\nprint(batch.total_silent_drops, \"pages dropped across\", batch.doc_count, \"docs\")\n```\n\nEach manifest carries a tamper-evident SHA-256 content signature over its canonical body and an embedded, honest limitations list: the certifier is **lexical, not linguistic** — it detects missing and garbled content, not faithful paraphrase or translation.\n\n### MCP\n\n`verify_extraction` is exposed as an MCP tool (the 7th — see [MCP Server](#mcp-server-ai-agents)), so an agent can certify an engine's output in the same session it extracts.\n\n### Free, MIT, patent-clean\n\nCertify Anything reuses only pdfmux's shipped MIT audit layer. It does **not** include, and does not require, the patent-pending decision-trace method — that stays in [pdfmux Cloud/Pro](#license). `pip install pdfmux` gives you the full `verify` command at no cost.\n\nFull reference: **[docs/CERTIFY-ANYTHING.md](docs/CERTIFY-ANYTHING.md)**.\n\n### When you need to prove it to someone else\n\nA local install can audit an extraction, but it cannot *attest* to one — anything it signs, anyone could forge. [pdfmux Cloud](https://app.pdfmux.com/pricing) returns an **Ed25519-signed manifest** over the extraction: your auditor verifies it **offline**, against a published public key, without an account and without trusting pdfmux.\n\n```bash\npdfmux verify-manifest manifest.json      # free, MIT, offline — no account\n```\n\nVerification is free and open forever; only *generation* is paid ($49/mo). That asymmetry is deliberate — you should never need our permission to check our work.\n\nFree tool, no signup: **[app.pdfmux.com/audit](https://app.pdfmux.com/audit)** — upload a PDF and see which pages your current extractor silently dropped. Measured accuracy (and its blind spots) published in [pdfmux-bench](https://github.com/NameetP/pdfmux/blob/feat/pdfmux-bench/pdfmux-bench/VERIFIER-VALIDATION.md).\n\n## Architecture\n\n```\n                           ┌─────────────────────────────┐\n                           │     Segment Detector         │\n                           │  text / tables / images /    │\n                           │  formulas / headers per page │\n                           └─────────────┬───────────────┘\n                                         │\n                    ┌────────────────────────────────────────┐\n                    │            Router Engine                │\n                    │                                        │\n                    │   economy ── balanced ── premium        │\n                    │   (minimize $)  (default)  (max quality)│\n                    │   budget caps: --budget 0.50            │\n                    └────────────────────┬───────────────────┘\n                                         │\n          ┌──────────┬──────────┬────────┴────────┬──────────┐\n          │          │          │                  │          │\n     PyMuPDF   OpenData    RapidOCR           Docling     LLM\n     digital   Loader      scanned            tables    (BYOK)\n     0.01s/pg  complex     CPU-only           97.9%    any provider\n               layouts                        TEDS\n          │          │          │                  │          │\n          └──────────┴──────────┴────────┬────────┴──────────┘\n                                         │\n                    ┌────────────────────────────────────────┐\n                    │           Quality Auditor               │\n                    │                                        │\n                    │   4-signal dynamic confidence scoring   │\n                    │   per-page: good / bad / empty          │\n                    │   if bad -> re-extract with next backend│\n                    └────────────────────┬───────────────────┘\n                                         │\n                    ┌────────────────────────────────────────┐\n                    │           Output Pipeline               │\n                    │                                        │\n                    │   heading injection (font-size analysis)│\n                    │   table extraction + normalization      │\n                    │   text cleanup + merge                  │\n                    │   confidence score (honest, not inflated)│\n                    └────────────────────────────────────────┘\n```\n\n### Key design decisions\n\n- **Router, not extractor.** pdfmux does not compete with PyMuPDF or Docling. It picks the best one per page.\n- **Agentic multi-pass.** Extract, audit confidence, re-extract failures with a stronger backend. Bad pages get retried automatically.\n- **Segment-level detection.** Each page is classified by content type (text, tables, images, formulas, headers) before routing.\n- **4-signal confidence.** Dynamic quality scoring from character density, OCR noise ratio, table integrity, and heading structure. Not hardcoded thresholds.\n- **Document cache.** Each PDF is opened once, not once per extractor. Shared across the full pipeline.\n- **Data flywheel.** Local telemetry tracks which extractors win per document type. Routing improves with usage.\n\n## Features\n\n| Feature | What it does | Command |\n|---------|-------------|---------|\n| Zero-config extraction | Routes to best backend automatically | `pdfmux convert file.pdf` |\n| RAG chunking | Section-aware chunks with token estimates | `pdfmux convert file.pdf --chunk --max-tokens 500` |\n| Cost modes | economy / balanced / premium with budget caps | `pdfmux convert file.pdf --mode economy --budget 0.50` |\n| Schema extraction | 5 built-in presets (invoice, receipt, contract, resume, paper) | `pdfmux convert file.pdf --schema invoice` |\n| Profiles | Save and re-use config; built-ins for invoices/receipts/papers/contracts/bulk-rag | `pdfmux convert file.pdf --profile invoices` |\n| BYOK LLM | Gemini, Gemma 3, Claude, GPT-4o, Ollama, Mistral, any OpenAI-compatible API | `pdfmux convert file.pdf --llm-provider claude` |\n| Cost estimate | Predict spend before running | `pdfmux estimate file.pdf --llm-provider gemini` |\n| Streaming output | NDJSON events page-by-page for long docs | `pdfmux stream file.pdf` |\n| Smart cache | Hash-keyed result cache, 30-day TTL, 1 GB LRU | `pdfmux convert file.pdf` (auto), `--no-cache` to bypass |\n| Watch mode | Auto-convert any PDF added to a folder | `pdfmux watch ./inbox/` |\n| Diff | Compare two extractions | `pdfmux diff a.pdf b.pdf` |\n| Benchmark | Eval all installed extractors against ground truth | `pdfmux benchmark` |\n| Doctor | Show installed backends, coverage gaps, recommendations | `pdfmux doctor` |\n| MCP server | AI agents read PDFs via stdio or HTTP | `pdfmux serve` |\n| Batch processing | Convert entire directories | `pdfmux convert ./docs/` |\n| Page-level streaming API | Bounded-memory page iteration for large files | `for page in ext.extract(\"500pg.pdf\")` |\n| Retry with backoff | Every LLM provider auto-retries with exponential backoff + `Retry-After` | (built-in) |\n\n## CLI Reference\n\n### `pdfmux convert`\n\n```bash\npdfmux convert <file-or-dir> [options]\n\nOptions:\n  -o, --output PATH          Output file or directory\n  -f, --format FORMAT        markdown | json | csv | llm (default: markdown)\n  -q, --quality QUALITY      fast | standard | high (default: standard)\n  -s, --schema SCHEMA        JSON schema file or preset (invoice, receipt, contract, resume, paper)\n  --chunk                    Output RAG-ready chunks\n  --max-tokens N             Max tokens per chunk (default: 500)\n  --mode MODE                economy | balanced | premium (default: balanced)\n  --budget AMOUNT            Max spend per document in USD\n  --llm-provider PROVIDER    LLM backend: gemini | claude | openai | ollama\n  --confidence               Include confidence score in output\n  --stdout                   Print to stdout instead of file\n```\n\n### `pdfmux serve`\n\nStart the MCP server for AI agent integration.\n\n```bash\npdfmux serve              # stdio mode (Claude Desktop, Cursor)\npdfmux serve --http 8080  # HTTP mode\n```\n\n### `pdfmux doctor`\n\n```bash\npdfmux doctor\n# ┌──────────────────┬─────────────┬─────────┬──────────────────────────────────┐\n# │ Extractor        │ Status      │ Version │ Install                          │\n# ├──────────────────┼─────────────┼─────────┼──────────────────────────────────┤\n# │ PyMuPDF          │ installed   │ 1.25.3  │                                  │\n# │ OpenDataLoader   │ installed   │ 0.3.1   │                                  │\n# │ RapidOCR         │ installed   │ 3.0.6   │                                  │\n# │ Docling          │ missing     │ --      │ pip install pdfmux[tables]       │\n# │ Surya            │ missing     │ --      │ pip install pdfmux[ocr-heavy]    │\n# │ LLM (Gemini)     │ configured  │ --      │ GEMINI_API_KEY set               │\n# └──────────────────┴─────────────┴─────────┴──────────────────────────────────┘\n```\n\n### `pdfmux benchmark`\n\n```bash\npdfmux benchmark report.pdf\n# ┌──────────────────┬────────┬────────────┬─────────────┬──────────────────────┐\n# │ Extractor        │   Time │ Confidence │      Output │ Status               │\n# ├──────────────────┼────────┼────────────┼─────────────┼──────────────────────┤\n# │ PyMuPDF          │  0.02s │        95% │ 3,241 chars │ all pages good       │\n# │ Multi-pass       │  0.03s │        95% │ 3,241 chars │ all pages good       │\n# │ RapidOCR         │  4.20s │        88% │ 2,891 chars │ ok                   │\n# │ OpenDataLoader   │  0.12s │        97% │ 3,310 chars │ best                 │\n# └──────────────────┴────────┴────────────┴─────────────┴──────────────────────┘\n```\n\n### `pdfmux estimate`\n\nPredict spend (and which backends will run) before processing.\n\n```bash\npdfmux estimate report.pdf --quality high --llm-provider gemini\n# Pages       : 47\n# Extractors  : pymupdf4llm + gemini-2.5-flash on 9 pages\n# Estimated   : $0.0234\n# Cache hit?  : no  (first run for this file)\n```\n\n### `pdfmux stream`\n\nEmit NDJSON events as pages complete — useful for very long PDFs and live UIs.\n\n```bash\npdfmux stream long.pdf --quality high\n# {\"event\":\"classified\",\"page_count\":312,\"plan\":\"pymupdf+gemini-fallback\"}\n# {\"event\":\"page\",\"page_num\":0,\"confidence\":0.97,\"chars\":1842}\n# {\"event\":\"page\",\"page_num\":1,\"confidence\":0.92,\"chars\":1611,\"ocr\":true}\n# ...\n# {\"event\":\"complete\",\"confidence\":0.94,\"cost_usd\":0.0712}\n```\n\n### `pdfmux watch`\n\nAuto-convert any PDFs that land in a directory. Survives until Ctrl+C.\n\n```bash\npdfmux watch ./inbox/ -o ./output/ --profile bulk-rag\n```\n\n### `pdfmux diff`\n\nSide-by-side extraction comparison (quality, content, cost).\n\n```bash\npdfmux diff a.pdf b.pdf --quality standard\n```\n\n### `pdfmux profiles`\n\nSaved configs at `~/.config/pdfmux/profiles.yaml`. Built-ins ship for the\ncommon shapes; save your own for project defaults.\n\n```bash\npdfmux profiles list\n# invoices    quality=standard, schema=invoice, format=json\n# receipts    quality=fast,     schema=receipt, format=json\n# papers      quality=high,     chunk=true, max_tokens=500\n# contracts   quality=high,     schema=contract\n# bulk-rag    quality=standard, format=llm, chunk=true\n\npdfmux profiles show invoices\npdfmux profiles save my-default --quality high --format llm --chunk\npdfmux profiles delete my-default\n\n# use a profile when converting\npdfmux convert file.pdf --profile invoices\n```\n\n## Python API\n\n### Text extraction\n\n```python\nimport pdfmux\n\ntext = pdfmux.extract_text(\"report.pdf\")                    # -> str (markdown)\ntext = pdfmux.extract_text(\"report.pdf\", quality=\"fast\")    # PyMuPDF only, instant\ntext = pdfmux.extract_text(\"report.pdf\", quality=\"high\")    # LLM-assisted\n```\n\n### Structured extraction\n\n```python\ndata = pdfmux.extract_json(\"report.pdf\")\n# data[\"page_count\"]   -> 12\n# data[\"confidence\"]   -> 0.91\n# data[\"ocr_pages\"]    -> [2, 5, 8]\n# data[\"pages\"][0][\"key_values\"]  -> [{\"key\": \"Date\", \"value\": \"2026-02-28\"}]\n# data[\"pages\"][0][\"tables\"]      -> [{\"headers\": [...], \"rows\": [...]}]\n```\n\n### RAG chunking\n\n```python\nchunks = pdfmux.chunk(\"report.pdf\", max_tokens=500)\nfor c in chunks:\n    print(f\"{c['title']}: {c['tokens']} tokens (pages {c['page_start']}-{c['page_end']})\")\n```\n\n### Schema-guided extraction\n\n```python\ndata = pdfmux.extract_json(\"invoice.pdf\", schema=\"invoice\")\n# Uses built-in invoice preset: extracts date, vendor, line items, totals\n# Also accepts a path to a custom JSON Schema file\n```\n\n### Streaming (bounded memory)\n\n```python\nfrom pdfmux.extractors import get_extractor\n\next = get_extractor(\"fast\")\nfor page in ext.extract(\"large-500-pages.pdf\"):  # Iterator[PageResult]\n    process(page.text)  # constant memory, even on 500-page PDFs\n```\n\n### Types and errors\n\n```python\nfrom pdfmux import (\n    # Enums\n    Quality,              # FAST, STANDARD, HIGH\n    OutputFormat,         # MARKDOWN, JSON, CSV, LLM\n    PageQuality,          # GOOD, BAD, EMPTY\n\n    # Data objects (frozen dataclasses)\n    PageResult,           # page: text, page_num, confidence, quality, extractor\n    DocumentResult,       # document: pages, source, confidence, extractor_used\n    Chunk,                # chunk: title, text, page_start, page_end, tokens\n\n    # Errors\n    PdfmuxError,          # base -- catch this for all pdfmux errors\n    FileError,            # file not found, unreadable, not a PDF\n    ExtractionError,      # extraction failed\n    ExtractorNotAvailable,# requested backend not installed\n    FormatError,          # invalid output format\n    AuditError,           # audit could not complete\n)\n```\n\n## Framework Integrations\n\n### LangChain\n\n```bash\npip install langchain-pdfmux\n```\n\n```python\nfrom langchain_pdfmux import PDFMuxLoader\n\nloader = PDFMuxLoader(\"report.pdf\", quality=\"standard\")\ndocs = loader.load()  # -> list[Document] with confidence metadata\n```\n\n### LlamaIndex\n\n```bash\npip install llama-index-readers-pdfmux\n```\n\n```python\nfrom llama_index.readers.pdfmux import PDFMuxReader\n\nreader = PDFMuxReader(quality=\"standard\")\ndocs = reader.load_data(\"report.pdf\")  # -> list[Document]\n```\n\n### MCP Server (AI Agents)\n\nListed on [mcpservers.org](https://mcpservers.org). One-line setup:\n\n```json\n{\n  \"mcpServers\": {\n    \"pdfmux\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"pdfmux-mcp\"]\n    }\n  }\n}\n```\n\nOr via Claude Code:\n\n```bash\nclaude mcp add pdfmux -- npx -y pdfmux-mcp\n```\n\nTools exposed: `convert_pdf`, `analyze_pdf`, `extract_structured`,\n`extract_streaming`, `get_pdf_metadata`, `batch_convert`.\n\n## BYOK LLM Configuration\n\npdfmux supports any LLM via 5 lines of YAML. Bring your own keys -- nothing leaves your machine unless you configure it to.\n\n```yaml\n# ~/.pdfmux/llm.yaml\nprovider: claude          # gemini | claude | openai | ollama | any OpenAI-compatible\nmodel: claude-sonnet-4-20250514\napi_key: ${ANTHROPIC_API_KEY}\nbase_url: https://api.anthropic.com  # optional, for custom endpoints\nmax_cost_per_page: 0.02   # budget cap\n```\n\nSupported providers:\n\n| Provider | Models | Local? | Cost |\n|----------|--------|--------|------|\n| Gemini | 2.5 Flash, 2.5 Pro | No | ~$0.01/page |\n| Gemma 3 | 27B IT, 12B IT (great for Arabic) | No (via Gemini key) | ~$0.0002/page |\n| Claude | Sonnet, Opus | No | ~$0.015/page |\n| GPT-4o | GPT-4o, GPT-4o-mini | No | ~$0.01/page |\n| Mistral | `mistral-ocr-latest` | No | $0.002/page |\n| Ollama | Any local model | Yes | Free |\n| Custom | Any OpenAI-compatible API | Configurable | Varies |\n\nEvery provider's `extract_page()` is wrapped in `@with_retry(max_attempts=3,\nbackoff_base=2.0)`, which honors `Retry-After` headers on 429s and skips\nretries on auth failures so a bad key fails fast.\n\n## Arabic & RTL Support\n\npdfmux ships first-class support for Arabic, Persian, Urdu, and Hebrew.\nOut of the box, RTL detection runs on every PDF and PyMuPDF-extracted\npages are passed through the Unicode Bidirectional Algorithm so glyphs\nthat were stored in left-to-right order render in correct reading order.\n\n```bash\n# Default install — already includes python-bidi for RTL reordering\npip install pdfmux\n\n# Recommended for Arabic-heavy docs — adds Gemma vision OCR\n# (Gemma speaks the OpenAI protocol, so it needs the openai SDK)\npip install \"pdfmux[llm-openai]\"\n\n# One credential covers Gemma + Gemini (same Google endpoint)\nexport GEMINI_API_KEY=...\n```\n\nWhat happens automatically:\n\n- `pdfmux convert` detects Arabic content and routes pages with >5%\n  Arabic characters through the Arabic-aware extractor chain.\n- PyMuPDF, RapidOCR, and Docling outputs are post-processed with the\n  Bidi algorithm — markdown headings (`#`) and pipe-table rows preserve\n  structure, only inner text is reordered.\n- `DocumentResult.has_arabic` is set to `True` whenever any page contains\n  Arabic script.\n\nWhat requires opt-in:\n\n- Vision LLM extraction. Set `--llm-provider gemma` (or any vision\n  provider) to route Arabic pages through Gemma instead of PyMuPDF.\n- Aggressive normalization (Tatweel removal, Alef/Yeh unification,\n  Tashkeel stripping) — call `pdfmux.arabic.normalize_arabic(text)`\n  on extracted strings if you need canonicalized output for search or\n  embedding.\n\n```python\nfrom pdfmux.arabic import (\n    is_arabic_text,\n    is_rtl_dominant,\n    fix_bidi_order,\n    normalize_arabic,\n)\n\ntext = \"مرحبا بالعالم\"\nassert is_arabic_text(text)\nassert is_rtl_dominant(text)\n\n# Fix glyph order from PyMuPDF / OCR engines\nvisual = fix_bidi_order(text)\n\n# Canonicalize for indexing — strip Tatweel, unify Alef variants, drop diacritics\nindexable = normalize_arabic(\"أَحْمَدْ\")  # → \"احمد\"\n```\n\n## Proof: a real customer batch\n\nWe measured pdfmux on **433 real customer documents** — technical and safety data sheets, mixed digital and scanned, some encoding-corrupted. Run the naive way first (an early pdfmux CLI in a subprocess, pypdf fallback, no OCR), the pipeline **silently dropped 16 documents — 11 of them with no log line at all.** That was our own tool failing at the exact thing it promises.\n\nRebuilt with the per-page audit + budgeted OCR cascade: **433 of 433 processed, zero silent failures.** Every unrecoverable page is flagged, not dropped.\n\n*(A small internal confidence-calibration set also ships under `eval/` — it's a regression guard on the confidence gate, not a competitive benchmark; see [`eval/README.md`](eval/README.md).)*\n\n## Benchmark\n\nOn [opendataloader-bench](https://github.com/opendataloader-project/opendataloader-bench) — 200 real-world PDFs (financial filings, academic papers, legal contracts, government reports) — pdfmux scores **0.903 overall — #2 of the 8 engines measured**, behind `opendataloader-hybrid` (0.909). Re-run 2026-07-16 (reproduction below).\n\n| Rank | Engine | Overall | Reading order | Tables (TEDS) | License | GPU |\n|---:|---|---:|---:|---:|---|---|\n| 1 | opendataloader-hybrid | 0.909 | 0.935 | 0.928 | Apache-2.0 | No |\n| **2** | **pdfmux** | **0.903** | **0.920** | **0.911** | **MIT** | **No** |\n| 3 | Docling | 0.877 | 0.900 | 0.887 | MIT | Optional |\n| 4 | marker | 0.861 | 0.890 | 0.808 | free | GPU |\n| 5 | mineru | 0.831 | 0.857 | 0.873 | free | GPU |\n\nFull per-document scores: the [200-PDF head-to-head](https://pdfmux.com/blog/pdfmux-vs-pymupdf-vs-marker-vs-docling/) · methodology: [best PDF extraction library, benchmarked](https://pdfmux.com/blog/best-pdf-extraction-library-python/).\n\n## Smart Result Cache\n\nRe-running the same extraction is instant. pdfmux hashes every input PDF\n(SHA-256) and keys results on `(file_hash, quality, format, schema)`. Cache\nfiles live under `~/.cache/pdfmux/results/`, expire after 30 days, and are\nLRU-evicted at 1 GB.\n\n```bash\npdfmux convert big-report.pdf            # first run: 14.2s\npdfmux convert big-report.pdf            # cache hit: 0.05s\npdfmux convert big-report.pdf --no-cache # bypass cache (still writes back)\npdfmux convert big-report.pdf --clear-cache  # purge and re-run\n```\n\nThe cache also speeds up `--profile`, `--schema`, and `--format` switches —\neach combination is keyed independently, so you can flip between Markdown\nand JSON for the same document for free after the first extraction.\n\n## Confidence Scoring\n\nEvery result includes a 4-signal confidence score:\n\n- **95-100%** -- clean digital text, fully extractable\n- **80-95%** -- good extraction, minor OCR noise on some pages\n- **50-80%** -- partial extraction, some pages unrecoverable\n- **<50%** -- significant content missing, warnings included\n\nWhen confidence drops below 80%, pdfmux tells you exactly what went wrong and how to fix it:\n\n```\nPage 4: 32% confidence. 0 chars extracted from image-heavy page.\n  -> Install pdfmux[ocr] for RapidOCR support on 6 image-heavy pages.\n```\n\n## Cost Modes\n\n| Mode | Behavior | Typical cost |\n|------|----------|-------------|\n| economy | Rule-based backends only. No LLM calls. | $0/page |\n| balanced | LLM only for pages that fail rule-based extraction. | ~$0.002/page avg |\n| premium | LLM on every page for maximum quality. | ~$0.01/page |\n\nSet a hard budget cap: `--budget 0.50` stops LLM calls when spend reaches $0.50 per document.\n\n## Why pdfmux?\n\npdfmux is not another PDF extractor. It is the orchestration layer that picks the right extractor per page, verifies the result, and retries failures.\n\n| Tool | Good at | Limitation |\n|------|---------|-----------|\n| PyMuPDF | Fast digital text | Cannot handle scans or image layouts |\n| Docling | Tables (97.9% accuracy) | Slow on non-table documents |\n| Marker | Neural extraction for academic papers | Needs GPU for speed; overkill for digital PDFs |\n| Mistral OCR | Tables (96.6% TEDS), $0.002/page | Cloud-only API |\n| Unstructured | Enterprise platform | Complex setup, paid tiers |\n| LlamaParse | Cloud-native | Requires API keys, not local |\n| Reducto | High accuracy | $0.015/page, closed source |\n| **pdfmux** | **Orchestrates all of the above** | Routes per page, audits, re-extracts |\n\nOpen source Reducto alternative: what costs $0.015/page elsewhere is free with pdfmux's rule-based backends, or ~$0.002/page average with BYOK LLM fallback.\n\n## Development\n\n```bash\ngit clone https://github.com/NameetP/pdfmux.git\ncd pdfmux\npython3.12 -m venv .venv && source .venv/bin/activate\npip install -e \".[dev]\"\n\npytest              # 659 tests\nruff check src/ tests/\nruff format src/ tests/\n```\n\n## Contributing\n\n1. Fork the repo\n2. Create a branch (`git checkout -b feature/your-feature`)\n3. Write tests for new functionality\n4. Ensure `pytest` and `ruff check` pass\n5. Open a PR\n\n## License\n\nThe pdfmux library and MCP server in this repository are **[MIT](LICENSE)** licensed — free for any use, and every released version stays MIT.\n\nThe confidence-budgeted **decision-trace** method (the persisted per-page decision trace with retained rejected candidates, and the monotonic repair guard) is **patent-pending** (US Provisional App No. 64/106,302) and is reserved for pdfmux Cloud/Pro under a separate commercial license — it is not part of the MIT grant. See **[LICENSING.md](LICENSING.md)** and **[NOTICE](NOTICE)**.\n\n<!-- mcp-name: io.github.NameetP/pdfmux -->\n\n",
  "bytes": 32070,
  "sha": "c0ceda5cb00b8441fe0c1b73192f4e50292e588793057f9f7f8386ac1c7bc07f",
  "repo_slug": "nameetp/pdfmux",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_nameetp_pdfmux_5009c629/readme"
}