Clear purpose: enable the LLM to, based on this skill, correctly configure and invoke
markitdown-ocrplugin, to complete OCR extraction of embedded images and scanned pages in PDF/DOCX/PPTX/XLSX.Offline baseline: based on
packages/markitdown-ocr/main branch (as of 2026-08). For any question like "does the plugin currently support X / has the parameter name changed", refer to the official README and source code:
- Source code:
packages/markitdown-ocr/src/markitdown_ocr/- Entry point:
_plugin.py- OCR service abstraction:
_ocr_service.py
When to Use This Skill
✅ When to use this skill(LLM Vision OCR / scanned documents / embedded images):
- User asks "how do I convert a scanned PDF to markdown" (the original PDF has no selectable text)
- User asks "how do I extract text from images embedded in PPT/Word/Excel"
- User wants to use GPT-4o / GPT-4o-mini / Claude / Gemini for OCR
- User asks "how do I configure markitdown-ocr with Azure OpenAI"
- User asks "how do I install and use the markitdown-ocr plugin"
- User wants to do local OCR (via vLLM / Ollama)
❌ When not to use this skill:
- Only ordinary CLI usage (converting PDF / DOCX, with no image text needs) →
markitdown-cli - User asks "what is MarkItDown / which formats does it support / how do I choose" →
markitdown-awesome - User asks "how do I configure Azure Document Intelligence" →
markitdown-cli(that is a built-in MarkItDown backend, not this OCR plugin) - User asks "how do I convert a PDF to plain text" (textract is more suitable) → hand off to
markitdown-awesome(references/ecosystem/alternatives.mdto give a comparison of markitdown / textract / unstructured / pandoc / docling)
⚠️ Gray areas:
- User says "convert PDF to markdown but the image text is missing" → by default go the OCR plugin route (this skill)
- User says "I want to use an LLM to extract tables" → OCR can do it, but if table accuracy requirements are high, prefer Azure Document Intelligence
- User is already using another OCR (Tesseract / PaddleOCR) → outside this skill's scope, guide them to compare tools
Workflow
Every time you answer an OCR-type question, go through the following process once:
Step 1 — Confirm the document type & the user's goal
- Input format: PDF / DOCX / PPTX / XLSX (this skill covers only these four)
- Text location: embedded images / full scanned pages / both
- LLM selection: OpenAI / Azure / Gemini / local
- Cost budget: expensive vs cheap (
gpt-4ovsgpt-4o-mini)
Step 2 — Check the environment
pip show markitdown-ocr openai | grep -E '^(Name|Version)'
markitdown --list-plugins | grep ocr
Both should produce output.
Step 3 — Choose the Python API form
- OpenAI:
MarkItDown(enable_plugins=True, llm_client=OpenAI(), llm_model="gpt-4o") - Azure OpenAI: switch
AzureOpenAI(...), passazure_endpoint/api_version - Local vLLM / Ollama:
OpenAI(base_url=..., api_key=<your-token>)(local services usually do not validate the key) - Gemini:
OpenAI(base_url=..., api_key=<your-key>)
Step 4 — Decide on the Prompt
- Default prompt (bundled with the plugin): "Extract all text … maintain layout … no commentary"
- To emphasize tables: add "Preserve table layout with Markdown pipes"
- To emphasize outputting only text: add "Do not describe, summarize, or comment"
Step 5 — Self-check & run
Refer to the Validation self-check checklist。
Step 6 — Processing result
- Output contains
*[Image OCR]…[End OCR]*block → success - Not present → check the plugin list, client, and model name (see §8 Troubleshooting)
Validation (self-check list)
Run through this before/after answering any OCR-type question:
- Has the user installed
markitdown-ocr?(pip show markitdown-ocr) - Has the user installed the OpenAI-compatible SDK? (
openai/azure-openai/ custom) - Has the user configured
OPENAI_API_KEY(or equivalent credentials)? - In the Python code,at the same timeprovided
enable_plugins=Trueandllm_clientandllm_model?(If any of the three is missing, OCR is skipped) - Does the model support vision? (You cannot use a text-only model such as
gpt-3.5-turbo) - When using Azure OpenAI,
azure_endpointdoes not have a trailing/、api_versionvalid? - Handling sensitive documents → recommend a private deployment backend (vLLM / Ollama) instead of a cloud API?
- Did you avoid falsely promising that the CLI can configure
--llm-client/--llm-model?(These two CLIs do not exist; Python wrapping is required)
Gotchas
From highest to lowest hit frequency:
- The CLI does not have
--llm-client/--llm-model— any claim of "enabling OCR directly from the command line" is wrong. Either write a Python wrapper or tell the user to use the Python API.__main__.pyin the argparse registry. enable_plugins=Trueomitted — MarkItDown defaults toenable_plugins=False, and the plugin will not be registered at all.must be included。- Missing
llm_clientorllm_model— the plugin "will still be registered" (without an OCR service), but the actual conversion will silently skip OCR. Checklist: all three parametersmustbe provided. - LLM failures are silently swallowed —
_ocr_service.py::extract_textconverts exceptions intoOCRResult(text="", error=str(e)); the plugin'sconvert()continues and does not throw. Symptom: the OCR block for a certain image is missing, but the rest of the document is OK.There is no stderr warning at all。 - PPTX descriptions take precedence over OCR — if the LLM returns an "image description", OCR is only a fallback. This means: for the same image, different calls may produce different text (description vs OCR).
- XLSX positional context is lost — images are uniformly appended after the data table (
### Images in this sheet:), and are not interleaved with rows. Use with caution in table-heavy scenarios. - Scanned PDFs render full pages at 300 DPI — much more expensive than "embedded small images"; a 50-page scan ≈ 50 vision calls, with high token cost.
- OCR blocks are identified by regex — the output format
*[Image OCR]\n…\n[End OCR]*is a public contract; when post-processing depends on it, modifying the plugin source code will break the regex. - Local vision models may not support
data:URI — support for Base64-embedded images is unstable in some vLLM / Ollama models; if it fails, confirm that the model itself supports it. - Azure OpenAI
api_versionmismatch — the deployment and SDK version must be compatible; using too new a version number may result in 404. - One LLM call per image = high concurrency risk — using a thread pool directly for 100 images may hit OpenAI RPM/TPM limits. Swallowed 429s manifest as "images not OCR'd" and are hard to notice.
- OCR blocks pollute the main text summary — if you want to "see only the main text", you must first strip
*[Image OCR]…[End OCR]*with regex, otherwise the LLM will cite OCR text as main text. gpt-3.5-turbodoes not support vision — When the user enters the wrong model name, the API returns a 400, but the plugin silently swallows it; the symptom is that all OCR blocks are missing.- Missing
PilloworPyMuPDF—markitdown-ocris already in the dependency list; if you carelesslypip install markitdown-ocr --no-depsthese will be missing, causingimportto directly report an error. - Single memory spike for large files — When the entire PDF goes through
convert_local, PyMuPDF rendering + base64 encoding are in memory at the same time; images of tens of MB will blow up memory.
1. What it is / what problem it solves
MarkItDown's built-inPDF/DOCX/PPTX/XLSX convertersdo notperform OCR on embedded images or scanned pages—they only take "text that is already parseable". If the document contains images, scanned pages, or text converted into images, that part is empty in the default output.
markitdown-ocr plugintakes over the above four built-in converters (priority -1.0, higher than the built-in 0.0 ), and via LLM Vision(any OpenAI-compatible model that supports image input) extracts text from embedded images and inserts it back into the Markdown stream according to the format.
Core features:
- ✅ Full support for PDF / DOCX / PPTX / XLSX
- ✅ Automatic full-page OCR fallback for scanned PDFs (render each page at 300 DPI and send the whole page to the LLM)
- ✅ DOCX image relationship graph fully preserved (paragraph/heading/table flow not disrupted)
- ✅ PPTX processes images/placeholders/images within groups in "top→left" reading order
- ✅ XLSX restores column/row letter coordinates by image anchor, and images are uniformly appended after the table
- ✅ Output wrapped with
*[Image OCR]…[End OCR]*around each OCR block, convenient for post-processing - ❌ Does not introduce new ML libraries or binary dependencies (the dependencies are already in the built-in converters)
- ⚠️ When the LLM call failssilently skip, and conversion continues
2. Installation
pip install markitdown-ocr
# 同时确保有可用的 OpenAI 兼容客户端
pip install openai # 官方 OpenAI
# 或
pip install azure-openai # Azure OpenAI(若走 Azure)
Install from source (development/debugging):
git clone https://github.com/microsoft/markitdown.git
cd markitdown/packages/markitdown-ocr
pip install -e .
Dependencies (pulled automatically):markitdown>=0.1.0、pdfminer.six、pdfplumber、PyMuPDF、mammoth、python-docx、python-pptx、pandas、openpyxl、Pillow。
3. Configuration — LLM client and model
Key constraint: The plugin itselfdoes not read any API key—it gets the client and model from MarkItDown(llm_client=…, llm_model=…, llm_prompt=…) , and authentication is entirely handled by the client (via its own environment variables/credential chain).
3.1 OpenAI (default)
export OPENAI_API_KEY=sk-...
from openai import OpenAI
client = OpenAI() # 自动读 OPENAI_API_KEY
3.2 Azure OpenAI
from openai import AzureOpenAI
client = AzureOpenAI(
api_key=os.environ["AZURE_OPENAI_API_KEY"], # 或读其它环境变量
azure_endpoint="https://<res>.openai.azure.com/",
api_version="2024-02-01",
)
3.3 Other OpenAI-compatible services (Gemini / Ollama / vLLM / LM Studio / DeepSeek-VL, etc.)
Any object that implements the client.chat.completions.create(model=…, messages=[…]) interface can be passed in,provided it supports vision input. Example:
import openai
client = openai.OpenAI(
base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
api_key=os.environ["GOOGLE_API_KEY"],
)
# 之后正常使用,llm_model="gemini-2.0-flash" 等
For a more complete compatible client matrix, see backends/compatible-clients.md; for practical scripts, see examples/backends/azure-and-local.md。
3.4 Behavior when llm_client is missing
_plugin.py::register_converters in:
if llm_client and llm_model:
ocr_service = LLMVisionOCRService(client=…, model=…, default_prompt=…)
If llm_client or llm_model is missing, the pluginis still registered(what is registered is the version without an OCR service), but during actual conversion itsilently skips OCR, falling back to the behavior of the built-in converter. Verify that the plugin exists:
markitdown --list-plugins
# 应当看到: ocr (package: markitdown_ocr)
4. Python API — three typical usages
4.1 Simplest (OpenAI + gpt-4o)
from markitdown import MarkItDown
from openai import OpenAI
md = MarkItDown(
enable_plugins=True, # ← 必须
llm_client=OpenAI(), # ← 必须
llm_model="gpt-4o", # ← 必须
)
result = md.convert("document_with_images.pdf")
print(result.markdown)
4.2 Azure OpenAI
from markitdown import MarkItDown
from openai import AzureOpenAI
md = MarkItDown(
enable_plugins=True,
llm_client=AzureOpenAI(
api_key=os.environ["AZURE_OPENAI_API_KEY"],
azure_endpoint="https://<res>.openai.azure.com/",
api_version="2024-02-01",
),
llm_model="gpt-4o",
)
print(md.convert("report.docx").markdown)
4.3 Custom Prompt
md = MarkItDown(
enable_plugins=True,
llm_client=OpenAI(),
llm_model="gpt-4o",
llm_prompt="Extract all text from this image, preserving table structure.",
)
5. CLI — currently there is no native --llm-client / --llm-model
⚠️ Important:
markitdownThe current CLI (__main__.py) is not registered--llm-client/--llm-model. To enable OCR via the command line,write a 3-line wrapper in PythonThat is sufficient.
5.1 Recommended practice: shell wrapper
Write a small file mdocr:
#!/usr/bin/env python3
# mdocr — 命令行包装 markitdown-ocr
import os, sys, argparse
from markitdown import MarkItDown
ap = argparse.ArgumentParser()
ap.add_argument("filename")
ap.add_argument("-o", "--output")
ap.add_argument("--model", default=os.environ.get("MD_OCR_MODEL", "gpt-4o"))
ap.add_argument("--prompt", default=None)
args = ap.parse_args()
client = OpenAI() # ← 替换为你要用的客户端构造
md = MarkItDown(
enable_plugins=True,
llm_client=client,
llm_model=args.model,
llm_prompt=args.prompt,
)
# 注意:请在环境变量 OPENAI_API_KEY / AZURE_OPENAI_API_KEY / OLLAMA_HOST 等中配置凭据
# 切勿把真实 key 写进源代码或 commit。
result = md.convert(args.filename)
sys.stdout.write(result.markdown)
if args.output:
with open(args.output, "w", encoding="utf-8") as f:
f.write(result.markdown)
chmod +x mdocr
./mdocr scan.pdf -o scan.md --model gpt-4o
5.2 One-liner python -c(for temporary use)
python -c "
from markitdown import MarkItDown
from openai import OpenAI
import sys
md = MarkItDown(enable_plugins=True, llm_client=OpenAI(), llm_model='gpt-4o')
sys.stdout.write(md.convert(sys.argv[1]).markdown)
" scan.pdf > scan.md
6. Format-level behavior differences (how OCR results are inserted back)
| Format | OCR block insertion location | Description |
|---|---|---|
| Inline (interspersed in the surrounding text), inserted in "top-to-bottom vertical reading order"; scanned pages are sent to the LLM as full pages | Embedded images are extracted by page.images / page XObjects; for malformed PDF, automatically switches to full-page rendering with PyMuPDF |
|
| DOCX | OCR is performed in the "DOCX→HTML→Markdown" pipelinebeforeexecution; the document flow (headings/paragraphs/tables) is 100% preserved | Via doc.part.rels extract image relationships |
| PPTX | In "top→left" reading order within the slide; if the LLM returns an "image description", the description is preferred, and OCR is used only as a fallback | Supports Picture shape, placeholder shape, and images within groups |
| XLSX | The images of each worksheet are uniformly appended after the data table and labeled as ### Images in this sheet: |
The position is calculated from the image anchor to column letters/row numbers;the positional context will be lost(different from the other three formats) |
OCR output format: each block is wrapped in Markdown emphasis for easy identification during post-processing:
*[Image OCR]
<抽取到的文字>
[End OCR]*
To batch strip OCR blocks and create a "body text only" summary, you can use a one-line sed:
sed -e '/^\*\[Image OCR\]$/,/^\[End OCR\]\*$/d' input.md > cleaned.md
7. Limitations and costs (must-read before deployment)
| Item | Description |
|---|---|
| One LLM call per image | 100 images = 100 vision calls; cost grows linearly with the number of images |
| Scanned PDFs are more expensive | Scanned pages are sent to the LLM as full pages at 300 DPI, consuming far more tokens than "small embedded images" |
| LLM errors are silent | "If the LLM call fails, conversion continues without that image's text"—failures are swallowed and require monitoring |
| PPTX descriptions take priority | If the LLM returns an "image description", OCR is used only as a fallback, and the final text is the description rather than the original text |
| XLSX position is lost | Images are appended after the table and will not appear near the cell where they were originally located |
| Image MIME | The OCR service uses stream_info.mimetype or PIL detection; if neither recognizes it, it is treated as image/png |
8. Troubleshooting quick reference
| Symptom | Troubleshooting |
|---|---|
| Not a single OCR block appears | markitdown --list-plugins Check whether it contains ocr; confirm that enable_plugins=True and llm_client + llm_model |
Reports ModuleNotFoundError: markitdown_ocr |
pip install markitdown-ocr, or pip install -e packages/markitdown-ocr |
Reports ModuleNotFoundError: openai |
pip install openai or switch to another OpenAI-compatible SDK |
| OCR is missing some images | That LLM call failed and was silently swallowed; add logging/retries, or switch to a more stable model |
BadRequestError: image_url invalid |
The client/MIME is not recognized; in _ocr_service.py confirm that content_type is inferred correctly, or switch clients |
| Azure OpenAI authentication fails | Confirm that api_version is a version supported by that deployment (e.g., 2024-02-01)、azure_endpoint without a trailing / |
| All data:image entries in the output are gone | is the behavior of the MarkItDown main body and is unrelated to OCR — add --keep-data-uris(CLI) or in the Python API pass keep_data_uris=True |
9. Testing / Secondary Development
# 跑插件自带的测试套件
cd packages/markitdown-ocr
pytest tests/ -v
# 本地装好后做最小冒烟
python -c "
from markitdown import MarkItDown
from openai import OpenAI
print(MarkItDown(enable_plugins=True, llm_client=OpenAI(), llm_model='gpt-4o').convert('sample.pdf').markdown[:500])
"
For the plugin's internal mechanisms (registration process, OCRService implementation details, differences between converters), see plugin/internals.md。
10. When to Switch to a Sibling Skill
| User Question | Switch to |
|---|---|
| "What is MarkItDown? Which formats does it support?" | markitdown-awesome |
| "How do I use the markitdown command and pass parameters?" | markitdown-cli |
| "I use Azure Document Intelligence / Content Understanding and don't need the OCR plugin" | markitdown-cli 's "Azure Backend" section |
11. Privacy Policy
This Skill provides local documents and sample code and does not collect, store, or transmit any user data.Actual OCR calls will send the image content you pass in (which may contain sensitive information) to the selected LLM service provider; please comply with the corresponding service provider's privacy terms and the regulations of the user's region. For production deployment, it is recommended to:
- Use a privatized LLM for highly sensitive documents (such as local vLLM / Ollama + OpenAI-compatible protocol)
- Disable request logging / temporary auditing
12. Reference Links
- Plugin README: https://github.com/microsoft/markitdown/blob/main/packages/markitdown-ocr/README.md
- Plugin source entry point: https://github.com/microsoft/markitdown/blob/main/packages/markitdown-ocr/src/markitdown_ocr/_plugin.py
- OCR service abstraction: https://github.com/microsoft/markitdown/blob/main/packages/markitdown-ocr/src/markitdown_ocr/_ocr_service.py
- MarkItDown main repository: https://github.com/microsoft/markitdown
- Sibling Skill:
markitdown-awesome— after installing withnpx skills add full-stack-skills/document-skills --skill markitdown-awesome, reference it by skill name - Sibling Skill:
markitdown-cli— after installing withnpx skills add full-stack-skills/document-skills --skill markitdown-cli, reference it by skill name - Full installation:
npx skills add full-stack-skills/document-skills(install all 11 skills at once)