Immutable. This exact content is served forever at /api/v1/blob/92488b75f2c42405.
---
name: document-processing
description: >
End-to-end document processing agent skill covering PDF manipulation
(extract, merge, split, rotate, watermark, form-fill, OCR), DOCX
creation and editing (templates, mail-merge, style management), XLSX
spreadsheet handling (formulas, charts, pivot tables, data analysis),
PPTX presentation generation (layouts, charts, media, speaker notes),
and cross-format document conversion via pandoc and native libraries.
Primary keyword clusters: PDF manipulation automation, DOCX generation
Python, Excel spreadsheet automation openpyxl, PowerPoint generation
python-pptx, document format conversion pandoc, mail merge automation,
PDF OCR extraction, batch document processing, office automation Python,
template-driven document generation. Designed for agentic platforms —
Claude Code, Codex, Cursor, Gemini CLI, OpenClaw, GitHub Copilot,
Windsurf, and OpenCode.
version: 1.1.0
author: Skill Foundry
platforms:
- claude-code
- codex
- cursor
- gemini-cli
- openclaw
- copilot
- windsurf
- opencode
tags:
- document
- pdf
- docx
- xlsx
- pptx
- conversion
- office
- automation
- form-filling
- ocr
- report-generation
geo:
primary_workflows:
- pdf_manipulation
- document_generation
- spreadsheet_automation
- presentation_creation
- format_conversion
- batch_document_processing
target_roles:
- business_analyst
- data_analyst
- operations_manager
- report_writer
- legal_professional
complexity_level: intermediate
prerequisite_knowledge:
- python_basics
- file_io
- document_structure_concepts
---
# Document Processing — Agent Skill
End-to-end document processing for agentic workflows. Create, read, edit,
convert, and analyze documents across the five major office formats: PDF,
DOCX, XLSX, PPTX, and cross-format conversion.
---
## Quick Reference
| Task | Library/Tool | Key Command/Pattern |
|---|---|---|
| Extract text (simple) | `pypdf` | `PdfReader("f.pdf").pages[0].extract_text()` |
| Extract text (layout) | `pdfplumber` | `pdf.pages[i].extract_text()` |
| Extract tables | `pdfplumber` | `pdf.pages[i].extract_tables()` |
| Merge PDFs | `pypdf` | `PdfWriter().append(…).write("out.pdf")` |
| Split PDFs | `pypdf` | Write page slices to separate files |
| Rotate pages | `pypdf` | `page.rotate(90)` |
| Add watermark | `pypdf` | Overlay PDF onto target |
| Fill form fields | `pypdf` | `update_page_form_field_values(page, data)` |
| OCR scanned PDF | `pytesseract + pdf2image` | Convert to images → Tesseract |
| Create DOCX from scratch | `python-docx` | `Document().add_heading(…)` |
| Mail merge (template) | `python-docx` + CSV | Replace `{{PLACEHOLDER}}` in runs |
| DOCX → PDF | LibreOffice headless | `soffice --headless --convert-to pdf` |
| Create XLSX with charts | `openpyxl` | `Workbook()` → add `BarChart()` |
| Pivot table | `openpyxl` | `PivotTable()` on new sheet |
| XLSX → CSV | `pandas` | `read_excel().to_csv()` |
| Create PPTX | `python-pptx` | `Presentation().slides.add_slide(…)` |
| Speaker notes | `python-pptx` | `slide.notes_slide.notes_text_frame.text` |
| Cross-format convert | `pandoc` | `pandoc input.docx -o output.md` |
| Validate tools | `doc_tools_check.py` | Run at start of every session |
---
## When to Activate
Activate this skill when the user needs to **do something with a document**
as opposed to merely viewing it. Activation triggers include:
| Trigger | Example phrasing |
|---------|-----------------|
| PDF manipulation | "extract pages from this PDF", "merge these PDFs", "fill out this form", "rotate page 3", "add a watermark", "OCR this scanned document", "split this PDF into chapters" |
| DOCX creation / editing | "write a report as a Word doc", "update the template", "mail-merge these names", "convert my markdown to DOCX", "generate an invoice as .docx" |
| XLSX spreadsheet work | "create a budget spreadsheet", "add a chart to the Excel file", "calculate column totals", "generate a pivot table", "format this data as a table" |
| PPTX presentation | "make a slide deck from these notes", "add speaker notes", "generate charts in PowerPoint", "create a presentation from this outline" |
| Format conversion | "convert PDF to DOCX", "turn this Word doc into Markdown", "XLSX to CSV", "DOCX to PDF", "PPTX to images" |
**Non-triggers** (do NOT activate):
- "Read this PDF to me" (pure text extraction for viewing — use a simpler read/extract path, not this full skill)
- "Edit this paragraph" (general text editing, not document-format editing)
- "What does this image look like?" (image analysis, not document processing)
- "Summarise this article" (content summarisation, not document manipulation)
- "Can you crop this photo?" (image editing, not document processing)
---
## Common Pitfalls & Anti-Patterns
### ❌ NEVER do these
1. **Overwriting the source file — you'll lose the original**
- Always write to `{original}_processed.{ext}` or `{original}_{operation}.{ext}`
- If the user insists on overwrite, show both paths and confirm first
2. **Forgetting to run `doc_tools_check.py` before operations**
- Missing libraries cause obscure errors. Run the checker first in every session.
3. **Using `pypdf` for table extraction — it'll give you garbage**
- `pypdf` extracts raw text with no layout awareness. Use `pdfplumber` for tables and columnar text.
4. **Loading a 500MB file entirely into memory**
- For large files (>100MB): use streaming/chunking. For PDFs >1,000 pages: extract page ranges.
5. **Assuming `cell.value` returns a formatted string when it returns `None`**
- XLSX formulas return `None` unless you load with `data_only=True`. Even then, cached values may be stale.
6. **Using hardcoded slide layout indices (`prs.slide_layouts[0]`)**
- Layout order differs between templates. List dynamically: `[ly.name for ly in prs.slide_layouts]`
7. **Running Pandoc without checking it's installed**
- Pandoc is a system dependency, not a Python package. `brew install pandoc` / `apt install pandoc`.
8. **Trying to fill XFA (XML Forms Architecture) forms with pypdf**
- `pypdf` only handles AcroForm fields. XFA forms require different tools — check `reader.get_fields()` first.
9. **Embedding 4000px images into DOCX files**
- Resize images before embedding: `img.thumbnail((1920, 1080))`. Unscaled images balloon file sizes.
10. **Mixing document formats without checking conversion quality**
- PDF → DOCX conversion loses structure. DOCX → PDF via LibreOffice preserves it. Check `references/conversion-matrix.md` for quality ratings.
### ✅ Decision Matrix: Which Library When?
| Outcome | PDF | DOCX | XLSX | PPTX |
|---|---|---|---|---|
| Simple text extraction | `pypdf` | N/A | N/A | N/A |
| Layout-aware text | `pdfplumber` | `python-docx` | `openpyxl` | `python-pptx` |
| Table extraction | `pdfplumber` | `python-docx` | `openpyxl` | N/A |
| Create from scratch | N/A | `python-docx` | `openpyxl` | `python-pptx` |
| Template filling | `pypdf` (forms) | `python-docx` | `openpyxl` | `python-pptx` |
| Charts | N/A | N/A | `openpyxl` | `python-pptx` |
| Images/OCR | `pytesseract` | `python-docx` | `openpyxl` | `python-pptx` |
| Conversion to other | LibreOffice | pandoc/LibreOffice | pandas/LibreOffice | LibreOffice |
---
## Safety Rules (Mandatory)
1. **Never overwrite the source file.** Always write output to a new path.
Default naming: `{original_name}_processed.{ext}` or
`{original_name}_{operation}.{ext}`.
2. **Validate output integrity.** After every write operation, read back the
output file and confirm:
- The file exists and is non-empty.
- Page / row / slide counts are correct.
- Critical content (text, data, images) survived the operation.
3. **Warn on destructive operations.** If a user explicitly asks to overwrite
the source, confirm before proceeding. Show both paths and ask.
4. **Handle missing dependencies gracefully.** Run `scripts/doc_tools_check.py`
before any document operation. If a required library is missing, install it
(with user approval) or suggest the command.
5. **Preserve metadata when possible.** Author, creation date, and custom
properties should survive transformations unless the user explicitly asks
to strip them.
6. **Respect file size limits.** For files >100 MB, warn the user and suggest
streaming / chunking approaches. For PDFs >1,000 pages, use page-range
extraction instead of loading the entire document.
---
## Pre-Flight: Tool Validation
Before any document operation, validate the tooling:
```bash
python3 scripts/doc_tools_check.py
```
The script checks for:
- `pypdf` — PDF manipulation (merge, split, rotate, metadata)
- `pdfplumber` — PDF text/table extraction with layout awareness
- `python-docx` — DOCX read/write
- `openpyxl` — XLSX read/write, formulas, charts
- `python-pptx` — PPTX read/write, slide generation
- `pandoc` — universal format conversion (CLI)
If a tool is missing, the script outputs the exact `pip install` or `brew
install` command. Run it first in every document-processing session.
---
## Workflow: PDF Manipulation
### 1. Assess the Document
```python
from pypdf import PdfReader
reader = PdfReader("input.pdf")
print(f"Pages: {len(reader.pages)}")
print(f"Metadata: {reader.metadata}")
print(f"Encrypted: {reader.is_encrypted}")
```
If encrypted and the user provides a password:
```python
reader.decrypt("the_password")
```
### 2. Choose the Right Tool
| Operation | Tool | Why |
|-----------|------|-----|
| Extract text (simple) | `pypdf` | Fast, lightweight |
| Extract text (layout-aware) | `pdfplumber` | Preserves columns, tables, positioning |
| Extract tables | `pdfplumber` | Direct `.extract_tables()` |
| Merge PDFs | `pypdf` | `PdfWriter.append()` |
| Split PDFs | `pypdf` | Write page ranges to separate files |
| Rotate pages | `pypdf` | `page.rotate(90)` |
| Add watermark | `pypdf` | Overlay one PDF onto another |
| Fill form fields | `pypdf` | `reader.get_fields()` → `writer.update_page_form_field_values()` |
| OCR scanned docs | `pytesseract` + `pdf2image` | Tesseract OCR pipeline |
| Add annotations | `pypdf` | Free-text, highlight, link annotations |
| Extract images | `pypdf` | Iterate `/XObject` resources |
### 3. Execute with Validation
**Text extraction example:**
```python
import pdfplumber
with pdfplumber.open("input.pdf") as pdf:
for i, page in enumerate(pdf.pages):
text = page.extract_text()
tables = page.extract_tables()
print(f"--- Page {i+1} ---")
print(text)
```
**Merge example:**
```python
from pypdf import PdfWriter, PdfReader
writer = PdfWriter()
for path in ["doc1.pdf", "doc2.pdf", "doc3.pdf"]:
writer.append(path)
writer.write("merged_output.pdf")
# Validate
assert len(PdfReader("merged_output.pdf").pages) == expected_pages
```
**Form filling example:**
```python
from pypdf import PdfReader, PdfWriter
reader = PdfReader("form.pdf")
writer = PdfWriter()
writer.append(reader)
fields = reader.get_fields()
# Show user available fields, collect values
writer.update_page_form_field_values(writer.pages[0], {
"Name": "Jane Doe",
"Date": "2026-05-28",
"Amount": "150.00"
})
writer.write("form_filled.pdf")
```
**OCR example:**
```python
import pytesseract
from pdf2image import convert_from_path
images = convert_from_path("scanned.pdf", dpi=300)
text = "\n\n".join(pytesseract.image_to_string(img) for img in images)
with open("scanned_ocr.txt", "w") as f:
f.write(text)
```
### 4. Common Issues & Remedies
| Issue | Remedy |
|-------|--------|
| Encrypted PDF without password | Ask user for password; if unavailable, report and stop |
| Scanned PDF (image-only) | Use OCR path (pdf2image + pytesseract) |
| Corrupt PDF | Try `pypdf.PdfReader(strict=False)`, or use `mutool` repair |
| Extracted text is garbled | Use `pdfplumber` instead of `pypdf` for layout-aware extraction |
| Form fields not found | Check `reader.get_fields()` — some PDFs use XFA forms (unsupported) |
---
## Workflow: DOCX Creation & Editing
### 1. Decide: Template or Blank
- **Template approach:** Start from an existing `.docx` with placeholders like
`{{NAME}}`, `{{DATE}}`, `{{AMOUNT}}`. Best for reports, invoices, letters.
- **Blank approach:** Build from `Document()` using `python-docx`. Best for
simple documents or when no template exists.
### 2. Template Pattern
```python
from docx import Document
doc = Document("template.docx")
# Replace placeholders across all paragraphs, tables, headers, footers
placeholders = {
"{{NAME}}": "Jane Doe",
"{{DATE}}": "28 May 2026",
"{{AMOUNT}}": "€1,500.00"
}
def replace_in_paragraphs(paragraphs):
for para in paragraphs:
for key, val in placeholders.items():
if key in para.text:
for run in para.runs:
if key in run.text:
run.text = run.text.replace(key, val)
replace_in_paragraphs(doc.paragraphs)
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
replace_in_paragraphs(cell.paragraphs)
for section in doc.sections:
replace_in_paragraphs(section.header.paragraphs)
replace_in_paragraphs(section.footer.paragraphs)
doc.save("output.docx")
```
### 3. Building from Scratch
```python
from docx import Document
from docx.shared import Inches, Pt, Cm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT
doc = Document()
# Styles
style = doc.styles['Normal']
style.font.name = 'Calibri'
style.font.size = Pt(11)
# Heading
doc.add_heading('Quarterly Report', level=1)
# Paragraph
p = doc.add_paragraph('Executive summary of Q1 2026 results.')
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
# Bold run
run = p.add_run(' Key highlights:')
run.bold = True
# Bullet list
for item in ['Revenue up 12%', 'Costs down 4%', '3 new markets entered']:
doc.add_paragraph(item, style='List Bullet')
# Table
table = doc.add_table(rows=3, cols=3, style='Light Grid Accent 1')
table.alignment = WD_TABLE_ALIGNMENT.CENTER
data = [['Metric', 'Q4 2025', 'Q1 2026'],
['Revenue', '€2.1M', '€2.35M'],
['Profit', '€420K', '€510K']]
for i, row_data in enumerate(data):
for j, cell_val in enumerate(row_data):
table.rows[i].cells[j].text = cell_val
# Image
doc.add_picture('chart.png', width=Inches(5.5))
doc.save('report.docx')
```
### 4. Mail Merge for Bulk Documents
```python
from docx import Document
import csv
doc = Document("letter_template.docx")
with open("recipients.csv") as f:
recipients = list(csv.DictReader(f))
for i, recipient in enumerate(recipients):
doc_copy = Document("letter_template.docx")
for para in doc_copy.paragraphs:
for key, val in recipient.items():
if f"{{{{{key}}}}}" in para.text:
for run in para.runs:
run.text = run.text.replace(f"{{{{{key}}}}}", val)
doc_copy.save(f"letter_{i+1:03d}.docx")
```
### 5. DOCX → PDF Conversion
```python
# Option A: LibreOffice headless (best fidelity)
# soffice --headless --convert-to pdf report.docx
# Option B: python-docx + reportlab (programmatic)
# Option C: pandoc + wkhtmltopdf (Markdown intermediary)
```
---
## Workflow: XLSX Spreadsheet Handling
### 1. Read & Analyse Existing Data
```python
import openpyxl
from openpyxl.utils import get_column_letter
wb = openpyxl.load_workbook("data.xlsx", data_only=True)
ws = wb.active
print(f"Sheet: {ws.title}, Rows: {ws.max_row}, Cols: {ws.max_column}")
# Read headers
headers = [cell.value for cell in ws[1]]
print(f"Columns: {headers}")
# Read all data as list of dicts
data = []
for row in ws.iter_rows(min_row=2, values_only=True):
data.append(dict(zip(headers, row)))
```
### 2. Create from Scratch with Formatting
```python
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side, numbers
from openpyxl.chart import BarChart, Reference
from openpyxl.formatting.rule import DataBarRule
wb = Workbook()
ws = wb.active
ws.title = "Q1 Budget"
# Header styling
header_font = Font(name="Calibri", size=12, bold=True, color="FFFFFF")
header_fill = PatternFill(start_color="2F5496", end_color="2F5496", fill_type="solid")
header_align = Alignment(horizontal="center", vertical="center")
thin_border = Border(
left=Side(style='thin'), right=Side(style='thin'),
top=Side(style='thin'), bottom=Side(style='thin')
)
headers = ["Category", "Budget", "Actual", "Variance", "Variance %"]
for col, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col, value=header)
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_align
cell.border = thin_border
# Data
data = [
["Marketing", 50000, 48500],
["R&D", 120000, 118000],
["Sales", 75000, 72000],
["Operations", 90000, 93500],
["Support", 45000, 44200],
]
for row_idx, (cat, budget, actual) in enumerate(data, 2):
ws.cell(row=row_idx, column=1, value=cat).border = thin_border
ws.cell(row=row_idx, column=2, value=budget).border = thin_border
ws.cell(row=row_idx, column=3, value=actual).border = thin_border
# Variance = Actual - Budget
ws.cell(row=row_idx, column=4,
value=actual - budget).border = thin_border
ws.cell(row=row_idx, column=4).number_format = '#,##0'
# Variance %
if budget != 0:
ws.cell(row=row_idx, column=5,
value=(actual - budget) / budget).border = thin_border
ws.cell(row=row_idx, column=5).number_format = '0.0%'
# Auto-width columns
for col in range(1, len(headers) + 1):
ws.column_dimensions[get_column_letter(col)].width = 16
# Totals row
total_row = len(data) + 2
ws.cell(row=total_row, column=1, value="TOTAL").font = Font(bold=True)
for col in [2, 3, 4]:
col_letter = get_column_letter(col)
ws.cell(row=total_row, column=col,
value=f"=SUM({col_letter}2:{col_letter}{total_row-1})")
ws.cell(row=total_row, column=col).font = Font(bold=True)
# Bar chart
chart = BarChart()
chart.title = "Budget vs Actual"
chart.x_axis.title = "Category"
chart.y_axis.title = "Amount (€)"
chart.style = 10
cats = Reference(ws, min_col=1, min_row=2, max_row=total_row-1)
budget_data = Reference(ws, min_col=2, min_row=1, max_row=total_row-1)
actual_data = Reference(ws, min_col=3, min_row=1, max_row=total_row-1)
chart.add_data(budget_data, titles_from_data=True)
chart.add_data(actual_data, titles_from_data=True)
chart.set_categories(cats)
chart.width = 20
chart.height = 12
ws.add_chart(chart, f"A{total_row + 2}")
# Conditional formatting — data bars on Actual
rule = DataBarRule(start_type='min', end_type='max',
color='2F5496', showValue=True)
ws.conditional_formatting.add(f"C2:C{total_row-1}", rule)
# Freeze header row
ws.freeze_panes = 'A2'
wb.save("budget_report.xlsx")
```
### 3. Pivot Tables
```python
from openpyxl import load_workbook
from openpyxl.pivot import PivotTable
wb = load_workbook("sales_data.xlsx")
ws = wb.active
# Create pivot on a new sheet
pivot_sheet = wb.create_sheet("Pivot")
pivot = PivotTable()
pivot.add_field("Region")
pivot.add_field("Product")
pivot.add_field("Revenue")
pivot_sheet.add_pivot(pivot, "A3")
wb.save("sales_with_pivot.xlsx")
```
### 4. Common XLSX Pitfalls
| Pitfall | Fix |
|---------|-----|
| Formula results return `None` | Use `data_only=True` when loading, or compute values manually |
| Large file (>50MB) | Use `read_only=True` mode or chunk with `iter_rows()` |
| Merged cells lose data | Read from top-left cell; `ws.merged_cells.ranges` to detect |
| Date values appear as numbers | Apply date number format: `cell.number_format = 'YYYY-MM-DD'` |
---
## Workflow: PPTX Presentation Generation
### 1. Read & Analyse Existing Deck
```python
from pptx import Presentation
prs = Presentation("deck.pptx")
print(f"Slides: {len(prs.slides)}")
print(f"Slide width: {prs.slide_width}, height: {prs.slide_height}")
for i, slide in enumerate(prs.slides):
print(f"\n--- Slide {i+1} ({slide.slide_layout.name}) ---")
for shape in slide.shapes:
if shape.has_text_frame:
print(f" [{shape.shape_type}] {shape.text[:100]}")
if shape.has_table:
print(f" [TABLE] {shape.table.rows.__len__()} rows")
```
### 2. Build from Scratch
```python
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.chart import XL_CHART_TYPE
from pptx.chart.data import CategoryChartData
prs = Presentation()
prs.slide_width = Inches(13.333) # 16:9
prs.slide_height = Inches(7.5)
# --- Title Slide ---
slide = prs.slides.add_slide(prs.slide_layouts[0]) # Title layout
slide.shapes.title.text = "Q1 2026 Business Review"
if slide.placeholders[1].has_text_frame:
slide.placeholders[1].text = "Strategy & Operations\n28 May 2026"
# --- Content Slide ---
slide = prs.slides.add_slide(prs.slide_layouts[1]) # Title + Content
slide.shapes.title.text = "Key Highlights"
tf = slide.placeholders[1].text_frame
tf.clear()
for highlight in [
"Revenue up 12% YoY to €2.35M",
"Operating margin improved to 21.7%",
"3 new markets: Spain, Portugal, Italy",
"Customer NPS score: 72 (up from 64)"
]:
p = tf.add_paragraph()
p.text = highlight
p.level = 0
p.font.size = Pt(18)
p.space_after = Pt(8)
# --- Chart Slide ---
slide = prs.slides.add_slide(prs.slide_layouts[5]) # Blank
slide.shapes.title.text = "Revenue Trend"
chart_data = CategoryChartData()
chart_data.categories = ['Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar']
chart_data.add_series('Revenue (€K)', [680, 710, 750, 770, 790, 810])
chart_frame = slide.shapes.add_chart(
XL_CHART_TYPE.LINE_MARKERS, Inches(1), Inches(1.5),
Inches(11), Inches(5.5), chart_data
)
chart = chart_frame.chart
chart.has_legend = True
chart.has_title = False
# --- Image Slide ---
slide = prs.slides.add_slide(prs.slide_layouts[5])
slide.shapes.title.text = "Market Expansion Map"
slide.shapes.add_picture("map.png", Inches(2), Inches(1.5),
Inches(9), Inches(5))
# --- Speaker Notes ---
for slide in prs.slides:
notes_slide = slide.notes_slide
notes_slide.notes_text_frame.text = (
f"Speaker notes for: {slide.shapes.title.text}"
)
prs.save("quarterly_review.pptx")
```
### 3. Template-Driven Slides
```python
from pptx import Presentation
import json
prs = Presentation("template.pptx")
with open("slide_content.json") as f:
content = json.load(f)
for slide_data in content["slides"]:
slide = prs.slides.add_slide(
prs.slide_layouts[slide_data["layout_index"]]
)
slide.shapes.title.text = slide_data["title"]
if "body" in slide_data:
slide.placeholders[1].text = slide_data["body"]
prs.save("generated_deck.pptx")
```
### 4. PPTX Common Issues
| Issue | Fix |
|-------|-----|
| Layout index not found | List available: `[ly.name for ly in prs.slide_layouts]` |
| Placeholder missing | Check `slide.placeholders`; not all layouts have placeholder[1] |
| Images too large | Resize with PIL first, or use `Inches()` to set exact dimensions |
| Font not available on system | Use common fonts (Calibri, Arial) or embed fonts |
---
## Workflow: Format Conversion
### The Conversion Matrix
See `references/conversion-matrix.md` for the full cross-reference. Quick
reference for the most common paths:
| From | To | Tool | Quality |
|------|----|------|---------|
| DOCX | PDF | LibreOffice headless | ⭐⭐⭐⭐⭐ Best |
| DOCX | Markdown | `pandoc -i input.docx -o output.md` | ⭐⭐⭐⭐ Good |
| Markdown | DOCX | `pandoc -i input.md -o output.docx` | ⭐⭐⭐⭐ Good |
| Markdown | PDF | `pandoc --pdf-engine=xelatex` | ⭐⭐⭐⭐⭐ Best |
| PDF | DOCX | `pdf2docx` library or LibreOffice | ⭐⭐⭐ Moderate |
| PDF | Text | `pdfplumber` or `pandoc` | ⭐⭐⭐⭐ Good |
| XLSX | CSV | `pandas.read_excel().to_csv()` | ⭐⭐⭐⭐⭐ Lossless |
| XLSX | PDF | LibreOffice headless | ⭐⭐⭐⭐ Good |
| PPTX | PDF | LibreOffice headless | ⭐⭐⭐⭐ Good |
| PPTX | Images | `pptx → PDF → pdf2image` | ⭐⭐⭐ Moderate |
| HTML | DOCX | `pandoc -i input.html -o output.docx` | ⭐⭐⭐ Moderate |
### Generic Pandoc Pattern
```bash
# Basic conversion
pandoc input.docx -o output.md
# With template and metadata
pandoc input.md \
--template=corporate.latex \
--metadata title="Q1 Report" \
--metadata author="Jane Doe" \
--pdf-engine=xelatex \
-o output.pdf
# Convert with reference document for styling
pandoc input.md --reference-doc=styles.docx -o output.docx
# Batch conversion
for file in *.md; do
pandoc "$file" -o "${file%.md}.docx"
done
```
### PDF → DOCX (Best Available)
```python
# Using pdf2docx (best fidelity for text-heavy PDFs)
from pdf2docx import Converter
cv = Converter("input.pdf")
cv.convert("output.docx", start=0, end=None)
cv.close()
# Fallback: extract text, rebuild in python-docx
import pdfplumber
from docx import Document
doc = Document()
with pdfplumber.open("input.pdf") as pdf:
for page in pdf.pages:
text = page.extract_text()
if text:
doc.add_paragraph(text)
doc.save("output.docx")
```
### XLSX → CSV / JSON
```python
import pandas as pd
df = pd.read_excel("data.xlsx", sheet_name=None) # All sheets
for sheet_name, sheet_df in df.items():
sheet_df.to_csv(f"data_{sheet_name}.csv", index=False)
sheet_df.to_json(f"data_{sheet_name}.json", orient="records")
```
---
## Platform Compatibility Notes
### Claude Code / Codex
- Full Python scripting capability — all libraries available.
- Use `subprocess` for pandoc and LibreOffice calls.
- Recommend `pip install` for missing packages.
### Cursor
- Works identically; execute scripts via integrated terminal.
- Use the `doc_tools_check.py` script as a pre-commit or task runner.
### Gemini CLI
- Can execute Python but may have restricted subprocess access.
- Prefer pure-Python libraries (pypdf, pdfplumber, python-docx, openpyxl,
python-pptx) over CLI tools when possible.
- Pandoc may not be available; fall back to Python-only conversion paths.
### OpenClaw
- Full tool access. Can install packages and run both Python scripts and
external CLI tools.
- Recommended: run `doc_tools_check.py` on first invocation to set up the
environment.
### GitHub Copilot
- Chat context; provide complete, self-contained Python snippets.
- Include dependency installation comments.
- Tip: reference `scripts/doc_tools_check.py` for the install list.
### Windsurf
- Full Python execution in IDE terminal; all libraries supported.
- Use `doc_tools_check.py` for environment validation.
- Store reusable template files in project workspace.
### OpenCode
- Execute scripts via terminal with full Python access.
- Prefer pure-Python libraries for portability.
- Run `doc_tools_check.py` to validate environment before any operation.
---
## Integration Pattern
A typical document-processing session follows this sequence:
```
1. User Request
↓
2. Trigger Detection (match phrases → activate skill)
↓
3. Tool Validation → scripts/doc_tools_check.py
↓
4. Document Assessment → Inspect input file(s)
↓
5. Operation Execution → Follow workflow above
↓
6. Output Validation → Read back, verify integrity
↓
7. Deliver Output → Present result path + summary
```
---
## Reference Files
| File | Contents |
|------|----------|
| `references/pdf-processing-guide.md` | Deep dive: extraction, merging, splitting, rotation, watermarking, form filling, OCR |
| `references/office-formats-guide.md` | DOCX templates, XLSX formatting, PPTX layouts, style consistency |
| `references/conversion-matrix.md` | Full conversion pair matrix with tool recommendations and quality ratings |
| `scripts/doc_tools_check.py` | PEP 723 pre-flight validation of all required tooling |
| `evals/eval_cases.json` | 5 positive + 3 near-miss negative test cases |
---
## Troubleshooting Quick Reference
| Problem | Likely Cause | Solution |
|---------|-------------|----------|
| `ModuleNotFoundError` | Missing Python package | Run `doc_tools_check.py`, install missing packages |
| `pandoc: command not found` | pandoc not installed | `brew install pandoc` (macOS) or `apt install pandoc` (Linux) |
| PDF text extraction empty | Scanned/image PDF | Use OCR path: pdf2image + pytesseract |
| DOCX styles look wrong | Template mismatch | Use `--reference-doc` with pandoc or LibreOffice conversion |
| XLSX formula error `#REF!` | Sheet/cell references broken | Check sheet names and cell ranges after row insertion |
| PPTX layout index changes | Template differs from code assumption | List layouts dynamically with `[ly.name for ly in prs.slide_layouts]` |
| File size ballooning | Embedded high-res images | Resize images before embedding: `img.thumbnail((1920, 1080))` |
| Character encoding issues | Non-UTF-8 content | Specify encoding: `encoding='utf-8'` in all file operations |