Image: AI generated
Allein
pip install markitdownverwandelt PDFs, Word-Dokumente, Excel-Tabellen und PowerPoint-Präsentationen in Markdown. Gibt man ihm aber ein PDF, das nur aus gescannten Bildern besteht — etwa ein gescannter Vertrag —, kommt ein leeres Dokument heraus. Dieser Beitrag verläuft auf zwei Spuren: warum und wie man MarkItDown nutzt, und wie man OCR genau an dem Punkt nachrüstet, an dem dieses Vertrauen zusammenbricht — reine Bild-PDFs.
Warum man MarkItDown nutzen sollte — die Agent-Operable-Bedingung
MarkItDown ist Microsofts Open-Source-CLI/Python-Bibliothek, die PDF-, Word-, Excel-, PowerPoint-, HTML-, Bild- und Audiodateien in Markdown umwandelt.
Beginnen wir mit dem Konzept agent-operable (ein Zustand, mit dem ein Agent arbeiten kann). Damit ein Agent ein Dokument autonom bearbeiten kann, muss dieses Dokument in einem Zustand vorliegen, den eine Maschine strukturell parsen kann — nicht nur in einem Zustand, den ein Mensch für lesbar hält. Das ist keine Bedingung, die nur für Code gilt. Eine Geschäftsregel, die in einem PDF oder einer Excel-Tabelle vergraben ist, existiert für einen Agenten praktisch nicht, da ein Mensch sie öffnen muss, um zu erfahren, was darin steht. Das ist die erste von drei Bedingungen — lesbar, verifizierbar, persistent — die in Building Agent-Operable Systems dargelegt werden: „lesbar ohne Rauschen".
Warum also ausgerechnet MarkItDown? Man könnte dieselbe Konvertierung selbst bauen, indem man Drittanbieter-Parser (PyPDF2, python-docx) zusammensetzt. Der Unterschied liegt darin, wer es gebaut hat. docx, xlsx und pptx sind allesamt OOXML — eine Spezifikation, die Microsoft selbst besitzt. Baut das Unternehmen, dem die Spezifikation gehört, den Konverter für sein eigenes Format, halten Randfälle (verbundene Zellen, verschachtelte Tabellen, Fußnoten) strukturell eher stand als bei einem Drittanbieter-Parser. Ist die Konvertierung selbst nicht vertrauenswürdig, wird alles, was darauf hin zu einem Agent-Operable-Zustand aufgebaut wird, bedeutungslos. Das ist der Grund, MarkItDown zu nutzen — für Microsofts eigene Formate ist dieses Vertrauen standardmäßig vorhanden.
Das Problem ist, dass sich dieses Vertrauen nicht auf PDFs überträgt, insbesondere auf reine Bild-PDFs. Die zweite Hälfte dieses Beitrags konzentriert sich darauf, diese Lücke mit OCR zu schließen.
Installation
Basisinstallation
pip install markitdown
Die reine Basisinstallation deckt nur leichte Formate wie Klartext und HTML ab. Um PDF- und Office-Dokumente zu konvertieren, braucht man die formatspezifischen Extras.
Formatspezifische Extras
pip install 'markitdown[pdf]' # PDF (pdfminer.six + pdfplumber)
pip install 'markitdown[docx]' # Word
pip install 'markitdown[pptx]' # PowerPoint
pip install 'markitdown[xlsx]' # Excel
pip install 'markitdown[all]' # everything above, plus audio transcription, YouTube captions, Azure Document Intelligence, etc.
Braucht dieser Beitrag nur PDF, reicht markitdown[pdf].
Grundlegende Nutzung
CLI
markitdown example.pdf # print markdown to stdout
markitdown example.pdf -o example.md # save to a file
cat example.pdf | markitdown # read from stdin (pipe)
Python-API
from markitdown import MarkItDown
md = MarkItDown()
result = md.convert("example.pdf")
print(result.markdown)
Beide Wege wählen den Konverter automatisch anhand der Dateierweiterung oder des MIME-Typs aus. Fehlt eine Erweiterungsinformation — etwa beim Lesen von stdin —, kann man mit -x (--extension) oder -m (--mime-type) einen Hinweis geben.
Was die PDF-Konvertierung tatsächlich tut — sie liest nur die Textebene
Microsofts PDF-Konverter (packages/markitdown/src/markitdown/converters/_pdf_converter.py) nutzt zunächst pdfplumber, um auf jeder Seite Tabellen-/Formularstrukturen zu erkennen, und greift auf page.extract_text() zurück, wenn eine Seite keine Tabelle ist. Schlägt pdfplumber fehl, greift es auf pdfminer.six zurück.
Beide Bibliotheken sind Werkzeuge, die eine bereits im PDF vorhandene Textebene lesen. Besteht eine Seite nur aus einem gescannten Bild ohne Textebene, gibt extract_text() einen leeren String zurück, und die Ausgabe von MarkItDown ist am Ende praktisch leer. Es gibt im Basispaket überhaupt keinen Codepfad, der ein Bild rendert oder Zeichen darin erkennt.
Anders gesagt sind das zwei unterschiedliche Probleme:
- Ein PDF mit Textebene (aus Word exportiert, die meisten elektronischen Verträge) → die Basisinstallation funktioniert einwandfrei. Dasselbe Vertrauen, das Microsofts eigene Formate trägt, überträgt sich auch hierher.
- Ein PDF ohne Textebene (ein durch einen Scanner gelaufenes Dokument, ein als Bild eingebettetes Foto) → die Basisinstallation liefert ein leeres Ergebnis. Hier bricht die Agent-Operable-Bedingung „lesbar".
OCR einrichten — wo das gebrochene Vertrauen geflickt wird
Hier beginnt die zweite Spur dieses Beitrags. Das Vertrauen, das MarkItDown für Microsofts eigene Formate sichert, muss für reine Bild-PDFs separat neu aufgebaut werden.
1. Das Plugin installieren
Die OCR-Unterstützung liegt in einem separaten Paket, markitdown-ocr. Es befindet sich im selben Repository (microsoft/markitdown), ist aber nicht in pip install markitdown enthalten.
pip install markitdown-ocr
pip install openai # an OpenAI-compatible client — this plugin has no OCR model of its own
Nach der Installation prüfen, ob das Plugin erkannt wird.
markitdown --list-plugins
# * ocr (package: markitdown_ocr)
2. Was unverzichtbar ist — ein LLM-Vision-Client
markitdown-ocr bringt keine klassische OCR-Engine wie Tesseract oder PaddleOCR mit. Alles, was es tatsächlich tut, ist, das Bild base64-zu-kodieren und eine OpenAI-kompatible chat.completions-API (z. B. gpt-4o) zu bitten, „den Text aus diesem Bild zu extrahieren" (LLMVisionOCRService in _ocr_service.py). Daher ist ein API-Schlüssel, der ein Vision-Modell aufrufen kann, eine zwingende Voraussetzung, damit OCR überhaupt funktioniert.
from markitdown import MarkItDown
from openai import OpenAI
md = MarkItDown(
enable_plugins=True,
llm_client=OpenAI(), # reads OPENAI_API_KEY from the environment
llm_model="gpt-4o",
)
result = md.convert("scanned_contract.pdf")
print(result.markdown)
Jeder OpenAI-kompatible Client — Azure OpenAI eingeschlossen — lässt sich auf dieselbe Weise übergeben.
from openai import AzureOpenAI
md = MarkItDown(
enable_plugins=True,
llm_client=AzureOpenAI(
api_key="...",
azure_endpoint="https://<resource>.openai.azure.com/",
api_version="2024-02-01",
),
llm_model="gpt-4o",
)
3. Über die CLI geht es nicht — anhand des tatsächlichen Codes verifiziert
Die README von markitdown-ocr führt diesen Befehl als Nutzungsbeispiel an:
markitdown document.pdf --use-plugins --llm-client openai --llm-model gpt-4o
Prüft man jedoch die CLI-Argumentdefinitionen des MarkItDown-Kerns direkt (packages/markitdown/src/markitdown/__main__.py), zeigt sich, dass diese Flags nicht existieren — es gibt weder --llm-client noch --llm-model. Die CLI unterstützt -o (Ausgabedatei), -x (Erweiterungshinweis), -m (MIME-Hinweis), -c (Zeichensatz), -d/--use-docintel, --use-cu, -p/--use-plugins, --list-plugins und --keep-data-uris, und das war’s — der MarkItDown-Konstruktor erhält nur enable_plugins=args.use_plugins. Anders gesagt: Das CLI-Beispiel der README ist ein Dokumentationsfehler; es funktioniert in Wirklichkeit nicht.
OCR funktioniert nur über die Python-API — llm_client/llm_model müssen direkt übergeben werden. Es gibt keine Möglichkeit, der CLI einen API-Schlüssel mitzugeben.
4. Wie es tatsächlich funktioniert — OCR eingebetteter Bilder und Fallback auf ganzseitiges OCR
PdfConverterWithOCR (_pdf_converter_with_ocr.py) läuft in zwei Stufen ab.
- Eingebettete Bilder zuerst: Mischt eine Seite Text mit Bildern (etwa eine gescannte Unterschrift mitten im Fließtext), wird nur das Bild ausgeschnitten, an LLM Vision gesendet, und das Ergebnis wird anhand seiner ursprünglichen Y-Position mit dem umgebenden Text neu verschachtelt, wobei die Lesereihenfolge erhalten bleibt.
- Fallback auf ganzseitiges OCR: Kommt die Textextraktion vollständig leer zurück (ein komplett gescanntes PDF), wird die ganze Seite als 300-dpi-PNG gerendert und Seite für Seite an LLM Vision gesendet (
_ocr_full_pages).
Extrahierter Text wird immer in dieses Format eingehüllt:
*[Image OCR]
<extracted text>
[End OCR]*
5. Genauigkeit mit einem eigenen Prompt verbessern
Der Standard-Prompt lautet „extrahiere den gesamten Text aus diesem Bild, wobei Layout und Reihenfolge des Originals erhalten bleiben". Bei tabellenlastigen Dokumenten, vertikalem Text oder Dokumenten mit Stempeln und Unterschriften kann eine Überschreibung mit llm_prompt die Genauigkeit verbessern.
md = MarkItDown(
enable_plugins=True,
llm_client=OpenAI(),
llm_model="gpt-4o",
llm_prompt="Extract all text from this image, but preserve table structure as markdown tables.",
)
6. Mehrere Dateien im Batch verarbeiten
Läuft man einen ganzen Ordner gescannter PDFs auf einmal durch, löst jede Datei ihren eigenen API-Aufruf aus — jede Konvertierung muss einzeln abgefangen werden, damit ein einzelner Fehler nicht den gesamten Batch zum Absturz bringt.
from pathlib import Path
from markitdown import MarkItDown
from openai import OpenAI
md = MarkItDown(enable_plugins=True, llm_client=OpenAI(), llm_model="gpt-4o")
for pdf_path in Path("scans/").glob("*.pdf"):
try:
result = md.convert(str(pdf_path))
pdf_path.with_suffix(".md").write_text(result.markdown, encoding="utf-8")
except Exception as e:
print(f"failed: {pdf_path.name} — {e}")
7. Was passiert, wenn man das Plugin ohne llm_client installiert?
Das Plugin lädt, aber OCR schaltet sich lautlos ab. register_converters() in _plugin.py baut nur dann einen LLMVisionOCRService, wenn sowohl llm_client als auch llm_model vorhanden sind; andernfalls registriert es die Konverter mit ocr_service=None. In diesem Zustand fällt es lautlos auf die reine Textebenen-Extraktion zurück — ohne Fehler, ohne Warnung. Die meisten Probleme der Art „ich habe das Plugin installiert, aber OCR funktioniert nicht" haben genau hier ihre Ursache.
8. Es versucht sogar, beschädigte PDFs zu reparieren
Können pdfplumber und pdfminer ein PDF überhaupt nicht öffnen — etwa bei einem abgeschnittenen EOF —, greift markitdown-ocr darauf zurück, die Seiten direkt mit PyMuPDF (fitz) zu rendern, und versucht es erneut. Scheitert auch dieser Pfad, hinterlässt es *[Error: Could not process scanned PDF]* im Ergebnis.
9. Kosten und Geschwindigkeit
Da ganze Seiten an ein Vision-Modell gesendet werden, summieren sich Kosten und Latenz proportional zur Seitenzahl des gescannten Dokuments. Ein 300-dpi-Rendering ist pro Bild nicht gerade günstig an Tokens, daher ist es sicherer, zunächst einige Seiten stichprobenartig zu prüfen, um Qualität und Kosten abzuschätzen, bevor man ein ganzes Dokument mit mehreren hundert Seiten durchlaufen lässt.
OCR-Ergebnisse verifizieren (Verifier) — folgt separat
Alles bisher Beschriebene dreht sich darum, wie man OCR „einschaltet". Es einzuschalten ist nicht dasselbe, wie dem Ergebnis vertrauen zu können — beim aktuellen Design, bei dem alles lautlos durchläuft, egal ob llm_client gesetzt ist oder nicht und egal ob die Seite leer ist oder nicht, gibt es kein deterministisches Verfahren, um zu beurteilen, ob die OCR-Ausgabe tatsächlich vertrauenswürdig ist.
Dieses Verfahren wird hier nicht behandelt. Wie sich das Symbolic-Feedback-Loop-Prinzip — das LLM generiert → ein deterministisches Werkzeug urteilt → Rückmeldung → Wiederholung — auf die OCR-Ausgabe anwenden lässt, ist Thema eines eigenen Beitrags/Werkzeugs.
Praktische Checkliste
- Wer nur gewöhnliche PDFs mit Textebene verarbeitet, kommt mit der Basisinstallation
markitdown[pdf]aus. Kein OCR-Plugin nötig. - Wer gescannte oder fotografierte PDFs verarbeiten muss, braucht zwingend
markitdown-ocrplus einen OpenAI- (oder kompatiblen) API-Schlüssel. Eine Integration mit einer kostenlosen, lokalen, klassischen OCR-Engine wie Tesseract gibt es nicht. - Kommt das OCR-Ergebnis leer zurück, sollte man zuerst prüfen, ob
llm_client/llm_modeltatsächlich übergeben wurden. Es schlägt lautlos fehl, daher taucht in den Logs nichts auf. - OCR lässt sich nicht allein über die CLI aktivieren. Nötig ist ein Python-Skript, das
MarkItDown(enable_plugins=True, llm_client=..., llm_model=...)aufruft. - Es gibt noch keine mechanische Möglichkeit, die Verlässlichkeit des Ergebnisses zu beurteilen — vorerst muss ein Mensch eine Stichprobe manuell prüfen.
Zusammenfassungstabelle
| Situation | Was benötigt wird | Anmerkungen |
|---|---|---|
| PDF mit Textebene | pip install markitdown[pdf] | pdfplumber/pdfminer extrahieren den Text |
| Einige eingebettete Bilder brauchen OCR | markitdown-ocr + LLM-Vision-Client | schneidet nur die Bilder aus, verschachtelt sie mit dem Text |
| Vollständig gescannt (die ganze Seite ist ein Bild) | markitdown-ocr + LLM-Vision-Client | rendert die ganze Seite bei 300dpi, dann OCR |
| Beschädigtes PDF | wie oben (automatischer Fallback) | versucht es erneut, indem es mit PyMuPDF rendert |
llm_client nicht gesetzt | — | OCR wird lautlos übersprungen, kein Fehler |
--llm-client in der CLI übergeben | nicht möglich (dieses Flag existiert nicht) | nur über die Python-API |
| Verlässlichkeit des OCR-Ergebnisses beurteilen | (geplant) | wird von einem separaten Verifier abgedeckt |
Verwandte Artikel
- Building Agent-Operable Systems — die drei Bedingungen (lesbar, verifizierbar, persistent), und wo MarkItDown hineinpasst
- Agent Operable Codebase — dasselbe Prinzip, angewandt auf Code
Quellen
- microsoft/markitdown — Repository-Wurzel
_pdf_converter.py— der PDF-Basiskonverter (nur Textebenen-Extraktion)__main__.py— CLI-Argumentdefinitionen (bestätigt, dass--llm-client/--llm-modelnicht existieren)markitdown-ocrREADME_pdf_converter_with_ocr.py_ocr_service.py_plugin.py- markitdown pyproject.toml — Liste der Extras
Änderungsverlauf
- 2026-07-09: Erstveröffentlichung — Agent-Operable-Rahmen ergänzt, OCR-Einrichtung als zweite Hälfte ausgebaut, Platzhalter-Abschnitt für den Verifier hinzugefügt