
Mistral Pdf To Markdown
- 46 installs
- 8 repo stars
- Updated December 5, 2025
- fuzhiyu/researchprojecttemplate
Convert PDFs to Markdown using the Mistral OCR API, extracting structured text and embedded images from scanned or complex documents.
About
Converts PDF documents to Markdown via Mistral's OCR API, preserving structure and extracting embedded images. A developer uses it when turning research papers or scanned PDFs into Markdown.
- Uses Mistral OCR API for text and image extraction
- Handles scanned documents and complex formatting
Mistral Pdf To Markdown by the numbers
- 46 all-time installs (skills.sh)
- Ranked #1,114 of 2,719 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Jul 24, 2026 (Skillselion catalog sync)
npx skills add https://github.com/fuzhiyu/researchprojecttemplate --skill mistral-pdf-to-markdownAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 46 |
|---|---|
| repo stars | ★ 8 |
| Last updated | December 5, 2025 |
| Repository | fuzhiyu/researchprojecttemplate ↗ |
What it does
Convert PDFs to Markdown using the Mistral OCR API, extracting structured text and embedded images from scanned or complex documents.
Files
Mistral PDF to Markdown Converter
Convert PDF documents to Markdown format using Mistral's OCR API. Automatically extracts text, formatting, and images.
When to Use
- Converting research papers or documents to Markdown
- Extracting text from scanned PDFs (OCR capability)
- Preserving document structure with headers and formatting
- Extracting embedded images from PDFs
Quick Start
Use the conversion script from this skill's directory:
# Convert entire PDF
python scripts/convert_pdf_to_markdown.py input.pdf output.md
# Convert specific pages
python scripts/convert_pdf_to_markdown.py input.pdf output.md --pages "1-5"
python scripts/convert_pdf_to_markdown.py input.pdf output.md --pages "1,3,5"Output Structure
Output/PDFConversions/
├── document.md # Markdown with text and image references
└── images/
├── img-0.jpeg # Extracted images
├── img-1.jpeg
└── ...Usage in Code
from pathlib import Path
import subprocess
# Run conversion script
result = subprocess.run([
"python",
".claude/skills/mistral-pdf-to-markdown/scripts/convert_pdf_to_markdown.py",
"input.pdf",
"Output/PDFConversions/output.md",
"--pages", "1-10"
], capture_output=True, text=True)
print(result.stdout)Key Features
- Markdown formatting: Preserves headers, lists, and structure
- Image extraction: Saves images to
images/subfolder automatically - Page selection: Extract specific pages or ranges
- Scanned PDF support: True OCR capability for image-based PDFs
- Relative paths: Image references use

