| name | pdf-extraction |
| description | Extract text, tables, and structured data from PDFs. Use when processing any PDF for text extraction, table extraction, search, metadata, or conversion to markdown. Supports text-based and scanned/image PDFs with OCR. |
PDF Extraction
Extract text, tables, metadata, and structured information from PDF documents.
When to Use
- Pulling text, tables, or metadata from any PDF
- Converting PDFs to markdown for LLM context or RAG pipelines
- Searching within PDF content for specific sections or values
- Processing scanned/image-based PDFs with OCR
For domain-specific extraction, combine with:
- datasheet-extraction — electronics/component datasheets
- paper-extraction — academic/scientific papers
Image & Figure Extraction (Do This First)
Whenever the PDF contains images, diagrams, charts, or figures, extract them to PNGs before any OCR or analysis. Saving the raw PNGs gives you a ground-truth reference to review and correct extraction results.
uv run ~/.config/opencode/skills/pdf-extraction/scripts/extract_figures.py input.pdf
uv run ~/.config/opencode/skills/pdf-extraction/scripts/extract_figures.py input.pdf my_figures/
Output naming: page{N:04d}_img{M:03d}.png for raster images, page{N:04d}_fig{M:03d}.png for vector/drawing regions.
Why first?
- OCR and extraction results can silently degrade or miss content; the PNG is the unambiguous source of truth
- Reviewing the PNGs lets you catch mis-detected regions, missed figures, or corrupt images before investing in downstream processing
- Figures directory doubles as a structured asset cache for any future re-extraction
After extraction, use the saved PNGs directly as input to OCR (pass them to Mistral OCR, pytesseract, or an LLM vision call) rather than re-extracting from the PDF.
Tool Selection
| Scenario | Tool | Why |
|---|
| Fast text extraction | pymupdf4llm | Fastest, LLM-optimized markdown |
| Table extraction | pdfplumber | Best table detection and extraction |
| Scanned PDFs (API) | Mistral OCR | Best accuracy on scans, complex layouts |
| Scanned PDFs (local) | pytesseract + pdf2image | Free, offline, requires system deps |
| RAG / chunking | pymupdf4llm with page_chunks=True | Returns chunks with metadata |
| Figure/image extraction | extract_figures.py | Always run first when figures are present |
Decision Flow
- Does the PDF contain figures, diagrams, or images? → Extract figures first (
extract_figures.py)
- Is it scanned / image-based? → OCR path (Mistral OCR or pytesseract) — feed saved PNGs as input
- Need tables? → pdfplumber
- Need speed or markdown? → pymupdf4llm
Scripts
All scripts are standalone, uv-runnable with inline dependencies (PEP 723). No pre-install needed.
uv run scripts/extract_figures.py input.pdf
uv run scripts/extract_figures.py input.pdf my_figures/
uv run scripts/extract_pymupdf.py input.pdf output.md
uv run scripts/extract_pdfplumber.py input.pdf output.md
MISTRAL_API_KEY="..." uv run scripts/extract_mistral_ocr.py input.pdf output.md
uv run scripts/extract_with_ocr.py input.pdf output.txt
Scripts are in scripts/ relative to this skill. Use absolute paths:
uv run ~/.config/opencode/skills/pdf-extraction/scripts/extract_figures.py input.pdf
Inline Extraction Patterns
Quick Text to Markdown
import pymupdf4llm
md = pymupdf4llm.to_markdown("document.pdf")
md = pymupdf4llm.to_markdown("document.pdf", pages=[0, 1, 2])
chunks = pymupdf4llm.to_markdown("document.pdf", page_chunks=True)
for chunk in chunks:
print(f"Page {chunk['metadata']['page']}: {chunk['text'][:200]}...")
Table Extraction
import pdfplumber
with pdfplumber.open("document.pdf") as pdf:
for page in pdf.pages:
tables = page.extract_tables()
for table in tables:
for row in table:
print(row)
Search Within PDF
import pdfplumber
def search_pdf(pdf_path, query, case_insensitive=True):
"""Find pages containing query text."""
results = []
with pdfplumber.open(pdf_path) as pdf:
for i, page in enumerate(pdf.pages):
text = page.extract_text() or ""
match = query.lower() in text.lower() if case_insensitive else query in text
if match:
results.append({"page": i + 1, "text": text})
return results
Extract by Region (Crop)
with pdfplumber.open("document.pdf") as pdf:
page = pdf.pages[0]
region = page.crop((0, 0, page.width, 150))
print(region.extract_text())
Extract by Font Size
def extract_by_font_size(page, min_size=14):
"""Extract text in larger fonts (likely headings)."""
chars = page.chars
large = [c for c in chars if c['size'] >= min_size]
if not large:
return []
lines, current = [], large[0]['text']
for prev, c in zip(large, large[1:]):
if abs(c['top'] - prev['top']) < 2:
current += c['text']
else:
lines.append(current.strip())
current = c['text']
lines.append(current.strip())
return [l for l in lines if l]
Metadata
import pdfplumber
with pdfplumber.open("document.pdf") as pdf:
meta = pdf.metadata
print(f"Title: {meta.get('Title')}")
print(f"Author: {meta.get('Author')}")
print(f"Pages: {len(pdf.pages)}")
OCR for Scanned Documents
Mistral OCR API (Recommended)
Best accuracy on complex layouts and scanned documents (98%+). ~$1/1000 pages.
from mistralai import Mistral
import base64
client = Mistral(api_key="your-key")
response = client.ocr.process(
model="mistral-ocr-latest",
document={"type": "document_url", "document_url": "https://example.com/doc.pdf"}
)
with open("scanned.pdf", "rb") as f:
b64 = base64.standard_b64encode(f.read()).decode()
response = client.ocr.process(
model="mistral-ocr-latest",
document={"type": "base64", "base64": b64}
)
for page in response.pages:
print(page.markdown)
Local OCR (Tesseract)
Free, offline. Requires: apt install tesseract-ocr poppler-utils.
import pytesseract
from pdf2image import convert_from_path
images = convert_from_path("scanned.pdf", dpi=300)
for i, img in enumerate(images):
text = pytesseract.image_to_string(img, lang="eng")
print(f"Page {i+1}: {text[:200]}...")
References