Image: AI generated
pip install markitdownsuffit à transformer PDF, documents Word, feuilles Excel et présentations PowerPoint en markdown. Mais donnez-lui un PDF qui n’est rien d’autre que des images scannées — un contrat scanné, par exemple — et vous obtenez un document vide. Cet article suit deux axes : pourquoi et comment utiliser MarkItDown, et comment greffer l’OCR précisément là où cette confiance se brise — les PDF composés uniquement d’images.
Pourquoi utiliser MarkItDown — la condition agent-operable
MarkItDown est la bibliothèque open source de Microsoft pour CLI/Python qui convertit les fichiers PDF, Word, Excel, PowerPoint, HTML, image et audio en markdown.
Commençons par le concept d’agent-operable (un état avec lequel un agent peut travailler). Pour qu’un agent traite un document de façon autonome, ce document doit se trouver dans un état qu’une machine peut analyser structurellement — pas simplement dans un état qu’un humain trouve lisible. Ce n’est pas une condition qui s’applique uniquement au code. Une règle métier enfouie dans un PDF ou une feuille Excel équivaut à ne pas exister pour un agent, puisqu’un humain doit l’ouvrir pour savoir ce qu’elle contient. C’est la première des trois conditions — lisible, vérifiable, persistant — exposées dans Building Agent-Operable Systems : « lisible sans bruit ».
Alors pourquoi MarkItDown en particulier ? On pourrait construire soi-même la même conversion en assemblant des parseurs tiers (PyPDF2, python-docx). La différence tient à qui l’a construit. docx, xlsx et pptx sont tous de l’OOXML — une spécification que Microsoft possède elle-même. Quand l’entreprise propriétaire de la spécification construit le convertisseur pour son propre format, les cas limites (cellules fusionnées, tableaux imbriqués, notes de bas de page) ont structurellement plus de chances de tenir la route qu’avec un parseur tiers. Si la conversion elle-même n’est pas fiable, tout ce qu’on construit par-dessus vers un état agent-operable perd son sens. C’est la raison d’utiliser MarkItDown — pour les formats propres à Microsoft, cette confiance est intégrée par défaut.
Le problème est que cette confiance ne se transmet pas aux PDF, en particulier ceux composés uniquement d’images. La seconde moitié de cet article se concentre sur la façon de combler cet écart avec l’OCR.
Installation
Installation de base
pip install markitdown
L’installation de base seule ne gère que des formats légers comme le texte brut et le HTML. Pour convertir des documents PDF et Office, il faut installer les extras spécifiques à chaque format.
Extras spécifiques par format
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.
Si cet article n’a besoin que du PDF, markitdown[pdf] suffit.
Utilisation de base
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)
API Python
from markitdown import MarkItDown
md = MarkItDown()
result = md.convert("example.pdf")
print(result.markdown)
Les deux approches sélectionnent automatiquement le convertisseur selon l’extension du fichier ou le type MIME. Quand il n’y a pas d’information d’extension — en lisant depuis stdin, par exemple — on peut donner un indice avec -x (--extension) ou -m (--mime-type).
Ce que fait réellement la conversion PDF — elle ne lit que la couche de texte
Le convertisseur PDF de MarkItDown (packages/markitdown/src/markitdown/converters/_pdf_converter.py) utilise d’abord pdfplumber pour détecter la structure de tableau/formulaire de chaque page, et se rabat sur page.extract_text() quand une page n’est pas un tableau. Si pdfplumber échoue, il se rabat sur pdfminer.six.
Les deux bibliothèques sont des outils qui lisent une couche de texte déjà existante à l’intérieur du PDF. Si une page ne consiste qu’en une image scannée sans couche de texte, extract_text() renvoie une chaîne vide, et la sortie de MarkItDown finit par être effectivement vide. Il n’existe aucun chemin de code dans le paquet de base qui restitue une image ou reconnaît des caractères à l’intérieur.
Autrement dit, ce sont deux problèmes différents :
- Un PDF avec couche de texte (exporté depuis Word, la plupart des contrats électroniques) → l’installation de base fonctionne bien. La même confiance qui porte les formats propres à Microsoft se transmet ici aussi.
- Un PDF sans couche de texte (un document passé au scanner, une photo intégrée comme image) → l’installation de base renvoie un résultat vide. C’est ici que la condition « lisible » d’agent-operable se brise.
Configurer l’OCR — là où la confiance brisée est réparée
C’est ici que commence le second axe de l’article. La confiance que MarkItDown assure pour les formats propres à Microsoft doit être reconstruite séparément pour les PDF composés uniquement d’images.
1. Installer le plugin
Le support OCR se trouve dans un paquet séparé, markitdown-ocr. Il est dans le même dépôt (microsoft/markitdown), mais n’est pas inclus dans pip install markitdown.
pip install markitdown-ocr
pip install openai # an OpenAI-compatible client — this plugin has no OCR model of its own
Après l’installation, vérifiez que le plugin est détecté.
markitdown --list-plugins
# * ocr (package: markitdown_ocr)
2. Ce qui n’est pas négociable — un client LLM Vision
markitdown-ocr n’intègre pas de moteur OCR traditionnel comme Tesseract ou PaddleOCR. Tout ce qu’il fait réellement, c’est encoder l’image en base64 et demander à une API chat.completions compatible OpenAI (par exemple gpt-4o) d’« extraire le texte de cette image » (LLMVisionOCRService dans _ocr_service.py). Par conséquent, une clé API capable d’appeler un modèle de vision est une exigence incontournable pour que l’OCR fonctionne, tout simplement.
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)
N’importe quel client compatible OpenAI — Azure OpenAI y compris — peut être fourni de la même manière.
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. Ça ne fonctionne pas depuis la CLI — vérifié sur le code réel
Le README de markitdown-ocr donne cette commande comme exemple d’usage :
markitdown document.pdf --use-plugins --llm-client openai --llm-model gpt-4o
Mais en vérifiant directement les définitions d’arguments de la CLI du cœur de MarkItDown (packages/markitdown/src/markitdown/__main__.py), on constate que ces drapeaux n’existent pas — il n’y a ni --llm-client ni --llm-model. La CLI prend en charge -o (fichier de sortie), -x (indice d’extension), -m (indice MIME), -c (charset), -d/--use-docintel, --use-cu, -p/--use-plugins, --list-plugins et --keep-data-uris, et c’est tout — le constructeur de MarkItDown ne reçoit que enable_plugins=args.use_plugins. Autrement dit, l’exemple de CLI du README est une erreur de documentation ; il ne fonctionne en réalité pas.
L’OCR ne fonctionne que via l’API Python — il faut passer llm_client/llm_model directement. Il n’existe aucun moyen de transmettre une clé API à la CLI.
4. Comment ça fonctionne réellement — OCR des images intégrées et repli sur l’OCR de page entière
PdfConverterWithOCR (_pdf_converter_with_ocr.py) fonctionne en deux étapes.
- Images intégrées en priorité : quand une page mélange texte et images (disons une signature scannée déposée au milieu du corps du texte), seule l’image est découpée, envoyée à LLM Vision, et le résultat est réintercalé avec le texte environnant selon sa position Y d’origine, en préservant l’ordre de lecture.
- Repli sur l’OCR de page entière : si l’extraction de texte revient complètement vide (un PDF entièrement scanné), la page entière est restituée en PNG à 300dpi et envoyée à LLM Vision une page à la fois (
_ocr_full_pages).
Le texte extrait est toujours enveloppé dans ce format :
*[Image OCR]
<extracted text>
[End OCR]*
5. Affiner la précision avec un prompt personnalisé
Le prompt par défaut est « extrais tout le texte de cette image, en conservant la mise en page et l’ordre d’origine ». Pour les documents riches en tableaux, en texte vertical, ou comportant des tampons et signatures, le remplacer via llm_prompt peut améliorer la précision.
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. Traitement par lots de plusieurs fichiers
En exécutant tout un dossier de PDF scannés d’un coup, chaque fichier déclenche son propre appel API — il faut envelopper chaque conversion pour qu’un seul échec ne fasse pas échouer tout le lot.
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. Que se passe-t-il si vous installez le plugin sans llm_client ?
Le plugin se charge, mais l’OCR se désactive silencieusement. register_converters() dans _plugin.py ne construit un LLMVisionOCRService que lorsque llm_client et llm_model sont tous deux présents ; sinon, il enregistre les convertisseurs avec ocr_service=None. Dans cet état, on retombe silencieusement sur l’extraction de la seule couche de texte — sans erreur, sans avertissement. La plupart des problèmes du type « j’ai installé le plugin mais l’OCR ne fonctionne pas » remontent exactement à ce point.
8. Il tente même de récupérer les PDF corrompus
Quand pdfplumber et pdfminer ne parviennent absolument pas à ouvrir un PDF — un EOF tronqué, disons — markitdown-ocr se rabat sur le rendu direct des pages avec PyMuPDF (fitz) et réessaie. Si ce chemin échoue aussi, il laisse *[Error: Could not process scanned PDF]* dans le résultat.
9. Coût et vitesse
Comme il envoie des pages entières à un modèle de vision, le coût et la latence s’accumulent proportionnellement au nombre de pages du document scanné. Un rendu à 300dpi n’est pas bon marché en tokens par image, il est donc plus prudent d’échantillonner d’abord quelques pages pour évaluer qualité et coût avant de traiter un document entier de plusieurs centaines de pages.
Vérifier le résultat de l’OCR — à venir, séparément
Tout ce qui précède concerne la façon d’« activer » l’OCR. L’activer n’équivaut pas à pouvoir faire confiance au résultat — dans la conception actuelle, où tout passe silencieusement que llm_client soit configuré ou non, et que la page soit vide ou non, il n’existe aucune procédure déterministe pour juger si la sortie OCR est réellement fiable.
Cette procédure n’est pas couverte ici. La façon d’appliquer le principe du Symbolic Feedback Loop — le LLM génère → un outil déterministe juge → rétroaction → répétition — à la sortie OCR fera l’objet d’un article/outil séparé.
Liste de vérification pratique
- Si vous ne traitez que des PDF ordinaires avec couche de texte, l’installation de base
markitdown[pdf]suffit. Aucun plugin OCR n’est nécessaire. - Si vous devez traiter des PDF scannés ou photographiés,
markitdown-ocrplus une clé API OpenAI (ou compatible) sont obligatoires. Il n’existe aucune intégration avec un moteur OCR traditionnel, local et gratuit, comme Tesseract. - Si le résultat OCR revient vide, la première chose à vérifier est si
llm_client/llm_modelont réellement été passés. L’échec est silencieux, donc rien n’apparaît dans les journaux. - L’OCR ne peut pas être activé depuis la seule CLI. Un script Python appelant
MarkItDown(enable_plugins=True, llm_client=..., llm_model=...)est nécessaire. - Il n’existe pas encore de moyen mécanique de juger de la fiabilité du résultat — pour l’instant, un humain doit vérifier un échantillon.
Tableau récapitulatif
| Situation | Ce qu’il faut | Notes |
|---|---|---|
| PDF avec couche de texte | pip install markitdown[pdf] | pdfplumber/pdfminer extraient le texte |
| Certaines images intégrées nécessitent l’OCR | markitdown-ocr + client LLM Vision | découpe seulement les images, les intercale avec le texte |
| Entièrement scanné (toute la page est une image) | markitdown-ocr + client LLM Vision | restitue la page entière à 300dpi, puis applique l’OCR |
| PDF corrompu | identique ci-dessus (repli automatique) | réessaie en restituant avec PyMuPDF |
llm_client non configuré | — | OCR ignoré silencieusement, sans erreur |
Passer --llm-client en CLI | impossible (ce drapeau n’existe pas) | uniquement via l’API Python |
| Juger de la fiabilité du résultat OCR | (prévu) | sera couvert par un vérificateur séparé |
Articles liés
- Building Agent-Operable Systems — les trois conditions (lisible, vérifiable, persistant), et où se situe MarkItDown
- Agent Operable Codebase — le même principe appliqué au code
Sources
- microsoft/markitdown — racine du dépôt
_pdf_converter.py— le convertisseur PDF de base (extraction de la couche de texte uniquement)__main__.py— définitions des arguments CLI (confirme que--llm-client/--llm-modeln’existent pas)markitdown-ocrREADME_pdf_converter_with_ocr.py_ocr_service.py_plugin.py- markitdown pyproject.toml — liste des extras
Journal des modifications
- 2026-07-09 : Version initiale — ajout du cadrage agent-operable, configuration de l’OCR développée comme seconde moitié, ajout d’une section provisoire pour le vérificateur