Requirements
The script requires:
- Mistral API key in
Notes/.env(line 2:mistral_api_key=...) - Python packages:
mistralai,python-dotenv,pypdf
Common Use Cases
Convert Research Paper
python scripts/convert_pdf_to_markdown.py \
"Data/papers/research.pdf" \
"Notes/Paper Markdown/research.md"Extract Specific Sections
# Extract pages 10-20 (introduction and methods)
python scripts/convert_pdf_to_markdown.py \
"paper.pdf" \
"Notes/Paper Markdown/intro_methods.md" \
--pages "10-20"Extract Figures Only
# Extract pages with figures
python scripts/convert_pdf_to_markdown.py \
"paper.pdf" \
"Notes/Paper Markdown/figures.md" \
--pages "25,27,30,35"Error Handling
API Key Not Found:
Error: Mistral API key not found in Notes/.env→ Add mistral_api_key=YOUR_KEY to line 2 of Notes/.env
Page Out of Range:
Warning: Page 100 out of range, skipping→ Check PDF page count and adjust page selection
API Rate Limit: → Wait a moment and retry, or reduce page count per request
Notes
- Images are saved as JPEG files in
images/subfolder - Markdown image references are automatically updated to
images/img-X.jpeg - Large PDFs may take longer to process due to API limits
- For simple text extraction without OCR, consider using the
pdfskill instead - Scanned PDFs benefit most from this skill's OCR capability
See Also
pdfskill - For local PDF manipulation without API callsreference.md- Additional details about the Mistral OCR API
Mistral PDF to Markdown - Reference Guide
Advanced usage, API details, and troubleshooting for the Mistral OCR PDF converter.
API Details
Mistral OCR API
The conversion uses Mistral's OCR API (mistral-ocr-latest model):
Endpoint: https://api.mistral.ai/v1/ocr
Authentication: Bearer token via MISTRAL_API_KEY
Supported Formats:
- PDF, PPTX, DOCX
- PNG, JPEG, AVIF images
Response Structure
OCRResponse
├── pages: List[OCRPageObject]
│ ├── index: int
│ ├── markdown: str
│ ├── images: List[ImageObject]
│ │ ├── id: str (e.g., "img-0.jpeg")
│ │ ├── top_left_x, top_left_y: float
│ │ ├── bottom_right_x, bottom_right_y: float
│ │ └── image_base64: str (when include_image_base64=True)
│ └── dimensions: OCRPageDimensions
│ ├── dpi: int
│ ├── height: int
│ └── width: int
├── model: str ("mistral-ocr-2505-completion")
├── usage_info: OCRUsageInfo
│ ├── pages_processed: int
│ └── doc_size_bytes: int
└── document_annotation: Optional[Any]Image Data Format
Images are returned in base64-encoded format:
data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD...The script automatically: 1. Strips the data URI prefix (data:image/jpeg;base64,) 2. Decodes the base64 string 3. Saves as JPEG files
Advanced Usage
Programmatic Usage
import sys
sys.path.append('.claude/skills/mistral-pdf-to-markdown/scripts')
from convert_pdf_to_markdown import (
load_api_key,
extract_pages,
process_with_mistral,
save_images
)
# Load API key
api_key = load_api_key()
# Extract pages
base64_pdf = extract_pages("input.pdf", page_selection="1-5")
# Process with Mistral
ocr_response = process_with_mistral(api_key, base64_pdf)
# Custom image handling
for page_idx, page in enumerate(ocr_response.pages):
print(f"Page {page_idx}: {len(page.images)} images")
for img in page.images:
print(f" Image position: ({img.top_left_x}, {img.top_left_y})")Batch Processing
from pathlib import Path
import subprocess
# Process multiple PDFs
pdf_dir = Path("Data/papers")
output_dir = Path("Output/PDFConversions")
for pdf_file in pdf_dir.glob("*.pdf"):
output_file = output_dir / f"{pdf_file.stem}.md"
subprocess.run([
"python",
".claude/skills/mistral-pdf-to-markdown/scripts/convert_pdf_to_markdown.py",
str(pdf_file),
str(output_file)
])Custom Output Location
The script always creates an images/ folder in the same directory as the output markdown file:
# Output structure is automatically created
output_path = Path("custom/location/document.md")
# → Images saved to: custom/location/images/img-*.jpegPerformance Considerations
API Limits
- Rate limits: Check Mistral API documentation for current limits
- File size: Large PDFs (>50 pages) may timeout; use page selection
- Processing time: ~2-5 seconds per page depending on complexity
Optimization Tips
1. Extract specific pages when you only need certain sections:
--pages "10-20" # Only process 10 pages instead of entire document2. Batch similar requests to minimize API overhead
3. Cache results - Save converted markdown to avoid re-processing
Troubleshooting
Common Issues
1. Empty or Missing Images
Symptom: Markdown shows image references but files not saved
Cause: Images may not have image_base64 attribute
Solution: Verify include_image_base64=True in API call
# Check API response
for page in ocr_response.pages:
for img in page.images:
if not hasattr(img, 'image_base64'):
print(f"Warning: Image {img.id} missing base64 data")2. Incorrect Image Paths
Symptom: Markdown shows  but images are in images/
Cause: Path replacement not applied
Solution: The script automatically fixes this with:
markdown_content = markdown_content.replace('](img-', '](images/img-')3. API Authentication Errors
Symptom: 401 Unauthorized error
Causes:
- Invalid API key
- Expired API key
- API key not loaded from
.env
Solutions:
# Verify API key exists
cat Notes/.env | grep mistral_api_key
# Test API key manually
export MISTRAL_API_KEY="your-key-here"
python -c "from mistralai import Mistral; print(Mistral(api_key='$MISTRAL_API_KEY'))"4. Large File Processing
Symptom: Timeout or memory errors with large PDFs
Solutions:
- Extract pages in chunks:
--pages "1-10", then--pages "11-20", etc. - Reduce PDF size before processing (compress images)
- Process locally with
pdfskill for non-OCR needs
Debugging
Enable verbose output:
# Add to script
import logging
logging.basicConfig(level=logging.DEBUG)Check response details:
print(f"Pages processed: {ocr_response.usage_info.pages_processed}")
print(f"Document size: {ocr_response.usage_info.doc_size_bytes} bytes")
print(f"Model: {ocr_response.model}")Comparison with Other Methods
| Feature | Mistral OCR | pypdf | pdfplumber |
|---|---|---|---|
| Text extraction | ✓ Excellent | ✓ Good | ✓ Good |
| Scanned PDFs | ✓ Yes (OCR) | ✗ No | ✗ No |
| Image extraction | ✓ Automatic | ✗ No | ✗ No |
| Markdown output | ✓ Native | ✗ Manual | ✗ Manual |
| Cost | $ API calls | Free | Free |
| Speed | Moderate | Fast | Moderate |
| Formatting | ✓ Excellent | ~ Basic | ✓ Good |
Use Mistral OCR when:
- PDF contains scanned images requiring OCR
- Need Markdown output with formatting
- Want automatic image extraction
- Willing to pay for API usage
Use local tools (`pdf` skill) when:
- Processing many documents (cost savings)
- Simple text extraction sufficient
- No OCR required
- Need faster processing
Example Workflows
Extract Figures from Research Paper
# Step 1: Identify figure pages (manually or via table of contents)
# Assume figures are on pages 15, 18, 22, 25
# Step 2: Extract those pages
python scripts/convert_pdf_to_markdown.py \
"paper.pdf" \
"Output/PDFConversions/paper_figures.md" \
--pages "15,18,22,25"
# Step 3: Images are now in Output/PDFConversions/images/
# Markdown contains captions and referencesConvert Book Chapter
# Chapter 3 is pages 45-78
python scripts/convert_pdf_to_markdown.py \
"book.pdf" \
"Output/PDFConversions/chapter3.md" \
--pages "45-78"Process Scanned Document
# Scanned documents benefit most from OCR
python scripts/convert_pdf_to_markdown.py \
"scanned_contract.pdf" \
"Output/PDFConversions/contract.md"API Cost Estimation
Check Mistral's pricing page for current rates. As of 2025:
- Charged per page processed
- Image extraction may incur additional costs
- Larger pages (higher DPI) may cost more
Example calculation:
- 100-page document
- Only need pages 50-60 (10 pages)
- Use
--pages "50-60"to process only 10 pages instead of 100
Future Enhancements
Potential improvements to the script:
1. Parallel processing - Process multiple pages concurrently 2. Resume capability - Continue from last processed page after interruption 3. Image format options - Save as PNG instead of JPEG 4. Markdown customization - Custom heading levels, formatting styles 5. OCR language detection - Automatic language detection and processing
#!/usr/bin/env python3
"""
Convert PDF to Markdown using Mistral OCR API.
Usage:
python convert_pdf_to_markdown.py <input.pdf> <output.md> [--pages "1-5"]
"""
import argparse
import base64
import os
import sys
from pathlib import Path
from dotenv import load_dotenv
from mistralai import Mistral
from pypdf import PdfReader, PdfWriter
import io
def load_api_key():
"""Load Mistral API key from Notes/.env"""
env_path = Path("Notes/.env")
load_dotenv(env_path)
api_key = os.getenv("mistral_api_key")
if not api_key:
raise ValueError("Mistral API key not found in Notes/.env")
return api_key
def extract_pages(pdf_path, page_selection=None):
"""
Extract specific pages from PDF or return entire PDF as base64.
Args:
pdf_path: Path to PDF file
page_selection: String like "1,3,5" or "1-5" or None for all pages
Returns:
base64-encoded PDF string
"""
if not page_selection:
# Return entire PDF
with open(pdf_path, "rb") as f:
return base64.b64encode(f.read()).decode('utf-8')
# Extract specific pages
reader = PdfReader(pdf_path)
writer = PdfWriter()
# Parse page selection
if '-' in page_selection:
start, end = map(int, page_selection.split('-'))
pages = range(start-1, end) # Convert to 0-indexed
else:
pages = [int(p)-1 for p in page_selection.split(',')] # Convert to 0-indexed
# Add selected pages
for page_num in pages:
if 0 <= page_num < len(reader.pages):
writer.add_page(reader.pages[page_num])
else:
print(f"Warning: Page {page_num+1} out of range, skipping")
# Write to bytes
pdf_bytes = io.BytesIO()
writer.write(pdf_bytes)
pdf_bytes.seek(0)
return base64.b64encode(pdf_bytes.read()).decode('utf-8')
def process_with_mistral(api_key, base64_pdf):
"""
Process PDF with Mistral OCR API.
Args:
api_key: Mistral API key
base64_pdf: Base64-encoded PDF
Returns:
OCR response object
"""
client = Mistral(api_key=api_key)
response = client.ocr.process(
model="mistral-ocr-latest",
document={
"type": "document_url",
"document_url": f"data:application/pdf;base64,{base64_pdf}"
},
include_image_base64=True
)
return response
def save_images(ocr_response, output_path):
"""
Extract and save images from OCR response.
Args:
ocr_response: Mistral OCR response object
output_path: Path to output markdown file
Returns:
Number of images saved
"""
output_dir = Path(output_path).parent
images_dir = output_dir / "images"
images_dir.mkdir(parents=True, exist_ok=True)
image_count = 0
for page_idx, page in enumerate(ocr_response.pages):
if page.images:
for img in page.images:
if hasattr(img, 'image_base64') and img.image_base64:
# Extract image data
img_data = img.image_base64
if img_data.startswith('data:image'):
# Remove data URI prefix
img_data = img_data.split(',', 1)[1]
# Decode and save
img_bytes = base64.b64decode(img_data)
img_filename = f"img-{image_count}.jpeg"
img_path = images_dir / img_filename
with open(img_path, 'wb') as f:
f.write(img_bytes)
print(f" Saved image: {img_path} ({len(img_bytes) / 1024:.1f} KB)")
image_count += 1
return image_count
def convert_pdf_to_markdown(pdf_path, output_path, page_selection=None):
"""
Main conversion function.
Args:
pdf_path: Path to input PDF
output_path: Path to output markdown file
page_selection: Optional page selection string
"""
pdf_path = Path(pdf_path)
output_path = Path(output_path)
print(f"Converting: {pdf_path.name}")
if page_selection:
print(f"Pages: {page_selection}")
# Load API key
print("Loading API key...")
api_key = load_api_key()
# Extract pages
print("Extracting PDF pages...")
base64_pdf = extract_pages(pdf_path, page_selection)
print(f" PDF size: {len(base64_pdf) / 1024:.1f} KB (base64)")
# Process with Mistral
print("Processing with Mistral OCR API...")
ocr_response = process_with_mistral(api_key, base64_pdf)
# Extract markdown
markdown_content = '\n\n---\n\n'.join([page.markdown for page in ocr_response.pages])
# Save images
print("Extracting images...")
image_count = save_images(ocr_response, output_path)
# Fix image paths in markdown
if image_count > 0:
markdown_content = markdown_content.replace('](img-', '](images/img-')
# Save markdown
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'w', encoding='utf-8') as f:
f.write(markdown_content)
# Report results
print(f"\n✓ Conversion complete!")
print(f" Markdown: {output_path}")
print(f" Pages: {len(ocr_response.pages)}")
print(f" Characters: {len(markdown_content)}")
print(f" Images: {image_count}")
def main():
parser = argparse.ArgumentParser(
description="Convert PDF to Markdown using Mistral OCR API"
)
parser.add_argument("input_pdf", help="Input PDF file path")
parser.add_argument("output_md", help="Output Markdown file path")
parser.add_argument("--pages", help='Page selection: "1,3,5" or "1-5"', default=None)
args = parser.parse_args()
try:
convert_pdf_to_markdown(args.input_pdf, args.output_md, args.pages)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()