
Document Pdf
- 601 installs
- 73 repo stars
- Updated July 13, 2026
- vasilyu1983/ai-agents-public
document-pdf is a Claude Code skill that enables coding agents to read and understand PDF documentation, research papers, and product specifications for developers who need structured text extracted from PDF artifacts.
About
document-pdf is a skill in vasilyu1983/ai-agents-public ranked #5 on skills.sh with 469 installs. It equips coding agents to read and comprehend PDF files including technical documentation, research papers, and product specifications. Developers invoke document-pdf when implementation depends on content locked in PDF format rather than markdown or web pages. The skill fits agent workflows that must summarize requirements, extract API details, or answer questions from uploaded specification PDFs. Reach for document-pdf when a task references .pdf files, vendor docs distributed as PDF, or academic papers needed for feature design.
- Enables Claude, Cursor and other agents to directly ingest PDF files as context
- Supports complex technical documents, whitepapers and manuals
- Reduces manual copy-paste of reference material
- Works with local and remote PDF sources
Document Pdf by the numbers
- 601 all-time installs (skills.sh)
- +11 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #1,580 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/vasilyu1983/ai-agents-public --skill document-pdfAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 601 |
|---|---|
| repo stars | ★ 73 |
| Last updated | July 13, 2026 |
| Repository | vasilyu1983/ai-agents-public ↗ |
How do coding agents read and parse PDF files?
Let their coding agent read and understand PDF documentation, research papers, or product specs.
Who is it for?
Developers whose agent workflows must ingest vendor PDFs, API specification documents, or research papers before writing code.
Skip if: Teams working exclusively with markdown, HTML, or plain-text docs where standard Read tool access suffices without PDF parsing.
When should I use this skill?
User uploads or references a .pdf file, asks to read PDF documentation, extract spec details, or summarize a research paper in an agent session.
What you get
Extracted PDF text, structured summaries, and agent-ready answers from documentation or research paper content.
- Extracted PDF text
- Structured document summary
By the numbers
- 469 installs on skills.sh
- Ranked #5 in vasilyu1983/ai-agents-public on skills.sh
Files
Document PDF Skill — Quick Reference
This skill enables PDF creation, extraction, manipulation, and analysis. Claude should apply these patterns when users need to generate invoices, reports, extract data from PDFs, merge documents, or work with PDF forms.
Modern Best Practices (Jan 2026):
- PDF is a release artifact, not the editable source of truth.
- Validate export fidelity (fonts, images, links) and accessibility where required.
- Accessibility: if compliance matters, target a tagged/structured PDF workflow (often PDF/UA-aligned) and validate with tooling.
- EU distribution: EAA (June 2025) typically implies EN 301 549 expectations for customer-facing PDFs.
- Treat PDFs as sensitive: scrub metadata, ensure real redaction, and control distribution.
Core Decision Rules (2026)
- First decide: born-digital PDF (selectable text) vs scanned PDF (images). Scanned PDFs usually require OCR; see
references/pdf-extraction-patterns.md. - If the user needs accessibility/compliance, prefer generating from a source format that supports structure (DOCX/HTML + proper export) rather than “post-fixing” an untagged PDF.
- For deterministic ops (merge/split/rotate/scrub), prefer
scripts/helpers over re-implementing ad hoc. - Never treat black rectangles or overlays as redaction; use real redaction and verify by copy/paste + search.
---
Quick Reference
| Task | Tool/Library | Language | When to Use |
|---|---|---|---|
| Create PDF | pdfkit | Node.js | Reports, invoices, certificates |
| Create PDF | ReportLab | Python | Complex layouts, tables |
| Create PDF | FPDF2 | Python | Simple PDFs with Unicode support |
| Create PDF | Borb | Python | Interactive elements, pure Python |
| Edit PDF | pdf-lib | Node.js | Modify existing PDFs, add pages |
| Extract text | pdfplumber | Python | OCR-free text extraction |
| OCR scanned PDF | PyMuPDF + Tesseract | Python | Scanned PDFs (no selectable text) |
| Extract tables | Camelot | Python | Tables with borders (Lattice mode) |
| Extract tables | Camelot/Tabula | Python | Tables without borders (Stream mode) |
| Parse/merge/split/rotate | pypdf | Python | Deterministic PDF manipulation |
| Fill forms | pdf-lib | Node.js | Form automation |
| HTML to PDF | Puppeteer/Playwright | Node.js | High-fidelity web page rendering |
| HTML to PDF | WeasyPrint | Python | CSS3-based, no browser needed |
When to Use This Skill
Claude should invoke this skill when a user requests:
- Generate PDFs from data (invoices, reports, certificates)
- Extract text or tables from existing PDFs
- Merge multiple PDFs into one document
- Split PDFs into separate files
- Fill PDF forms programmatically
- Add watermarks, headers, footers
- Convert HTML/web pages to PDF
---
Default Workflow
- Create: pick
pdfkit(Node) orReportLab(Python) and start fromassets/invoice-template.mdorassets/report-template.md; for advanced layouts usereferences/pdf-generation-patterns.md. - Extract: use
references/pdf-extraction-patterns.md(text/tables/images/metadata + OCR fallback). - Ship: run
assets/pdf-release-checklist.md(fidelity, links, accessibility baseline, privacy).
Scripts (Deterministic Operations)
Scripts are optional helpers; they assume Python 3 plus the listed dependencies in each file.
- Merge:
python3 scripts/merge_pdfs.py merged.pdf a.pdf b.pdf - Split:
python3 scripts/split_pdf.py in.pdf out_dir --each-page - Rotate:
python3 scripts/rotate_pdf.py in.pdf out.pdf --degrees 90 - Scrub metadata:
python3 scripts/scrub_metadata.py in.pdf out.pdf
PDF Structure Patterns
Invoice Template
INVOICE STRUCTURE
├── Header (logo, company info, invoice #)
├── Bill To / Ship To blocks
├── Line items table
│ ├── Description | Qty | Unit Price | Total
│ └── Subtotal, Tax, Total
├── Payment terms
└── Footer (contact, thank you)Report Template
REPORT PDF STRUCTURE
├── Cover page (title, author, date)
├── Table of contents
├── Body sections with page numbers
├── Charts/images with captions
├── Appendices
└── Running header/footer---
Decision Tree
PDF Task: [What do you need?]
├─ Create new PDF?
│ ├─ Simple text/tables → pdfkit (Node) or ReportLab (Python)
│ ├─ Complex layouts → ReportLab with Platypus
│ └─ From HTML → Puppeteer or wkhtmltopdf
│
├─ Extract from PDF?
│ ├─ Text only → pdfplumber (Python)
│ ├─ Tables → pdfplumber or camelot (Python)
│ └─ Images → PyMuPDF/fitz (Python)
│
├─ Modify existing PDF?
│ ├─ Add text/images → pdf-lib (Node)
│ ├─ Merge/split → pypdf or pdf-lib
│ └─ Fill forms → pdf-lib
│
└─ Batch processing?
└─ pypdf + pdfplumber pipeline---
Do / Avoid (Jan 2026)
Do
- Keep a versioned source document (doc/slide/design file) alongside the PDF.
- Verify links and reading order for long documents.
- Use real redaction and test by copy/paste.
Avoid
- Editing PDFs as the primary workflow when a source doc exists.
- Shipping PDFs with broken links or illegible charts.
- Including customer PII or secrets in PDFs without explicit approval.
What Good Looks Like
- Fidelity: export is reproducible from a versioned source file (doc/slide/design) and looks identical across viewers.
- Accessibility: tags/reading order are correct; links work; scanned docs are OCRed when appropriate.
- Release hygiene: file naming includes version/date; metadata is clean; no “PDF as source of truth”.
- Security: redaction is verified (copy/paste test) and sensitive data is minimized.
- QA: release checklist completed using
assets/pdf-release-checklist.md.
Optional: AI / Automation
Use only when explicitly requested and policy-compliant.
- Generate a release checklist run; humans verify the final PDF manually.
Navigation
Resources
- references/pdf-generation-patterns.md — Complex layouts, multi-page docs
- references/pdf-extraction-patterns.md — Text, table, image extraction
- references/pdf-accessibility-compliance.md — Tagged PDFs, PDF/UA, EAA compliance
- references/pdf-forms-interactive.md — AcroForms, form filling, digital signatures
- references/pdf-security-redaction.md — Encryption, permissions, real redaction
- data/sources.json — Library documentation links
Templates
- assets/invoice-template.md — Invoice PDF generation
- assets/report-template.md — Multi-page report structure
- assets/pdf-release-checklist.md — Links, accessibility, export fidelity
Related Skills
- ../document-docx/SKILL.md — Word document generation
- ../document-xlsx/SKILL.md — Excel/spreadsheet workflows
- ../document-pptx/SKILL.md — PowerPoint presentations
Fact-Checking
- Use web search/web fetch to verify current external facts, versions, pricing, deadlines, regulations, or platform behavior before final answers.
- Prefer primary sources; report source links and dates for volatile information.
- If web access is unavailable, state the limitation and mark guidance as unverified.
Invoice PDF Template
Copy-paste templates for generating invoice PDFs.
---
Node.js (PDFKit)
import PDFDocument from 'pdfkit';
import fs from 'fs';
interface InvoiceItem {
description: string;
quantity: number;
unitPrice: number;
}
interface InvoiceData {
invoiceNumber: string;
date: string;
dueDate: string;
company: {
name: string;
address: string[];
email: string;
phone: string;
};
billTo: {
name: string;
address: string[];
email: string;
};
items: InvoiceItem[];
taxRate: number;
notes?: string;
}
function generateInvoice(data: InvoiceData, outputPath: string): void {
const doc = new PDFDocument({ size: 'A4', margin: 50 });
doc.pipe(fs.createWriteStream(outputPath));
// Header
doc.fontSize(24).font('Helvetica-Bold').text('INVOICE', { align: 'right' });
doc.moveDown(0.5);
doc.fontSize(10).font('Helvetica')
.text(`Invoice #: ${data.invoiceNumber}`, { align: 'right' })
.text(`Date: ${data.date}`, { align: 'right' })
.text(`Due Date: ${data.dueDate}`, { align: 'right' });
doc.moveDown(2);
// Company info (left) and Bill To (right)
const startY = doc.y;
doc.fontSize(12).font('Helvetica-Bold').text('From:', 50);
doc.fontSize(10).font('Helvetica')
.text(data.company.name)
.text(data.company.address.join('\n'))
.text(data.company.email)
.text(data.company.phone);
doc.y = startY;
doc.fontSize(12).font('Helvetica-Bold').text('Bill To:', 300);
doc.fontSize(10).font('Helvetica')
.text(data.billTo.name, 300)
.text(data.billTo.address.join('\n'), 300)
.text(data.billTo.email, 300);
doc.moveDown(3);
// Table header
const tableTop = doc.y;
const colWidths = { desc: 250, qty: 60, price: 80, total: 80 };
doc.rect(50, tableTop, 495, 20).fill('#2c3e50');
doc.fillColor('white').fontSize(10).font('Helvetica-Bold')
.text('Description', 55, tableTop + 5)
.text('Qty', 305, tableTop + 5, { width: colWidths.qty, align: 'center' })
.text('Unit Price', 365, tableTop + 5, { width: colWidths.price, align: 'right' })
.text('Total', 445, tableTop + 5, { width: colWidths.total, align: 'right' });
// Table rows
let y = tableTop + 25;
let subtotal = 0;
doc.fillColor('black').font('Helvetica');
data.items.forEach((item, i) => {
const lineTotal = item.quantity * item.unitPrice;
subtotal += lineTotal;
const bgColor = i % 2 === 0 ? '#f8f9fa' : '#ffffff';
doc.rect(50, y - 5, 495, 20).fill(bgColor);
doc.fillColor('black')
.text(item.description, 55, y)
.text(item.quantity.toString(), 305, y, { width: colWidths.qty, align: 'center' })
.text(`$${item.unitPrice.toFixed(2)}`, 365, y, { width: colWidths.price, align: 'right' })
.text(`$${lineTotal.toFixed(2)}`, 445, y, { width: colWidths.total, align: 'right' });
y += 20;
});
// Totals
y += 10;
const tax = subtotal * data.taxRate;
const total = subtotal + tax;
doc.font('Helvetica')
.text('Subtotal:', 365, y, { width: 80, align: 'right' })
.text(`$${subtotal.toFixed(2)}`, 445, y, { width: 80, align: 'right' });
y += 15;
doc.text(`Tax (${(data.taxRate * 100).toFixed(0)}%):`, 365, y, { width: 80, align: 'right' })
.text(`$${tax.toFixed(2)}`, 445, y, { width: 80, align: 'right' });
y += 20;
doc.rect(360, y - 5, 185, 25).fill('#2c3e50');
doc.fillColor('white').font('Helvetica-Bold')
.text('Total:', 365, y, { width: 80, align: 'right' })
.text(`$${total.toFixed(2)}`, 445, y, { width: 80, align: 'right' });
// Notes
if (data.notes) {
doc.fillColor('black').moveDown(4);
doc.fontSize(10).font('Helvetica-Bold').text('Notes:');
doc.font('Helvetica').text(data.notes);
}
// Footer
doc.fontSize(8).fillColor('gray')
.text('Thank you for your business!', 50, 780, { align: 'center' });
doc.end();
}
// Usage
const invoiceData: InvoiceData = {
invoiceNumber: 'INV-2025-001',
date: '2025-01-15',
dueDate: '2025-02-15',
company: {
name: 'Acme Corp',
address: ['123 Business St', 'Suite 100', 'New York, NY 10001'],
email: 'billing@acme.com',
phone: '+1 (555) 123-4567',
},
billTo: {
name: 'Client Company',
address: ['456 Client Ave', 'Floor 5', 'Los Angeles, CA 90001'],
email: 'accounts@client.com',
},
items: [
{ description: 'Web Development Services', quantity: 40, unitPrice: 150 },
{ description: 'UI/UX Design', quantity: 20, unitPrice: 125 },
{ description: 'Project Management', quantity: 10, unitPrice: 100 },
],
taxRate: 0.08,
notes: 'Payment is due within 30 days. Please include invoice number with payment.',
};
generateInvoice(invoiceData, 'invoice.pdf');---
Python (ReportLab)
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from dataclasses import dataclass
@dataclass
class InvoiceItem:
description: str
quantity: int
unit_price: float
@dataclass
class InvoiceData:
invoice_number: str
date: str
due_date: str
company_name: str
company_address: list[str]
company_email: str
bill_to_name: str
bill_to_address: list[str]
bill_to_email: str
items: list[InvoiceItem]
tax_rate: float
notes: str = ''
def generate_invoice(data: InvoiceData, output_path: str):
doc = SimpleDocTemplate(output_path, pagesize=A4,
leftMargin=20*mm, rightMargin=20*mm,
topMargin=20*mm, bottomMargin=20*mm)
styles = getSampleStyleSheet()
title_style = ParagraphStyle('Title', parent=styles['Heading1'],
fontSize=24, alignment=2) # Right align
story = []
# Header
story.append(Paragraph('INVOICE', title_style))
story.append(Spacer(1, 10))
# Invoice details
invoice_info = [
[f'Invoice #: {data.invoice_number}'],
[f'Date: {data.date}'],
[f'Due Date: {data.due_date}'],
]
info_table = Table(invoice_info, colWidths=[170*mm])
info_table.setStyle(TableStyle([
('ALIGN', (0, 0), (-1, -1), 'RIGHT'),
('FONTSIZE', (0, 0), (-1, -1), 10),
]))
story.append(info_table)
story.append(Spacer(1, 20))
# From / Bill To
addresses = [
['From:', 'Bill To:'],
[data.company_name, data.bill_to_name],
['\n'.join(data.company_address), '\n'.join(data.bill_to_address)],
[data.company_email, data.bill_to_email],
]
addr_table = Table(addresses, colWidths=[85*mm, 85*mm])
addr_table.setStyle(TableStyle([
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, -1), 10),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
]))
story.append(addr_table)
story.append(Spacer(1, 30))
# Items table
items_data = [['Description', 'Qty', 'Unit Price', 'Total']]
subtotal = 0
for item in data.items:
line_total = item.quantity * item.unit_price
subtotal += line_total
items_data.append([
item.description,
str(item.quantity),
f'${item.unit_price:.2f}',
f'${line_total:.2f}',
])
# Add totals
tax = subtotal * data.tax_rate
total = subtotal + tax
items_data.extend([
['', '', 'Subtotal:', f'${subtotal:.2f}'],
['', '', f'Tax ({data.tax_rate*100:.0f}%):', f'${tax:.2f}'],
['', '', 'Total:', f'${total:.2f}'],
])
items_table = Table(items_data, colWidths=[90*mm, 20*mm, 30*mm, 30*mm])
items_table.setStyle(TableStyle([
# Header
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2c3e50')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
# Body
('FONTSIZE', (0, 0), (-1, -1), 10),
('ALIGN', (1, 1), (-1, -1), 'RIGHT'),
# Alternating rows
('BACKGROUND', (0, 1), (-1, -4), colors.HexColor('#f8f9fa')),
# Grid
('GRID', (0, 0), (-1, -4), 0.5, colors.grey),
# Totals
('FONTNAME', (2, -3), (-1, -1), 'Helvetica-Bold'),
('LINEABOVE', (2, -3), (-1, -3), 1, colors.black),
('BACKGROUND', (2, -1), (-1, -1), colors.HexColor('#2c3e50')),
('TEXTCOLOR', (2, -1), (-1, -1), colors.white),
]))
story.append(items_table)
# Notes
if data.notes:
story.append(Spacer(1, 30))
story.append(Paragraph('<b>Notes:</b>', styles['Normal']))
story.append(Paragraph(data.notes, styles['Normal']))
doc.build(story)
# Usage
invoice_data = InvoiceData(
invoice_number='INV-2025-001',
date='2025-01-15',
due_date='2025-02-15',
company_name='Acme Corp',
company_address=['123 Business St', 'Suite 100', 'New York, NY 10001'],
company_email='billing@acme.com',
bill_to_name='Client Company',
bill_to_address=['456 Client Ave', 'Floor 5', 'Los Angeles, CA 90001'],
bill_to_email='accounts@client.com',
items=[
InvoiceItem('Web Development Services', 40, 150.00),
InvoiceItem('UI/UX Design', 20, 125.00),
InvoiceItem('Project Management', 10, 100.00),
],
tax_rate=0.08,
notes='Payment is due within 30 days. Please include invoice number with payment.',
)
generate_invoice(invoice_data, 'invoice.pdf')---
Related
- report-template.md - Multi-page report generation
- ../references/pdf-generation-patterns.md - Advanced patterns
PDF Release Checklist (Core, Non-AI)
Purpose: ship a PDF that is readable, link-correct, and reproducible (the PDF is not the source of truth).
Inputs
- Source file(s): doc/slide/design file + linked assets
- Release context: audience, distribution channel, confidentiality level
Outputs
- Verified PDF ready for distribution
- Release notes: source version, export settings, and owner
Core
A) Source-of-Truth and Versioning
- [ ] Source document is stored and versioned (doc/ppt/design file)
- [ ] PDF filename includes date/version (e.g.,
Report_2025-12-18_v2.pdf) - [ ] Owner and last-updated date are present in the document
B) Export Fidelity
- [ ] Fonts are embedded (or rendering verified on a second machine)
- [ ] Images are not pixelated; charts are legible at 100% zoom
- [ ] Page size is correct (A4/Letter) and margins are intentional
- [ ] Interactive elements behave as expected (links, TOC, form fields)
C) Links and Navigation
- [ ] All hyperlinks work (external + internal)
- [ ] Table of contents links work (if present)
- [ ] Headings/bookmarks exist for long PDFs (if supported)
D) Accessibility (baseline)
- [ ] Text is selectable (not a scanned image unless necessary)
- [ ] Reading order is correct (test with selection or a screen reader if possible)
- [ ] Images/figures have alt text (where supported by source tool)
- [ ] Color is not the only carrier of meaning; contrast is sufficient
Adobe reference: https://helpx.adobe.com/acrobat/using/creating-accessible-pdfs.html
E) Privacy and Compliance
- [ ] No customer PII or confidential data unless explicitly approved
- [ ] Redaction is real (not just black boxes); verify by copy/paste
- [ ] Metadata scrubbed if needed (author, comments, hidden layers)
Decision Rules
- No-ship if: links are broken, reading order is wrong, or sensitive data is present.
- Re-export if: any formatting changed after the last PDF export.
Risks
- PDF becomes an unmaintainable source of truth
- Hidden metadata leaks confidential info
- Accessibility failures block distribution (esp. enterprise/government buyers)
Optional: AI / Automation
Use only if allowed by policy and data handling rules.
- Generate a link-check report and a redaction checklist; humans verify final PDF manually.
Report PDF Template
Copy-paste templates for generating multi-page reports with cover pages, TOC, and sections.
---
Node.js (PDFKit)
import PDFDocument from 'pdfkit';
import fs from 'fs';
interface ReportSection {
title: string;
content: string;
charts?: { title: string; data: number[] }[];
}
interface ReportData {
title: string;
subtitle: string;
author: string;
date: string;
sections: ReportSection[];
}
function generateReport(data: ReportData, outputPath: string): void {
const doc = new PDFDocument({
size: 'A4',
bufferPages: true,
margins: { top: 72, bottom: 72, left: 72, right: 72 }
});
doc.pipe(fs.createWriteStream(outputPath));
// Track sections for TOC
const tocEntries: { title: string; page: number }[] = [];
// ===== COVER PAGE =====
doc.rect(0, 0, doc.page.width, doc.page.height).fill('#2c3e50');
doc.fillColor('white')
.fontSize(36)
.font('Helvetica-Bold')
.text(data.title, 72, 280, { align: 'center' });
doc.fontSize(18)
.font('Helvetica')
.text(data.subtitle, 72, 340, { align: 'center' });
doc.fontSize(12)
.text(data.author, 72, 500, { align: 'center' })
.text(data.date, 72, 520, { align: 'center' });
// ===== TABLE OF CONTENTS =====
doc.addPage();
doc.fillColor('black')
.fontSize(24)
.font('Helvetica-Bold')
.text('Table of Contents', { align: 'left' });
doc.moveDown(2);
// Placeholder for TOC (we'll come back to this)
const tocPageNum = doc.bufferedPageRange().count;
const tocY = doc.y;
// ===== CONTENT SECTIONS =====
data.sections.forEach((section, index) => {
doc.addPage();
// Track for TOC
tocEntries.push({
title: section.title,
page: doc.bufferedPageRange().count,
});
// Section header
doc.fillColor('#2c3e50')
.fontSize(20)
.font('Helvetica-Bold')
.text(`${index + 1}. ${section.title}`);
doc.moveDown();
// Section content
doc.fillColor('black')
.fontSize(11)
.font('Helvetica')
.text(section.content, {
align: 'justify',
lineGap: 4,
});
// Charts (simple bar representation)
if (section.charts) {
section.charts.forEach(chart => {
doc.moveDown(2);
doc.fontSize(12).font('Helvetica-Bold').text(chart.title);
doc.moveDown(0.5);
const barWidth = 300;
const barHeight = 15;
const maxVal = Math.max(...chart.data);
chart.data.forEach((val, i) => {
const width = (val / maxVal) * barWidth;
doc.rect(doc.x, doc.y, width, barHeight).fill('#3498db');
doc.fillColor('black')
.fontSize(9)
.text(`${val}`, doc.x + width + 5, doc.y - barHeight + 3);
doc.y += barHeight + 5;
});
});
}
});
// ===== ADD PAGE NUMBERS =====
const range = doc.bufferedPageRange();
for (let i = 1; i < range.count; i++) { // Skip cover page
doc.switchToPage(i);
doc.fontSize(9)
.fillColor('gray')
.text(
`Page ${i} of ${range.count - 1}`,
72,
doc.page.height - 50,
{ align: 'center', width: doc.page.width - 144 }
);
}
// ===== FILL IN TOC =====
doc.switchToPage(tocPageNum - 1);
doc.y = tocY;
tocEntries.forEach((entry, i) => {
const dots = '.'.repeat(60);
doc.fontSize(11)
.font('Helvetica')
.fillColor('black')
.text(`${i + 1}. ${entry.title}`, 72, doc.y, { continued: true })
.text(dots.slice(0, 50 - entry.title.length), { continued: true })
.text(`${entry.page}`, { align: 'right' });
doc.moveDown(0.5);
});
doc.end();
}
// Usage
const reportData: ReportData = {
title: 'Q4 2024 Performance Report',
subtitle: 'Annual Review and 2025 Outlook',
author: 'Analytics Team',
date: 'January 2025',
sections: [
{
title: 'Executive Summary',
content: `This report provides a comprehensive overview of our Q4 2024 performance.
Key highlights include a 25% increase in revenue, successful expansion into two new
markets, and the launch of three new product lines. Customer satisfaction scores
reached an all-time high of 92%, reflecting our continued focus on quality and service.
The following sections provide detailed analysis of each business unit, financial
metrics, and strategic initiatives completed during the quarter.`,
},
{
title: 'Financial Performance',
content: `Revenue for Q4 2024 reached $12.5M, representing a 25% increase over the
same period last year. Operating margins improved by 3 percentage points to 18%,
driven by operational efficiencies and favorable product mix.
Key drivers of growth included strong performance in the enterprise segment and
successful upselling initiatives in our existing customer base.`,
charts: [
{ title: 'Monthly Revenue ($M)', data: [3.8, 4.2, 4.5] },
{ title: 'Customer Acquisition', data: [45, 52, 61] },
],
},
{
title: 'Strategic Initiatives',
content: `During Q4, we completed several key strategic initiatives:
1. Market Expansion: Successfully entered the European and APAC markets
2. Product Launch: Introduced three new product lines with strong initial adoption
3. Technology: Completed cloud migration, reducing infrastructure costs by 30%
4. Talent: Expanded team by 40 new hires across engineering and sales
These initiatives position us well for continued growth in 2025.`,
},
{
title: 'Outlook and Recommendations',
content: `Based on current trends and market conditions, we project continued strong
growth in 2025. Key focus areas include:
- Scaling operations in new markets
- Expanding product portfolio through R&D investment
- Enhancing customer success programs
- Pursuing strategic partnerships
We recommend increasing investment in product development and customer success
to capitalize on market opportunities.`,
},
],
};
generateReport(reportData, 'report.pdf');---
Python (ReportLab)
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, PageBreak,
Table, TableStyle, Image
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.graphics.shapes import Drawing, Rect
from reportlab.graphics.charts.barcharts import VerticalBarChart
from dataclasses import dataclass
from typing import Optional
@dataclass
class ChartData:
title: str
categories: list[str]
values: list[float]
@dataclass
class ReportSection:
title: str
content: str
chart: Optional[ChartData] = None
@dataclass
class ReportData:
title: str
subtitle: str
author: str
date: str
sections: list[ReportSection]
class ReportGenerator:
def __init__(self, data: ReportData, output_path: str):
self.data = data
self.output_path = output_path
self.page_count = 0
self.toc_entries = []
self.styles = getSampleStyleSheet()
self._setup_styles()
def _setup_styles(self):
self.styles.add(ParagraphStyle(
'CoverTitle',
parent=self.styles['Heading1'],
fontSize=36,
textColor=colors.white,
alignment=1, # Center
spaceAfter=20,
))
self.styles.add(ParagraphStyle(
'CoverSubtitle',
parent=self.styles['Normal'],
fontSize=18,
textColor=colors.white,
alignment=1,
))
self.styles.add(ParagraphStyle(
'SectionTitle',
parent=self.styles['Heading1'],
fontSize=20,
textColor=colors.HexColor('#2c3e50'),
spaceBefore=20,
spaceAfter=12,
))
self.styles.add(ParagraphStyle(
'BodyText',
parent=self.styles['Normal'],
fontSize=11,
leading=16,
alignment=4, # Justify
))
def _header_footer(self, canvas, doc):
"""Add page numbers to all pages except cover."""
if doc.page > 1:
canvas.saveState()
canvas.setFont('Helvetica', 9)
canvas.setFillColor(colors.gray)
canvas.drawCentredString(
A4[0] / 2,
20 * mm,
f'Page {doc.page - 1}'
)
canvas.restoreState()
def _create_cover_page(self) -> list:
"""Generate cover page elements."""
elements = []
# Background rectangle (handled differently in platypus)
# We'll use a drawing
cover_bg = Drawing(A4[0], A4[1])
cover_bg.add(Rect(0, 0, A4[0], A4[1], fillColor=colors.HexColor('#2c3e50')))
elements.append(cover_bg)
elements.append(Spacer(1, 200))
elements.append(Paragraph(self.data.title, self.styles['CoverTitle']))
elements.append(Spacer(1, 20))
elements.append(Paragraph(self.data.subtitle, self.styles['CoverSubtitle']))
elements.append(Spacer(1, 150))
elements.append(Paragraph(self.data.author, self.styles['CoverSubtitle']))
elements.append(Paragraph(self.data.date, self.styles['CoverSubtitle']))
elements.append(PageBreak())
return elements
def _create_toc(self) -> list:
"""Generate table of contents."""
elements = []
elements.append(Paragraph('Table of Contents', self.styles['Heading1']))
elements.append(Spacer(1, 20))
# TOC entries will be added after we know page numbers
for i, section in enumerate(self.data.sections):
toc_line = f'{i + 1}. {section.title}'
elements.append(Paragraph(toc_line, self.styles['Normal']))
elements.append(Spacer(1, 8))
elements.append(PageBreak())
return elements
def _create_chart(self, chart_data: ChartData) -> Drawing:
"""Create a bar chart."""
drawing = Drawing(400, 200)
chart = VerticalBarChart()
chart.x = 50
chart.y = 50
chart.width = 300
chart.height = 125
chart.data = [chart_data.values]
chart.categoryAxis.categoryNames = chart_data.categories
chart.bars[0].fillColor = colors.HexColor('#3498db')
chart.valueAxis.valueMin = 0
chart.valueAxis.valueMax = max(chart_data.values) * 1.2
drawing.add(chart)
return drawing
def _create_section(self, index: int, section: ReportSection) -> list:
"""Generate a report section."""
elements = []
# Section title
title = f'{index + 1}. {section.title}'
elements.append(Paragraph(title, self.styles['SectionTitle']))
# Content paragraphs
for para in section.content.split('\n\n'):
if para.strip():
elements.append(Paragraph(para.strip(), self.styles['BodyText']))
elements.append(Spacer(1, 10))
# Chart if present
if section.chart:
elements.append(Spacer(1, 20))
elements.append(Paragraph(
f'<b>{section.chart.title}</b>',
self.styles['Normal']
))
elements.append(self._create_chart(section.chart))
elements.append(PageBreak())
return elements
def generate(self):
"""Generate the complete report."""
doc = SimpleDocTemplate(
self.output_path,
pagesize=A4,
leftMargin=20*mm,
rightMargin=20*mm,
topMargin=25*mm,
bottomMargin=25*mm,
)
story = []
# Cover page (simplified - full cover needs canvas drawing)
story.append(Paragraph(self.data.title, self.styles['Heading1']))
story.append(Paragraph(self.data.subtitle, self.styles['Normal']))
story.append(Spacer(1, 50))
story.append(Paragraph(f'Author: {self.data.author}', self.styles['Normal']))
story.append(Paragraph(f'Date: {self.data.date}', self.styles['Normal']))
story.append(PageBreak())
# Table of contents
story.extend(self._create_toc())
# Content sections
for i, section in enumerate(self.data.sections):
story.extend(self._create_section(i, section))
doc.build(story, onFirstPage=self._header_footer,
onLaterPages=self._header_footer)
# Usage
report_data = ReportData(
title='Q4 2024 Performance Report',
subtitle='Annual Review and 2025 Outlook',
author='Analytics Team',
date='January 2025',
sections=[
ReportSection(
title='Executive Summary',
content='''This report provides a comprehensive overview of our Q4 2024 performance.
Key highlights include a 25% increase in revenue, successful expansion into two new
markets, and the launch of three new product lines.
Customer satisfaction scores reached an all-time high of 92%.''',
),
ReportSection(
title='Financial Performance',
content='''Revenue for Q4 2024 reached $12.5M, representing a 25% increase.
Operating margins improved by 3 percentage points to 18%, driven by operational
efficiencies and favorable product mix.''',
chart=ChartData(
title='Monthly Revenue ($M)',
categories=['Oct', 'Nov', 'Dec'],
values=[3.8, 4.2, 4.5],
),
),
ReportSection(
title='Strategic Initiatives',
content='''During Q4, we completed several key strategic initiatives:
1. Market Expansion: Successfully entered European and APAC markets
2. Product Launch: Introduced three new product lines
3. Technology: Completed cloud migration, reducing costs by 30%
4. Talent: Expanded team by 40 new hires''',
),
],
)
generator = ReportGenerator(report_data, 'report.pdf')
generator.generate()---
Related
- invoice-template.md - Invoice generation
- ../references/pdf-generation-patterns.md - Advanced patterns
{
"metadata": {
"skill": "document-pdf",
"updated": "2026-01-17",
"total_sources": 17,
"description": "PDF creation/extraction libraries plus accessibility and release-quality guidance.",
"version": "2.1"
},
"categories": {
"nodejs_libraries": [
{
"name": "pdf-lib",
"url": "https://pdf-lib.js.org/",
"type": "documentation",
"relevance": "Create and modify PDFs in JavaScript/TypeScript (forms, pages, attachments).",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true,
"tags": ["nodejs", "pdf"]
},
{
"name": "PDFKit",
"url": "https://pdfkit.org/",
"type": "documentation",
"relevance": "Generate PDFs programmatically in Node.js with low-level layout control.",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true,
"tags": ["nodejs", "pdf"]
}
],
"python_libraries": [
{
"name": "pypdf Documentation",
"url": "https://pypdf.readthedocs.io/",
"type": "documentation",
"relevance": "Manipulate PDFs in Python (merge/split/rotate, metadata, encryption).",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true,
"tags": ["python", "pdf"]
},
{
"name": "pdfplumber",
"url": "https://github.com/jsvine/pdfplumber",
"type": "library",
"relevance": "Extract text/tables from PDFs in Python (OCR-free extraction patterns).",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true,
"tags": ["python", "pdf", "extraction"]
},
{
"name": "ReportLab Documentation",
"url": "https://www.reportlab.com/documentation/",
"type": "documentation",
"relevance": "Generate complex PDFs in Python (tables, charts, pagination).",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true,
"tags": ["python", "pdf", "generation"]
},
{
"name": "FPDF2 Documentation",
"url": "https://py-pdf.github.io/fpdf2/",
"type": "documentation",
"relevance": "Modern successor to FPDF with Unicode support, improved image handling, HTML rendering.",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true,
"tags": ["python", "pdf", "generation"]
},
{
"name": "Borb Documentation",
"url": "https://github.com/jorisschellekens/borb",
"type": "library",
"relevance": "Pure Python PDF library for creating and manipulating PDFs with interactive elements.",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true,
"tags": ["python", "pdf", "generation"]
},
{
"name": "WeasyPrint Documentation",
"url": "https://doc.courtbouillon.org/weasyprint/stable/",
"type": "documentation",
"relevance": "CSS3-based HTML to PDF conversion without browser dependency.",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true,
"tags": ["python", "pdf", "html-to-pdf"]
},
{
"name": "Camelot Documentation",
"url": "https://camelot-py.readthedocs.io/",
"type": "documentation",
"relevance": "PDF table extraction with Lattice (bordered) and Stream (borderless) modes.",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true,
"tags": ["python", "pdf", "extraction"]
}
],
"accessibility_and_quality": [
{
"name": "Creating accessible PDFs in Adobe Acrobat",
"url": "https://helpx.adobe.com/acrobat/using/creating-accessible-pdfs.html",
"type": "guide",
"relevance": "Practical PDF accessibility guidance: tagging, reading order, alt text, checks.",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true,
"tags": ["accessibility", "pdf"]
},
{
"name": "Web Content Accessibility Guidelines (WCAG) 2.2 (W3C Recommendation)",
"url": "https://www.w3.org/TR/WCAG22/",
"type": "specification",
"relevance": "Accessibility baseline; relevant when PDFs are used for regulated/enterprise contexts.",
"update_frequency": "static",
"access": "free",
"add_as_web_search": false,
"tags": ["accessibility", "wcag"]
},
{
"name": "EN 301 549 (Accessibility requirements for ICT products and services)",
"url": "https://www.etsi.org/deliver/etsi_en/301500_301599/301549/",
"type": "specification",
"relevance": "Common EU-aligned accessibility requirements referenced in procurement and compliance programs.",
"update_frequency": "periodic",
"access": "free",
"add_as_web_search": true,
"tags": ["accessibility", "en301549", "eu"]
},
{
"name": "PDF/UA (Universal Accessibility) resources",
"url": "https://pdfa.org/resource/pdfua/",
"type": "reference",
"relevance": "PDF accessibility standard and ecosystem resources (useful for understanding tagged PDF requirements).",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true,
"tags": ["accessibility", "pdfua", "standards"]
},
{
"name": "PDF/A (Archival) resources",
"url": "https://pdfa.org/resource/pdfa/",
"type": "reference",
"relevance": "Archival PDF guidance (PDF/A) for long-term retention and reproducible rendering.",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true,
"tags": ["pdfa", "standards", "archival"]
},
{
"name": "PDF Association",
"url": "https://pdfa.org/",
"type": "reference",
"relevance": "Industry resources on PDF standards and accessibility practices (use for deeper dives).",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true,
"tags": ["pdf", "standards"]
},
{
"name": "veraPDF",
"url": "https://verapdf.org/",
"type": "tool",
"relevance": "Open-source validator for PDF/A conformance; useful when archiving or compliance requires PDF/A.",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true,
"tags": ["pdfa", "validation", "tool"]
},
{
"name": "Section508.gov",
"url": "https://www.section508.gov/",
"type": "reference",
"relevance": "US federal accessibility guidance often referenced in procurement and compliance.",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true,
"tags": ["accessibility", "us", "procurement"]
}
]
}
}
PDF Accessibility and Compliance
Patterns for producing accessible, standards-compliant PDF documents.
---
Contents
- Tagged vs Untagged PDFs
- PDF/UA Standard (ISO 14289)
- Creating Tagged PDFs
- Reading Order and Alt Text
- EU EAA and EN 301 549
- Validation Tools
- Decision Guide: When to Invest
- Checklist: PDF Accessibility Review
---
Tagged vs Untagged PDFs
A tagged PDF contains a logical structure tree mapping visual elements to semantic roles (headings, paragraphs, tables, figures). Screen readers depend on this tree. Without tags, assistive technology guesses reading order from character positions, which fails on multi-column layouts, tables, and sidebars.
Untagged PDFs cannot be reliably fixed after the fact. Retroactively tagging a complex document is manual, error-prone, and typically more expensive than regenerating from a structured source. Budget 30-60 minutes per page for manual remediation.
---
PDF/UA Standard (ISO 14289)
Key requirements: every content element tagged or marked as artifact; tag tree reflects logical reading order; all images have alt text; tables use TH/TD with scope; document language declared; fonts embedded with Unicode mappings; no reliance on colour alone. Compliance with PDF/UA generally satisfies EN 301 549 and Section 508 for PDF content.
---
Creating Tagged PDFs
Generate from structured source. Do not post-fix untagged PDFs.
WeasyPrint (Python) — HTML+CSS to Tagged PDF
from weasyprint import HTML
html = """
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8">
<style>
body { font-family: sans-serif; font-size: 12pt; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #333; padding: 6px; }
</style></head>
<body>
<h1>Quarterly Report</h1>
<p>Summary of Q4 metrics.</p>
<table>
<thead><tr><th>Metric</th><th>Value</th></tr></thead>
<tbody><tr><td>Revenue</td><td>$1.2M</td></tr></tbody>
</table>
<img src="chart.png" alt="Bar chart showing quarterly revenue growth">
</body></html>
"""
HTML(string=html).write_pdf('report.pdf', presentational_hints=True)The tag tree mirrors HTML semantics: <h1> becomes Heading, <table> maps to Table/TR/TH/TD, <img alt="..."> becomes Figure with alt text. DOCX export also works: use built-in heading styles, alt text on images, table header rows, then export with "Tagged PDF" enabled.
---
Reading Order and Alt Text
Reading order must match logical content flow. Common failures: multi-column text read across columns, headers injected mid-body, footnotes before referencing paragraphs. Prevention: generate from single-flow HTML or style-based DOCX authoring.
Alt text: describe what the image communicates, not what it looks like. Charts: state the trend ("Revenue grew 23% YoY"). Logos: company name. Keep under 150 characters. Mark decorative images as artifacts.
---
EU EAA and EN 301 549
The European Accessibility Act (EAA, effective June 2025) requires products and services sold in the EU to meet accessibility standards. EN 301 549 clause 10 covers non-web documents and points to WCAG 2.1 AA plus PDF/UA. Customer-facing PDFs (invoices, contracts, statements, product docs) distributed in the EU should be tagged and meet PDF/UA. Internal-only documents have lower regulatory exposure.
---
Validation Tools
| Tool | Platform | Scope |
|---|---|---|
| PAC (PDF Accessibility Checker) | Windows (free) | Full PDF/UA validation |
| Adobe Acrobat Accessibility Checker | Win/macOS | Tags, reading order, alt text |
| axe-pdf (Deque) | CLI/CI | Automated pipeline integration |
| VoiceOver / NVDA | macOS / Windows | Manual screen reader testing |
# Quick tag presence check (Poppler)
pdfinfo report.pdf | grep Tagged
# "Tagged: yes" means tags exist — not that they are correct.Always pair automated checks with at least one manual screen reader pass.
---
Decision Guide: When to Invest
| Scenario | Level |
|---|---|
| Customer-facing PDFs in EU | Full PDF/UA compliance |
| Public marketing / docs | Tagged PDF + alt text + reading order |
| Internal reports, sighted team | Selectable text, bookmarks, clean metadata |
| Archival / legal hold | PDF/A; add tags if public-facing |
| One-off personal exports | No accessibility work needed |
---
Checklist: PDF Accessibility Review
- [ ] Document language declared (
/Langentry) - [ ] PDF is tagged (
/MarkInfowithMarked: true) - [ ] Heading hierarchy correct (H1 > H2 > H3, no skipped levels)
- [ ] All meaningful images have alt text
- [ ] Decorative images marked as artifacts
- [ ] Tables use TH for headers with scope
- [ ] Reading order matches logical flow (screen reader test)
- [ ] Links have descriptive text (not raw URLs)
- [ ] Fonts embedded with Unicode mappings
- [ ] PAC or Acrobat accessibility check passes with zero errors
---
Do / Avoid
Do
- Generate tagged PDFs from semantic HTML (WeasyPrint) or styled DOCX.
- Set document language at the root level.
- Test with a real screen reader at least once per template.
- Automate validation in CI for recurring document types.
Avoid
- Manually tagging complex untagged PDFs — regenerate from source instead.
- Using text boxes or absolute positioning in Word for layout.
- Assuming "it looks fine" means it is accessible.
- Treating accessibility as a post-release patch.
---
Related
- pdf-generation-patterns.md — Layout and generation code
- pdf-extraction-patterns.md — Text and table extraction
- ../assets/pdf-release-checklist.md — Pre-distribution quality gate
PDF Extraction Patterns
Patterns for extracting text, tables, images, and metadata from PDF documents.
---
Text Extraction
Basic Text (pdfplumber)
import pdfplumber
def extract_all_text(pdf_path: str) -> str:
"""Extract text from all pages."""
text_parts = []
with pdfplumber.open(pdf_path) as pdf:
for page in pdf.pages:
text = page.extract_text()
if text:
text_parts.append(text)
return '\n\n'.join(text_parts)Text with Layout Preservation
import pdfplumber
def extract_with_layout(pdf_path: str) -> str:
"""Preserve original layout using character positions."""
with pdfplumber.open(pdf_path) as pdf:
page = pdf.pages[0]
# Extract with layout preservation
text = page.extract_text(
layout=True, # Preserve layout
x_tolerance=3, # Horizontal tolerance
y_tolerance=3, # Vertical tolerance
)
return textSpecific Page Regions
import pdfplumber
def extract_region(pdf_path: str, bbox: tuple) -> str:
"""Extract text from specific region (x0, y0, x1, y1)."""
with pdfplumber.open(pdf_path) as pdf:
page = pdf.pages[0]
# Crop to region
cropped = page.crop(bbox)
text = cropped.extract_text()
return text
# Example: Extract header region
header_text = extract_region('doc.pdf', (0, 0, 612, 100))---
Table Extraction
Simple Tables (pdfplumber)
import pdfplumber
import pandas as pd
def extract_tables(pdf_path: str) -> list[pd.DataFrame]:
"""Extract all tables as DataFrames."""
dataframes = []
with pdfplumber.open(pdf_path) as pdf:
for page in pdf.pages:
tables = page.extract_tables()
for table in tables:
if table and len(table) > 1:
# First row as header
df = pd.DataFrame(table[1:], columns=table[0])
dataframes.append(df)
return dataframesCustom Table Settings
import pdfplumber
def extract_complex_table(pdf_path: str, page_num: int = 0) -> list:
"""Extract table with custom settings for complex layouts."""
table_settings = {
'vertical_strategy': 'text', # 'lines', 'text', or 'explicit'
'horizontal_strategy': 'text',
'snap_tolerance': 3,
'snap_x_tolerance': 3,
'snap_y_tolerance': 3,
'join_tolerance': 3,
'edge_min_length': 3,
'min_words_vertical': 3,
'min_words_horizontal': 1,
'intersection_tolerance': 3,
}
with pdfplumber.open(pdf_path) as pdf:
page = pdf.pages[page_num]
tables = page.extract_tables(table_settings)
return tablesCamelot for Complex Tables
import camelot
# Lattice mode - for tables with visible borders
tables = camelot.read_pdf('document.pdf', flavor='lattice')
# Stream mode - for tables without visible borders
tables = camelot.read_pdf('document.pdf', flavor='stream')
# Access table data
for table in tables:
print(f'Accuracy: {table.accuracy}')
df = table.df
print(df)
# Export
table.to_csv('table.csv')
table.to_excel('table.xlsx')---
Image Extraction
Extract Images (PyMuPDF/fitz)
import fitz # PyMuPDF
from pathlib import Path
def extract_images(pdf_path: str, output_dir: str) -> list[str]:
"""Extract all images from PDF."""
output_path = Path(output_dir)
output_path.mkdir(exist_ok=True)
doc = fitz.open(pdf_path)
saved_images = []
for page_num, page in enumerate(doc):
images = page.get_images()
for img_index, img in enumerate(images):
xref = img[0]
base_image = doc.extract_image(xref)
image_bytes = base_image['image']
image_ext = base_image['ext']
filename = f'page{page_num + 1}_img{img_index + 1}.{image_ext}'
filepath = output_path / filename
with open(filepath, 'wb') as f:
f.write(image_bytes)
saved_images.append(str(filepath))
doc.close()
return saved_imagesImage with Metadata
import fitz
def get_image_info(pdf_path: str) -> list[dict]:
"""Get detailed image information."""
doc = fitz.open(pdf_path)
image_info = []
for page_num, page in enumerate(doc):
for img in page.get_images():
xref = img[0]
base = doc.extract_image(xref)
info = {
'page': page_num + 1,
'xref': xref,
'width': base['width'],
'height': base['height'],
'colorspace': base['colorspace'],
'bpc': base['bpc'], # bits per component
'ext': base['ext'],
'size_bytes': len(base['image']),
}
image_info.append(info)
doc.close()
return image_info---
Metadata Extraction
Document Metadata (pypdf)
from pypdf import PdfReader
def extract_metadata(pdf_path: str) -> dict:
"""Extract PDF metadata."""
reader = PdfReader(pdf_path)
metadata = {
'num_pages': len(reader.pages),
'is_encrypted': reader.is_encrypted,
}
if reader.metadata:
metadata.update({
'title': reader.metadata.get('/Title'),
'author': reader.metadata.get('/Author'),
'subject': reader.metadata.get('/Subject'),
'creator': reader.metadata.get('/Creator'),
'producer': reader.metadata.get('/Producer'),
'creation_date': reader.metadata.get('/CreationDate'),
'modification_date': reader.metadata.get('/ModDate'),
})
return metadataForm Fields
from pypdf import PdfReader
def extract_form_fields(pdf_path: str) -> dict:
"""Extract form field values."""
reader = PdfReader(pdf_path)
fields = {}
if reader.get_fields():
for field_name, field_data in reader.get_fields().items():
value = field_data.get('/V')
field_type = field_data.get('/FT')
fields[field_name] = {
'value': value,
'type': str(field_type) if field_type else None,
}
return fields---
OCR Integration
Tesseract OCR for Scanned PDFs
import fitz
from PIL import Image
import pytesseract
import io
def ocr_pdf(pdf_path: str) -> str:
"""Extract text from scanned PDF using OCR."""
doc = fitz.open(pdf_path)
text_parts = []
for page in doc:
# Render page as image
pix = page.get_pixmap(dpi=300)
img_data = pix.tobytes('png')
# OCR with Tesseract
image = Image.open(io.BytesIO(img_data))
text = pytesseract.image_to_string(image)
text_parts.append(text)
doc.close()
return '\n\n'.join(text_parts)Hybrid Extraction (Text + OCR)
import pdfplumber
import fitz
from PIL import Image
import pytesseract
import io
def hybrid_extract(pdf_path: str, ocr_threshold: int = 50) -> str:
"""Use OCR only when text extraction fails."""
text_parts = []
with pdfplumber.open(pdf_path) as pdf:
doc = fitz.open(pdf_path)
for i, page in enumerate(pdf.pages):
text = page.extract_text()
if text and len(text.strip()) > ocr_threshold:
# Text extraction worked
text_parts.append(text)
else:
# Fall back to OCR
fitz_page = doc[i]
pix = fitz_page.get_pixmap(dpi=300)
img = Image.open(io.BytesIO(pix.tobytes('png')))
ocr_text = pytesseract.image_to_string(img)
text_parts.append(ocr_text)
doc.close()
return '\n\n'.join(text_parts)---
Batch Processing
Process Multiple PDFs
from pathlib import Path
from concurrent.futures import ProcessPoolExecutor
import pdfplumber
def process_pdf(pdf_path: Path) -> dict:
"""Process single PDF and return results."""
try:
with pdfplumber.open(pdf_path) as pdf:
text = '\n'.join(
page.extract_text() or ''
for page in pdf.pages
)
return {
'file': pdf_path.name,
'pages': len(pdf.pages),
'text': text,
'success': True,
}
except Exception as e:
return {
'file': pdf_path.name,
'error': str(e),
'success': False,
}
def batch_process(input_dir: str, workers: int = 4) -> list[dict]:
"""Process all PDFs in directory using multiprocessing."""
pdf_files = list(Path(input_dir).glob('*.pdf'))
with ProcessPoolExecutor(max_workers=workers) as executor:
results = list(executor.map(process_pdf, pdf_files))
return results---
Error Handling
Robust Extraction
import pdfplumber
from pypdf import PdfReader
def safe_extract(pdf_path: str) -> dict:
"""Extract with fallback strategies."""
result = {
'text': None,
'tables': [],
'metadata': {},
'errors': [],
}
# Try pdfplumber first
try:
with pdfplumber.open(pdf_path) as pdf:
result['text'] = '\n'.join(
page.extract_text() or ''
for page in pdf.pages
)
for page in pdf.pages:
result['tables'].extend(page.extract_tables())
except Exception as e:
result['errors'].append(f'pdfplumber: {e}')
# Get metadata with pypdf
try:
reader = PdfReader(pdf_path)
if reader.metadata:
result['metadata'] = dict(reader.metadata)
except Exception as e:
result['errors'].append(f'pypdf: {e}')
return result---
Related
- pdf-generation-patterns.md - Creating PDFs
- ../SKILL.md - Quick reference
PDF Forms and Interactive Elements
Patterns for creating, filling, reading, and flattening PDF forms.
---
Contents
- AcroForms vs XFA
- Creating Forms with pdf-lib
- Filling Forms with pdf-lib
- Reading Form Data with pypdf
- Flattening Forms
- Digital Signatures Overview
- Common Pitfalls
- Checklist: Form Development Review
---
AcroForms vs XFA
AcroForms are the standard interactive form format. Every major PDF library supports them. Use AcroForms for all new work.
XFA is Adobe-proprietary, deprecated since PDF 2.0 (2017). Chrome, Firefox, Edge, Preview, and most libraries cannot render XFA. If you receive XFA, re-create as AcroForm. Never create new XFA forms.
---
Creating Forms with pdf-lib
import { PDFDocument, StandardFonts } from 'pdf-lib';
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage([600, 800]);
const form = pdfDoc.getForm();
// Text, dropdown, checkbox, radio group
const name = form.createTextField('fullName');
name.addToPage(page, { x: 150, y: 710, width: 250, height: 20 });
const country = form.createDropdown('country');
country.addOptions(['US', 'UK', 'Germany']);
country.addToPage(page, { x: 150, y: 670, width: 250, height: 20 });
const agree = form.createCheckBox('agreeTerms');
agree.addToPage(page, { x: 150, y: 635, width: 15, height: 15 });
const priority = form.createRadioGroup('priority');
priority.addOptionToPage('low', page, { x: 150, y: 597, width: 12, height: 12 });
priority.addOptionToPage('high', page, { x: 210, y: 597, width: 12, height: 12 });---
Filling Forms with pdf-lib
const pdfDoc = await PDFDocument.load(existingBytes);
const form = pdfDoc.getForm();
form.getTextField('fullName').setText('Jane Smith');
form.getDropdown('country').select('Germany');
form.getCheckBox('agreeTerms').check();
form.getRadioGroup('priority').select('high');
// Discover field names in unknown forms
form.getFields().forEach(f => console.log(`${f.getName()} (${f.constructor.name})`));---
Reading Form Data with pypdf
from pypdf import PdfReader, PdfWriter
def read_form(pdf_path: str) -> dict:
fields = PdfReader(pdf_path).get_fields()
if not fields:
return {}
return {n: str(f.get('/V', '')) for n, f in fields.items()}
def fill_form(input_path: str, output_path: str, data: dict) -> None:
reader = PdfReader(input_path)
writer = PdfWriter()
writer.append(reader)
writer.update_page_form_field_values(writer.pages[0], data)
with open(output_path, 'wb') as f:
writer.write(f)---
Flattening Forms
Flattening converts fields into static page content. Use before distributing completed forms.
// pdf-lib — straightforward
pdfDoc.getForm().flatten();# pypdf — remove AcroForm entry after filling
writer = PdfWriter()
writer.append(PdfReader('filled.pdf'))
if '/AcroForm' in writer._root_object:
del writer._root_object['/AcroForm']---
Digital Signatures Overview
No general-purpose library (pdf-lib, pypdf, pdfkit) handles signing end-to-end. Use: pyHanko (self-hosted PKCS#11/PFX), DocuSign/Adobe Sign API (production e-sig), or JSignPdf (batch CLI). Do not implement PKCS#7/CMS from scratch.
---
Common Pitfalls
- Field naming conflicts: merging PDFs with duplicate names causes overwrites. Namespace names (
form1_name) before merging. - Font embedding: fields using non-embedded fonts display squares. Always embed and call
form.updateFieldAppearances(font). - JavaScript in PDFs: most non-Adobe viewers ignore it. Validate server-side, not in PDF JS.
- Appearance streams: some viewers skip regeneration. Call
updateFieldAppearances()explicitly.
---
Checklist: Form Development Review
- [ ] All fields use AcroForms (not XFA)
- [ ] Field names unique and namespaced if merging planned
- [ ] Tab order follows logical field sequence
- [ ] Fonts used in fields are embedded
- [ ] Form displays correctly in Adobe Reader, Chrome, and Preview
- [ ] Flatten works without missing values
- [ ] No business logic relies on embedded PDF JavaScript
---
Do / Avoid
Do
- Use AcroForms for all new forms.
- Flatten before distributing completed documents.
- Test in at least three viewers.
- Embed all fonts used in form fields.
Avoid
- Creating XFA forms.
- Relying on PDF JavaScript for validation.
- Distributing editable forms when the intent is a final record.
- Assuming field names match across different PDF templates.
---
Related
- pdf-generation-patterns.md — Layout and generation code
- pdf-extraction-patterns.md — Reading form data and metadata
- pdf-security-redaction.md — Encryption and permissions
PDF Generation Patterns
Advanced patterns for creating complex PDF documents with precise layouts.
---
Multi-Page Documents
Page Management (PDFKit)
import PDFDocument from 'pdfkit';
const doc = new PDFDocument({ autoFirstPage: false });
// Add pages with different sizes
doc.addPage({ size: 'A4' }); // Standard page
doc.addPage({ size: 'LETTER' }); // US Letter
doc.addPage({ size: [612, 792] }); // Custom dimensions
doc.addPage({
size: 'A4',
layout: 'landscape',
margins: { top: 50, bottom: 50, left: 72, right: 72 }
});Page Breaks (ReportLab)
from reportlab.platypus import SimpleDocTemplate, Paragraph, PageBreak, Spacer
from reportlab.lib.styles import getSampleStyleSheet
doc = SimpleDocTemplate('multi_page.pdf')
styles = getSampleStyleSheet()
story = []
# Content that spans pages
for i in range(50):
story.append(Paragraph(f'Paragraph {i}', styles['Normal']))
story.append(Spacer(1, 12))
# Force page break
story.append(PageBreak())
story.append(Paragraph('New Section', styles['Heading1']))
doc.build(story)---
Headers and Footers
Running Headers (ReportLab)
from reportlab.platypus import SimpleDocTemplate, Paragraph
from reportlab.lib.pagesizes import letter
def header_footer(canvas, doc):
canvas.saveState()
# Header
canvas.setFont('Helvetica-Bold', 10)
canvas.drawString(72, letter[1] - 40, 'Company Name')
canvas.drawRightString(letter[0] - 72, letter[1] - 40, 'Confidential')
# Footer with page number
canvas.setFont('Helvetica', 9)
canvas.drawCentredString(letter[0] / 2, 30, f'Page {doc.page}')
canvas.restoreState()
doc = SimpleDocTemplate('with_headers.pdf')
doc.build(story, onFirstPage=header_footer, onLaterPages=header_footer)Headers (PDFKit)
doc.on('pageAdded', () => {
const top = doc.page.margins.top;
doc.save();
doc.fontSize(10)
.text('Header Text', 50, top - 30, { align: 'left' })
.text(`Page ${doc.bufferedPageRange().count}`, 0, top - 30, { align: 'right' });
doc.restore();
});---
Complex Tables
Styled Table (ReportLab)
from reportlab.platypus import Table, TableStyle
from reportlab.lib import colors
data = [
['Product', 'Qty', 'Price', 'Total'],
['Widget A', '10', '$50.00', '$500.00'],
['Widget B', '5', '$75.00', '$375.00'],
['Widget C', '20', '$25.00', '$500.00'],
['', '', 'Subtotal', '$1,375.00'],
['', '', 'Tax (8%)', '$110.00'],
['', '', 'Total', '$1,485.00'],
]
table = Table(data, colWidths=[200, 60, 80, 80])
table.setStyle(TableStyle([
# Header row
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2c3e50')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
# Data rows
('FONTNAME', (0, 1), (-1, -4), 'Helvetica'),
('ALIGN', (1, 1), (-1, -1), 'RIGHT'),
# Alternating row colors
('BACKGROUND', (0, 1), (-1, 1), colors.HexColor('#ecf0f1')),
('BACKGROUND', (0, 3), (-1, 3), colors.HexColor('#ecf0f1')),
# Totals section
('FONTNAME', (2, -3), (-1, -1), 'Helvetica-Bold'),
('LINEABOVE', (2, -3), (-1, -3), 1, colors.black),
# Grid
('GRID', (0, 0), (-1, -4), 0.5, colors.grey),
('BOX', (0, 0), (-1, -1), 1, colors.black),
]))---
Images and Graphics
Image Placement (pdf-lib)
import { PDFDocument } from 'pdf-lib';
import fs from 'fs';
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage([600, 800]);
// Embed PNG
const pngBytes = fs.readFileSync('logo.png');
const pngImage = await pdfDoc.embedPng(pngBytes);
const pngDims = pngImage.scale(0.5);
page.drawImage(pngImage, {
x: 50,
y: 700,
width: pngDims.width,
height: pngDims.height,
});
// Embed JPG
const jpgBytes = fs.readFileSync('photo.jpg');
const jpgImage = await pdfDoc.embedJpg(jpgBytes);
page.drawImage(jpgImage, {
x: 50,
y: 400,
width: 200,
height: 150,
});Charts (ReportLab)
from reportlab.graphics.shapes import Drawing
from reportlab.graphics.charts.barcharts import VerticalBarChart
from reportlab.graphics.charts.piecharts import Pie
# Bar chart
drawing = Drawing(400, 200)
chart = VerticalBarChart()
chart.x = 50
chart.y = 50
chart.width = 300
chart.height = 125
chart.data = [[10, 20, 30, 40], [15, 25, 35, 45]]
chart.categoryAxis.categoryNames = ['Q1', 'Q2', 'Q3', 'Q4']
drawing.add(chart)
# Pie chart
pie_drawing = Drawing(200, 200)
pie = Pie()
pie.x = 50
pie.y = 50
pie.width = 100
pie.height = 100
pie.data = [30, 25, 20, 15, 10]
pie.labels = ['A', 'B', 'C', 'D', 'E']
pie_drawing.add(pie)---
Fonts and Typography
Custom Fonts (PDFKit)
// Register custom fonts
doc.registerFont('CustomFont', 'fonts/CustomFont-Regular.ttf');
doc.registerFont('CustomFont-Bold', 'fonts/CustomFont-Bold.ttf');
doc.font('CustomFont').fontSize(12).text('Regular text');
doc.font('CustomFont-Bold').fontSize(14).text('Bold heading');Font Embedding (pdf-lib)
import { PDFDocument, StandardFonts } from 'pdf-lib';
import fontkit from '@pdf-lib/fontkit';
import fs from 'fs';
const pdfDoc = await PDFDocument.create();
pdfDoc.registerFontkit(fontkit);
// Embed custom font
const fontBytes = fs.readFileSync('fonts/Roboto-Regular.ttf');
const customFont = await pdfDoc.embedFont(fontBytes);
// Use standard font
const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica);
const page = pdfDoc.addPage();
page.drawText('Custom Font Text', {
font: customFont,
size: 24,
x: 50,
y: 700,
});---
Performance Optimization
Streaming Large PDFs (PDFKit)
import PDFDocument from 'pdfkit';
import fs from 'fs';
const doc = new PDFDocument({ bufferPages: false }); // Don't buffer
doc.pipe(fs.createWriteStream('large.pdf'));
// Generate content incrementally
for (let i = 0; i < 1000; i++) {
doc.addPage();
doc.text(`Page ${i + 1}`);
doc.flushPages(); // Write pages immediately
}
doc.end();Batch Processing (pypdf)
from pypdf import PdfWriter, PdfReader
from pathlib import Path
def batch_add_watermark(input_dir: Path, watermark_pdf: str, output_dir: Path):
watermark = PdfReader(watermark_pdf).pages[0]
for pdf_file in input_dir.glob('*.pdf'):
reader = PdfReader(pdf_file)
writer = PdfWriter()
for page in reader.pages:
page.merge_page(watermark)
writer.add_page(page)
output_path = output_dir / pdf_file.name
with open(output_path, 'wb') as f:
writer.write(f)---
Related
- pdf-extraction-patterns.md - Text and table extraction
- ../assets/invoice-template.md - Invoice generation
- ../assets/report-template.md - Report generation
PDF Security, Encryption, and Redaction
Patterns for protecting PDF content, controlling permissions, and performing verified redaction.
---
Contents
- Encryption Types
- Password Protection
- Setting Permissions
- Real vs Fake Redaction
- Redaction Workflow
- Metadata Scrubbing
- Do / Avoid
- Checklist: Pre-Distribution Security Review
---
Encryption Types
| Algorithm | Key | Status |
|---|---|---|
| RC4 40-bit | Broken | Crackable in seconds. Never use. |
| RC4 128-bit | Weak | Not recommended for new documents. |
| AES 128-bit | Acceptable | PDF 1.6+. |
| AES 256-bit | Recommended | PDF 2.0. Use for all new work. |
PDF uses two passwords: user (to open) and owner (to change permissions). Permission flags are viewer-enforced, not cryptographic. Encryption prevents content access; permissions are advisory.
---
Password Protection
pypdf
from pypdf import PdfReader, PdfWriter
reader = PdfReader('report.pdf')
writer = PdfWriter()
for page in reader.pages:
writer.add_page(page)
writer.encrypt(user_password='viewpass', owner_password='adminpass', algorithm='AES-256')
with open('encrypted.pdf', 'wb') as f:
writer.write(f)pdf-lib
const pdfDoc = await PDFDocument.load(fs.readFileSync('report.pdf'));
const bytes = await pdfDoc.save({
userPassword: 'viewpass', ownerPassword: 'adminpass',
permissions: { printing: 'highResolution', modifying: false, copying: false,
fillingForms: true, contentAccessibility: true },
});---
Setting Permissions
from pypdf.constants import UserAccessPermissions
permissions = UserAccessPermissions.PRINT | UserAccessPermissions.PRINT_TO_REPRESENTATION
writer.encrypt(user_password='', owner_password='adminpass',
algorithm='AES-256', permissions_flag=permissions)Common flags: PRINT, MODIFY, EXTRACT, FILL_FORM, PRINT_TO_REPRESENTATION.
---
Real vs Fake Redaction
Fake: black rectangles, background-coloured text, overlaid shapes. All leave original text in the content stream. Anyone can copy or extract it. This is the most common PDF data breach source.
Real: permanently removes content bytes. After real redaction, original text no longer exists in the file.
---
Redaction Workflow
Three phases: mark, apply, verify.
import fitz # PyMuPDF
# 1. MARK
doc = fitz.open('sensitive.pdf')
for page in doc:
for pattern in ['SSN: \\d{3}-\\d{2}-\\d{4}', 'CONFIDENTIAL']:
for inst in page.search_for(pattern):
page.add_redact_annot(inst, fill=(0, 0, 0), text='[REDACTED]')
# 2. APPLY — permanently destroys content
for page in doc:
page.apply_redactions()
doc.save('redacted.pdf', garbage=4, deflate=True)Save with garbage=4 to clean orphaned objects. Then verify:
import pdfplumber
with pdfplumber.open('redacted.pdf') as pdf:
for i, page in enumerate(pdf.pages):
text = page.extract_text() or ''
for term in ['123-45-6789', 'CONFIDENTIAL']:
assert term.lower() not in text.lower(), f"Page {i+1}: '{term}' remains"Manual checks: select/copy in redacted areas, search in viewer, pdftotext redacted.pdf - | grep -i "secret".
---
Metadata Scrubbing
python3 scripts/scrub_metadata.py input.pdf cleaned.pdfSee scripts/scrub_metadata.py for implementation. For deeper scrubbing (XMP, embedded files, JS), use PyMuPDF's doc.scrub(metadata=True, javascript=True, embedded_files=True, xml_metadata=True) and save with garbage=4.
---
Do / Avoid
Do
- Use AES-256 for all password-protected PDFs.
- Use real redaction (
apply_redactions()) that removes content bytes. - Verify redaction with extraction and copy/paste tests.
- Scrub metadata before external distribution.
Avoid
- RC4 encryption (40 or 128-bit).
- Black rectangles as "redaction" (content remains extractable).
- Assuming permission flags stop a determined attacker.
- Skipping verification after redaction.
---
Checklist: Pre-Distribution Security Review
- [ ] Encryption uses AES-256; owner password differs from user password
- [ ] Permission flags match intended restrictions
- [ ] Sensitive content uses real redaction, not overlays
- [ ] Redaction verified: extraction and copy/paste yield nothing
- [ ] Metadata scrubbed (author, creator, producer, timestamps)
- [ ] No embedded files, JavaScript, or hidden layers remain
- [ ] Saved with
garbage=4to remove orphaned objects
---
Related
- pdf-forms-interactive.md — Form creation and filling
- pdf-generation-patterns.md — Layout and generation code
- pdf-accessibility-compliance.md — Tags and compliance
- ../scripts/scrub_metadata.py — Metadata scrubbing helper
#!/usr/bin/env python3
from __future__ import annotations
import argparse
from pathlib import Path
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Merge PDFs in order.",
epilog="Dependencies: pip install pypdf",
)
parser.add_argument("output_pdf", type=Path)
parser.add_argument("input_pdfs", nargs="+", type=Path)
return parser.parse_args()
def main() -> int:
args = parse_args()
from pypdf import PdfMerger
args.output_pdf.parent.mkdir(parents=True, exist_ok=True)
merger = PdfMerger()
try:
for pdf_path in args.input_pdfs:
merger.append(str(pdf_path))
merger.write(str(args.output_pdf))
finally:
merger.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
from __future__ import annotations
import argparse
from pathlib import Path
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Rotate all pages of a PDF by 90/180/270 degrees.",
epilog="Dependencies: pip install pypdf",
)
parser.add_argument("input_pdf", type=Path)
parser.add_argument("output_pdf", type=Path)
parser.add_argument("--degrees", type=int, choices=(90, 180, 270), required=True)
return parser.parse_args()
def main() -> int:
args = parse_args()
from pypdf import PdfReader, PdfWriter
reader = PdfReader(str(args.input_pdf))
writer = PdfWriter()
for page in reader.pages:
page.rotate(args.degrees)
writer.add_page(page)
args.output_pdf.parent.mkdir(parents=True, exist_ok=True)
with args.output_pdf.open("wb") as f:
writer.write(f)
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
from __future__ import annotations
import argparse
from pathlib import Path
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Rewrite a PDF while clearing document-info metadata fields.",
epilog=(
"Dependencies: pip install pypdf\n"
"Note: some producers may still add a /Producer entry on write."
),
)
parser.add_argument("input_pdf", type=Path)
parser.add_argument("output_pdf", type=Path)
return parser.parse_args()
def main() -> int:
args = parse_args()
from pypdf import PdfReader, PdfWriter
reader = PdfReader(str(args.input_pdf))
writer = PdfWriter()
for page in reader.pages:
writer.add_page(page)
writer.add_metadata({})
args.output_pdf.parent.mkdir(parents=True, exist_ok=True)
with args.output_pdf.open("wb") as f:
writer.write(f)
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
from __future__ import annotations
import argparse
from pathlib import Path
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Split a PDF into one PDF per page (default) or extract a page range.",
epilog="Dependencies: pip install pypdf",
)
parser.add_argument("input_pdf", type=Path)
parser.add_argument("output_dir", type=Path)
parser.add_argument("--each-page", action="store_true", help="Write one file per page.")
parser.add_argument(
"--range",
dest="page_range",
default=None,
help="1-based inclusive range like 3-10 (writes a single PDF).",
)
parser.add_argument(
"--prefix",
default="page",
help="Filename prefix when using --each-page (default: page).",
)
return parser.parse_args()
def parse_range(page_range: str, num_pages: int) -> tuple[int, int]:
start_str, end_str = page_range.split("-", 1)
start = max(1, int(start_str))
end = min(num_pages, int(end_str))
if start > end:
raise ValueError("Invalid --range; start must be <= end.")
return start - 1, end - 1
def main() -> int:
args = parse_args()
if (args.page_range is None) == (not args.each_page):
raise SystemExit("Choose exactly one of: --each-page OR --range 3-10")
from pypdf import PdfReader, PdfWriter
reader = PdfReader(str(args.input_pdf))
args.output_dir.mkdir(parents=True, exist_ok=True)
if args.each_page:
for i, page in enumerate(reader.pages, start=1):
writer = PdfWriter()
writer.add_page(page)
out_path = args.output_dir / f"{args.prefix}_{i:03d}.pdf"
with out_path.open("wb") as f:
writer.write(f)
return 0
start_idx, end_idx = parse_range(args.page_range, len(reader.pages))
writer = PdfWriter()
for i in range(start_idx, end_idx + 1):
writer.add_page(reader.pages[i])
out_path = args.output_dir / f"{args.prefix}_{start_idx + 1:03d}-{end_idx + 1:03d}.pdf"
with out_path.open("wb") as f:
writer.write(f)
return 0
if __name__ == "__main__":
raise SystemExit(main())
Related skills
How it compares
Choose document-pdf over generic Read tool usage when source material is PDF-encoded documentation or research papers agents cannot parse natively.
FAQ
What file types does document-pdf handle?
document-pdf targets PDF files including technical documentation, research papers, and product specifications. Coding agents use it to extract and summarize PDF text before answering implementation questions.
How popular is document-pdf on skills.sh?
document-pdf in vasilyu1983/ai-agents-public ranks #5 on skills.sh with 469 installs. Developers add it when agent sessions routinely depend on PDF source documents.
When should an agent invoke document-pdf?
Invoke document-pdf when a user references .pdf files, uploads specification documents, or asks to read research papers. The skill structures PDF content for downstream coding or summarization tasks.