| name | pdf |
| description | Comprehensive PDF manipulation toolkit with OpenAI Codex integration for intelligent PDF-to-Markdown conversion. IMPORTANT - Always convert PDFs to markdown first using Codex, then process the markdown. Also supports text/table extraction, PDF creation, merging/splitting, and forms. Use for all PDF document processing workflows. |
| version | 1.2.2 |
| last_updated | 2026-01-04T00:00:00.000Z |
| category | document-handling |
| related_skills | ["pdf-text-extractor","document-rag-pipeline","knowledge-base-builder"] |
PDF Processing Skill
Overview
This skill enables comprehensive PDF operations through Python libraries and command-line tools. Use it for reading, creating, modifying, and analyzing PDF documents.
Quick Start
from pypdf import PdfReader
reader = PdfReader("document.pdf")
for page in reader.pages:
text = page.extract_text()
print(text)
When to Use
- Converting PDFs to Markdown - Use OpenAI Codex for intelligent conversion (RECOMMENDED FIRST STEP)
- Extracting text and metadata from PDF files
- Merging multiple PDFs into a single document
- Splitting large PDFs into individual pages
- Adding watermarks or annotations to PDFs
- Password-protecting or decrypting PDFs
- Extracting images from PDF documents
- OCR processing for scanned documents
- Creating new PDFs with reportlab
- Extracting tables from structured PDFs
PDF to Markdown Conversion (OpenAI Codex)
IMPORTANT: For all PDF documents, utilize OpenAI Codex to convert contents to .md file first, then use the markdown for further work.
Why Convert to Markdown First?
- Better structure preservation - Maintains headings, lists, tables
- Easier text processing - Standard markdown format
- Improved AI understanding - Codex understands document structure
- Format flexibility - Markdown can be converted to any format
- Version control friendly - Plain text, diff-friendly
OpenAI Codex Conversion
Prerequisites:
pip install openai pypdf
export OPENAI_API_KEY="your-api-key-here"
Basic Conversion:
import openai
from pypdf import PdfReader
from pathlib import Path
def pdf_to_markdown_codex(pdf_path, output_md_path=None, model="gpt-4"):
"""
Convert PDF to markdown using OpenAI Codex.
Args:
pdf_path: Path to PDF file
output_md_path: Optional path for output .md file (auto-generated if None)
model: OpenAI model to use (gpt-4, gpt-3.5-turbo, etc.)
Returns:
Path to generated markdown file
"""
reader = PdfReader(pdf_path)
pdf_text = ""
for page_num, page in enumerate(reader.pages, 1):
text = page.extract_text()
pdf_text += f"\n\n--- Page {page_num} ---\n\n{text}"
client = openai.OpenAI()
response = client.chat.completions.create(
model=model,
messages=[
{
"role": "system",
"content": """You are an expert document converter. Convert the provided PDF text
to well-structured markdown format. Preserve:
- Document structure (headings, sections)
- Lists and bullet points
- Tables (convert to markdown tables)
- Code blocks and technical content
- Links and references
Format the output as clean, readable markdown."""
},
{
"role": "user",
"content": f"Convert this PDF text to markdown:\n\n{pdf_text}"
}
],
temperature=0.3,
)
markdown_content = response.choices[].message.content
output_md_path :
pdf_stem = Path(pdf_path).stem
output_md_path = Path(pdf_path).parent /
Path(output_md_path).parent.mkdir(parents=, exist_ok=)
Path(output_md_path).write_text(markdown_content, encoding=)
output_md_path
md_file = pdf_to_markdown_codex()
()
Batch Conversion:
from pathlib import Path
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def batch_pdf_to_markdown(pdf_directory, output_directory=None, model="gpt-4"):
"""
Convert all PDFs in a directory to markdown.
Args:
pdf_directory: Directory containing PDF files
output_directory: Optional output directory (defaults to pdf_directory/markdown)
model: OpenAI model to use
"""
pdf_dir = Path(pdf_directory)
if output_directory is None:
output_dir = pdf_dir / "markdown"
else:
output_dir = Path(output_directory)
output_dir.mkdir(parents=True, exist_ok=True)
pdf_files = list(pdf_dir.glob("*.pdf"))
total = len(pdf_files)
logger.info(f"Found {total} PDF files to convert")
for i, pdf_file in enumerate(pdf_files, 1):
try:
output_md = output_dir / f"{pdf_file.stem}.md"
logger.info(f"[{i}/{total}] Converting {pdf_file.name}...")
pdf_to_markdown_codex(pdf_file, output_md, model=model)
logger.info(f"✓ Saved to {output_md.name}")
except Exception as e:
logger.error(f"✗ Failed to convert {pdf_file.name}: {e}")
logger.info(f"\nConversion complete! Files in: {output_dir}")
batch_pdf_to_markdown(, model=)
Chunked Conversion for Large PDFs:
def pdf_to_markdown_chunked(pdf_path, output_md_path=None,
chunk_pages=10, model="gpt-4"):
"""
Convert large PDF by processing in chunks.
Args:
pdf_path: Path to PDF file
output_md_path: Optional output path
chunk_pages: Number of pages per chunk
model: OpenAI model to use
"""
reader = PdfReader(pdf_path)
total_pages = len(reader.pages)
markdown_sections = []
for start_page in range(0, total_pages, chunk_pages):
end_page = min(start_page + chunk_pages, total_pages)
chunk_text = ""
for page_num in range(start_page, end_page):
text = reader.pages[page_num].extract_text()
chunk_text += f"\n\n--- Page {page_num + 1} ---\n\n{text}"
client = openai.OpenAI()
response = client.chat.completions.create(
model=model,
messages=[
{
"role": "system",
"content": "Convert PDF text to markdown. Maintain structure and formatting."
},
{
"role": "user",
"content": f"Convert pages {start_page + 1}-{end_page} to markdown:\n\n{chunk_text}"
}
],
temperature=0.3,
)
markdown_sections.append(response.choices[0].message.content)
()
full_markdown = .join(markdown_sections)
output_md_path :
output_md_path = Path(pdf_path).with_suffix()
Path(output_md_path).parent.mkdir(parents=, exist_ok=)
Path(output_md_path).write_text(full_markdown, encoding=)
output_md_path
md_file = pdf_to_markdown_chunked(, chunk_pages=)
Workflow: PDF → Markdown → Further Processing:
from pathlib import Path
def pdf_workflow(pdf_path):
"""
Complete workflow: PDF → Markdown → Process markdown.
Returns:
dict with paths to original PDF, markdown, and processed content
"""
print("Step 1: Converting PDF to markdown...")
md_path = pdf_to_markdown_codex(pdf_path)
print("Step 2: Reading markdown content...")
markdown_content = Path(md_path).read_text(encoding='utf-8')
print("Step 3: Processing markdown...")
headings = [line for line in markdown_content.split('\n') if line.startswith('#')]
word_count = len(markdown_content.split())
return {
'pdf_path': pdf_path,
'markdown_path': md_path,
'markdown_content': markdown_content,
'headings': headings,
'word_count': word_count,
}
result = pdf_workflow("technical_document.pdf")
print(f"Markdown saved: {result['markdown_path']}")
print(f"Found {len(result['headings'])} headings")
print(f"Word count: ")
(result[]) f:
markdown = f.read()
Cost-Effective Options:
md_file = pdf_to_markdown_codex("document.pdf", model="gpt-3.5-turbo")
from pypdf import PdfReader
def hybrid_conversion(pdf_path):
"""Extract text locally, use Codex only for formatting."""
reader = PdfReader(pdf_path)
raw_text = ""
for page in reader.pages:
raw_text += page.extract_text()
client = openai.OpenAI()
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{
"role": "system",
"content": "Format the text as markdown. Add appropriate headings, lists, and structure."
},
{
"role": "user",
"content": raw_text
}
],
temperature=0.3,
)
markdown = response.choices[0].message.content
output_path = Path(pdf_path).with_suffix('.md')
output_path.write_text(markdown, encoding='utf-8')
return output_path
Best Practices:
- Always convert to markdown first - Makes downstream processing easier
- Use chunking for large PDFs - Avoids token limits and API timeouts
- Cache conversions - Store markdown files to avoid re-conversion
- Choose model based on complexity - GPT-4 for complex docs, GPT-3.5 for simple ones
- Validate output - Check that markdown structure makes sense
- Handle errors gracefully - Log failures, continue batch processing
CLI Tool:
"""PDF to Markdown converter using OpenAI Codex."""
import argparse
import logging
from pathlib import Path
import openai
from pypdf import PdfReader
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def pdf_to_markdown_codex(pdf_path, output_md_path=None, model="gpt-4"):
"""
Convert PDF to markdown using OpenAI Codex.
Args:
pdf_path: Path to PDF file
output_md_path: Optional path for output .md file (auto-generated if None)
model: OpenAI model to use (gpt-4, gpt-3.5-turbo, etc.)
Returns:
Path to generated markdown file
"""
reader = PdfReader(pdf_path)
pdf_text = ""
for page_num, page in enumerate(reader.pages, 1):
text = page.extract_text()
pdf_text += f"\n\n--- Page {page_num} ---\n\n{text}"
client = openai.OpenAI()
response = client.chat.completions.create(
model=model,
messages=[
{
"role": "system",
"content": """You are an expert document converter. Convert the provided PDF text
to well-structured markdown format. Preserve:
- Document structure (headings, sections)
- Lists and bullet points
- Tables (convert to markdown tables)
- Code blocks and technical content
- Links and references
Format the output as clean, readable markdown."""
},
{
"role": "user",
"content":
}
],
temperature=,
)
markdown_content = response.choices[].message.content
output_md_path :
pdf_stem = Path(pdf_path).stem
output_md_path = Path(pdf_path).parent /
Path(output_md_path).parent.mkdir(parents=, exist_ok=)
Path(output_md_path).write_text(markdown_content, encoding=)
output_md_path
():
pdf_dir = Path(pdf_directory)
output_directory :
output_dir = pdf_dir /
:
output_dir = Path(output_directory)
output_dir.mkdir(parents=, exist_ok=)
pdf_files = (pdf_dir.glob())
total = (pdf_files)
logger.info()
i, pdf_file (pdf_files, ):
:
output_md = output_dir /
logger.info()
pdf_to_markdown_codex(pdf_file, output_md, model=model)
logger.info()
Exception e:
logger.error()
logger.info()
():
parser = argparse.ArgumentParser(description=)
parser.add_argument(, =)
parser.add_argument(, , =)
parser.add_argument(, , default=, =)
parser.add_argument(, =, default=, =)
args = parser.parse_args()
input_path = Path(args.)
input_path.is_file():
output = args.output input_path.with_suffix()
md_path = pdf_to_markdown_codex(input_path, output, model=args.model)
()
:
batch_pdf_to_markdown(input_path, args.output, model=args.model)
__name__ == :
main()
Save as pdf2md.py and use:
python pdf2md.py document.pdf
python pdf2md.py /path/to/pdfs -o /path/to/markdown
python pdf2md.py document.pdf --model gpt-3.5-turbo
Python Libraries
pypdf - Core PDF Operations
Merging PDFs:
from pypdf import PdfMerger
merger = PdfMerger()
merger.append("file1.pdf")
merger.append("file2.pdf")
merger.write("merged.pdf")
merger.close()
Splitting PDFs:
from pypdf import PdfReader, PdfWriter
reader = PdfReader("document.pdf")
for i, page in enumerate(reader.pages):
writer = PdfWriter()
writer.add_page(page)
writer.write(f"page_{i+1}.pdf")
Extracting Metadata:
reader = PdfReader("document.pdf")
info = reader.metadata
print(f"Author: {info.author}")
print(f"Title: {info.title}")
print(f"Pages: {len(reader.pages)}")
pdfplumber - Advanced Text Extraction
Text with Layout Preservation:
import pdfplumber
with pdfplumber.open("document.pdf") as pdf:
for page in pdf.pages:
text = page.extract_text()
print(text)
Table Extraction:
import pdfplumber
with pdfplumber.open("document.pdf") as pdf:
page = pdf.pages[0]
tables = page.extract_tables()
for table in tables:
for row in table:
print(row)
reportlab - Creating PDFs
Create PDF from Scratch:
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
c = canvas.Canvas("output.pdf", pagesize=letter)
c.drawString(100, 750, "Hello, World!")
c.showPage()
c.save()
Multi-page Documents:
from reportlab.lib.pagesizes import letter
from reportlab.platypus import SimpleDocTemplate, Paragraph
from reportlab.lib.styles import getSampleStyleSheet
doc = SimpleDocTemplate("output.pdf", pagesize=letter)
styles = getSampleStyleSheet()
story = []
story.append(Paragraph("Title", styles['Heading1']))
story.append(Paragraph("Body text here.", styles['Normal']))
doc.build(story)
Command-Line Tools
pdftotext (Poppler)
pdftotext document.pdf output.txt
pdftotext -layout document.pdf output.txt
qpdf
qpdf --empty --pages file1.pdf file2.pdf -- merged.pdf
qpdf document.pdf --pages . 1-5 -- first_five.pdf
qpdf --decrypt encrypted.pdf decrypted.pdf
pdftk
pdftk file1.pdf file2.pdf cat output merged.pdf
pdftk document.pdf burst output page_%02d.pdf
pdftk document.pdf cat 1-endeast output rotated.pdf
PDF-Large-Reader - Robust Extraction for Large Files
For large PDFs (100MB+, 1000+ pages), use the pdf-large-reader library with robust table extraction.
Why Use PDF-Large-Reader?
- Memory-efficient - Handles 100MB+ PDFs without memory issues
- Robust table extraction - Handles irregular tables with column count normalization
- Multiple output formats - Generator (streaming), List, or Plain Text
- Automatic strategy selection - Intelligent chunk size calculation
- Complete extraction - Text, images, tables, and metadata in one pass
- High test coverage - 93.58% coverage with 215 tests
Installation
cd /mnt/github/workspace-hub/pdf-large-reader
pip install -e .
pip install -e ".[dev,progress]"
Quick Start
from pdf_large_reader import process_large_pdf, extract_text_only, extract_everything
text = extract_text_only("large_document.pdf")
print(text)
pages = process_large_pdf(
"large_document.pdf",
output_format="list",
extract_images=True,
extract_tables=True
)
for page in process_large_pdf("huge_file.pdf", output_format="generator"):
print(f"Page {page.page_number}: {len(page.text)} characters")
Robust Table Extraction
NEW: Column Count Normalization (v1.3.0+)
The table extraction now handles irregular tables with different column counts:
from pdf_large_reader import extract_everything
pages = extract_everything("technical_standard.pdf")
for page in pages:
if 'tables' in page.metadata:
tables = page.metadata['tables']
print(f"Page {page.page_number}: Found {len(tables)} tables")
for i, table_df in enumerate(tables):
print(f" Table {i+1}: {table_df.shape[0]} rows x {table_df.shape[1]} cols")
print(table_df.head())
How It Works:
- Detects table-like structures from text positioning
- Normalizes column counts across all rows
- Pads short rows with empty strings
- Gracefully handles malformed tables with try-except
- Logs warnings instead of crashing
Typical Performance:
- API Std 650 (28 MB, 461 pages): 14,648 chars/sec, 5.18 pages/sec
- API RP 579 (41 MB, 966 pages): 2,090 chars/sec, 8.48 pages/sec
Command Line Usage
pdf-large-reader document.pdf
pdf-large-reader document.pdf --output result.txt
pdf-large-reader document.pdf --extract-images --extract-tables
pdf-large-reader huge.pdf --output-format generator
pdf-large-reader document.pdf --verbose
API Reference
process_large_pdf(
pdf_path,
output_format="generator",
extract_images=False,
extract_tables=False,
chunk_size=None,
fallback_api_key=None,
fallback_model="gpt-4o",
progress_callback=None,
auto_strategy=True
)
extract_text_only(pdf_path) -> str
extract_pages_with_images(pdf_path) -> List[PDFPage]
extract_pages_with_tables(pdf_path) -> List[PDFPage]
extract_everything(pdf_path) -> List[PDFPage]
PDFPage Data Structure
@dataclass
class PDFPage:
page_number: int
text: str
images: List[dict]
metadata: dict
Performance Benchmarks
Tested on Ubuntu 22.04, Python 3.11, 16GB RAM:
| File Size | Pages | Time | Memory | Strategy |
|---|
| 5 MB | 10 | < 5s | ~50 MB | batch_all |
| 50 MB | 100 | < 30s | ~150 MB | chunked |
| 100 MB | 500 | < 60s | ~200 MB | stream_pages |
| 200 MB | 1000 | < 2min | ~250 MB | stream_pages |
Real-World Validation
Tested with actual API standards:
- ✅ API RP 579 (2000) - 41 MB, 966 pages
- ✅ API Std 650 (2001) - 28 MB, 461 pages
- ✅ All extraction methods working (text, auto strategy, generator, complete)
- ✅ Table extraction with column normalization
- ✅ Image extraction (461-966 images per document)
Common Tasks
OCR for Scanned Documents
import pytesseract
from pdf2image import convert_from_path
images = convert_from_path("scanned.pdf")
for i, image in enumerate(images):
text = pytesseract.image_to_string(image)
print(f"Page {i+1}:\n{text}")
Add Watermark
from pypdf import PdfReader, PdfWriter
reader = PdfReader("document.pdf")
watermark = PdfReader("watermark.pdf")
writer = PdfWriter()
for page in reader.pages:
page.merge_page(watermark.pages[0])
writer.add_page(page)
writer.write("watermarked.pdf")
Extract Images
from pypdf import PdfReader
reader = PdfReader("document.pdf")
for page_num, page in enumerate(reader.pages):
for img_num, image in enumerate(page.images):
with open(f"image_{page_num}_{img_num}.png", "wb") as f:
f.write(image.data)
Password Protection
from pypdf import PdfReader, PdfWriter
reader = PdfReader("document.pdf")
writer = PdfWriter()
for page in reader.pages:
writer.add_page(page)
writer.encrypt("user_password", "owner_password")
writer.write("protected.pdf")
Execution Checklist
Error Handling
Common Errors
Error: FileNotFoundError
- Cause: PDF file path is incorrect
- Solution: Verify file path and ensure file exists
Error: PdfReadError (encrypted)
- Cause: PDF is password-protected or DRM-encrypted
- Solution: Provide password or use qpdf to decrypt
Error: Empty text extraction
- Cause: PDF contains scanned images, not text
- Solution: Use OCR with pytesseract and pdf2image
Error: DependencyError (Tesseract)
- Cause: Tesseract OCR not installed
- Solution:
sudo apt-get install tesseract-ocr or brew install tesseract
Metrics
| Metric | Typical Value |
|---|
| Text extraction speed | ~50 pages/second |
| OCR processing speed | ~2-5 pages/minute |
| Memory usage (pypdf) | ~10MB per 100 pages |
| Merge operation | ~100 PDFs/second |
Quick Reference
| Task | Tool |
|---|
| Read text | pypdf, pdfplumber |
| Extract tables | pdfplumber |
| Create PDFs | reportlab |
| Merge/split | pypdf, qpdf, pdftk |
| OCR | pytesseract + pdf2image |
| Fill forms | pypdf, pdfrw |
| Watermark | pypdf |
| Encrypt/decrypt | pypdf, qpdf |
Dependencies
pip install pypdf pdfplumber reportlab pytesseract pdf2image
pip install openai
System tools:
- Poppler (pdftotext, pdftoppm)
- qpdf
- pdftk
- Tesseract OCR
Environment variables:
export OPENAI_API_KEY="your-api-key-here"
Version History
- 1.2.2 (2026-01-04): Fixed P2 issue - added
parents=True to all mkdir() calls to handle nested output paths; prevents FileNotFoundError when creating directories with non-existent parent paths
- 1.2.1 (2026-01-04): Fixed CLI tool missing imports - added complete standalone script with all required imports (openai, pypdf, logging) and function definitions; resolved P1 issue from Codex review
- 1.2.0 (2026-01-04): MAJOR UPDATE - Added OpenAI Codex integration for PDF-to-Markdown conversion as recommended first step for all PDF processing; includes batch conversion, chunking for large files, cost-effective options, and complete CLI tool
- 1.1.0 (2026-01-02): Added Quick Start, When to Use, Execution Checklist, Error Handling, Metrics sections; updated frontmatter with version, category, related_skills
- 1.0.0 (2024-10-15): Initial release with pypdf, pdfplumber, reportlab, CLI tools