Initial commit
This commit is contained in:
621
skills/reportlab/SKILL.md
Normal file
621
skills/reportlab/SKILL.md
Normal file
@@ -0,0 +1,621 @@
|
||||
---
|
||||
name: reportlab
|
||||
description: "PDF generation toolkit. Create invoices, reports, certificates, forms, charts, tables, barcodes, QR codes, Canvas/Platypus APIs, for professional document automation."
|
||||
---
|
||||
|
||||
# ReportLab PDF Generation
|
||||
|
||||
## Overview
|
||||
|
||||
ReportLab is a powerful Python library for programmatic PDF generation. Create anything from simple documents to complex reports with tables, charts, images, and interactive forms.
|
||||
|
||||
**Two main approaches:**
|
||||
- **Canvas API** (low-level): Direct drawing with coordinate-based positioning - use for precise layouts
|
||||
- **Platypus** (high-level): Flowing document layout with automatic page breaks - use for multi-page documents
|
||||
|
||||
**Core capabilities:**
|
||||
- Text with rich formatting and custom fonts
|
||||
- Tables with complex styling and cell spanning
|
||||
- Charts (bar, line, pie, area, scatter)
|
||||
- Barcodes and QR codes (Code128, EAN, QR, etc.)
|
||||
- Images with transparency
|
||||
- PDF features (links, bookmarks, forms, encryption)
|
||||
|
||||
## Choosing the Right Approach
|
||||
|
||||
### Use Canvas API when:
|
||||
- Creating labels, business cards, certificates
|
||||
- Precise positioning is critical (x, y coordinates)
|
||||
- Single-page documents or simple layouts
|
||||
- Drawing graphics, shapes, and custom designs
|
||||
- Adding barcodes or QR codes at specific locations
|
||||
|
||||
### Use Platypus when:
|
||||
- Creating multi-page documents (reports, articles, books)
|
||||
- Content should flow automatically across pages
|
||||
- Need headers/footers that repeat on each page
|
||||
- Working with paragraphs that can split across pages
|
||||
- Building complex documents with table of contents
|
||||
|
||||
### Use Both when:
|
||||
- Complex reports need both flowing content AND precise positioning
|
||||
- Adding headers/footers to Platypus documents (use `onPage` callback with Canvas)
|
||||
- Embedding custom graphics (Canvas) within flowing documents (Platypus)
|
||||
|
||||
## Quick Start Examples
|
||||
|
||||
### Simple Canvas Document
|
||||
|
||||
```python
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.lib.units import inch
|
||||
|
||||
c = canvas.Canvas("output.pdf", pagesize=letter)
|
||||
width, height = letter
|
||||
|
||||
# Draw text
|
||||
c.setFont("Helvetica-Bold", 24)
|
||||
c.drawString(inch, height - inch, "Hello ReportLab!")
|
||||
|
||||
# Draw a rectangle
|
||||
c.setFillColorRGB(0.2, 0.4, 0.8)
|
||||
c.rect(inch, 5*inch, 4*inch, 2*inch, fill=1)
|
||||
|
||||
# Save
|
||||
c.showPage()
|
||||
c.save()
|
||||
```
|
||||
|
||||
### Simple Platypus Document
|
||||
|
||||
```python
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
|
||||
from reportlab.lib.styles import getSampleStyleSheet
|
||||
from reportlab.lib.units import inch
|
||||
|
||||
doc = SimpleDocTemplate("output.pdf", pagesize=letter)
|
||||
story = []
|
||||
styles = getSampleStyleSheet()
|
||||
|
||||
# Add content
|
||||
story.append(Paragraph("Document Title", styles['Title']))
|
||||
story.append(Spacer(1, 0.2*inch))
|
||||
story.append(Paragraph("This is body text with <b>bold</b> and <i>italic</i>.", styles['BodyText']))
|
||||
|
||||
# Build PDF
|
||||
doc.build(story)
|
||||
```
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Creating Tables
|
||||
|
||||
Tables work with both Canvas (via Drawing) and Platypus (as Flowables):
|
||||
|
||||
```python
|
||||
from reportlab.platypus import Table, TableStyle
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.units import inch
|
||||
|
||||
# Define data
|
||||
data = [
|
||||
['Product', 'Q1', 'Q2', 'Q3', 'Q4'],
|
||||
['Widget A', '100', '150', '130', '180'],
|
||||
['Widget B', '80', '120', '110', '160'],
|
||||
]
|
||||
|
||||
# Create table
|
||||
table = Table(data, colWidths=[2*inch, 1*inch, 1*inch, 1*inch, 1*inch])
|
||||
|
||||
# Apply styling
|
||||
style = TableStyle([
|
||||
# Header row
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.darkblue),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||
|
||||
# Data rows
|
||||
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightgrey]),
|
||||
('GRID', (0, 0), (-1, -1), 1, colors.black),
|
||||
])
|
||||
|
||||
table.setStyle(style)
|
||||
|
||||
# Add to Platypus story
|
||||
story.append(table)
|
||||
|
||||
# Or draw on Canvas
|
||||
table.wrapOn(c, width, height)
|
||||
table.drawOn(c, x, y)
|
||||
```
|
||||
|
||||
**Detailed table reference:** See `references/tables_reference.md` for cell spanning, borders, alignment, and advanced styling.
|
||||
|
||||
### Creating Charts
|
||||
|
||||
Charts use the graphics framework and can be added to both Canvas and Platypus:
|
||||
|
||||
```python
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
from reportlab.graphics.charts.barcharts import VerticalBarChart
|
||||
from reportlab.lib import colors
|
||||
|
||||
# Create drawing
|
||||
drawing = Drawing(400, 200)
|
||||
|
||||
# Create chart
|
||||
chart = VerticalBarChart()
|
||||
chart.x = 50
|
||||
chart.y = 50
|
||||
chart.width = 300
|
||||
chart.height = 125
|
||||
|
||||
# Set data
|
||||
chart.data = [[100, 150, 130, 180, 140]]
|
||||
chart.categoryAxis.categoryNames = ['Q1', 'Q2', 'Q3', 'Q4', 'Q5']
|
||||
|
||||
# Style
|
||||
chart.bars[0].fillColor = colors.blue
|
||||
chart.valueAxis.valueMin = 0
|
||||
chart.valueAxis.valueMax = 200
|
||||
|
||||
# Add to drawing
|
||||
drawing.add(chart)
|
||||
|
||||
# Use in Platypus
|
||||
story.append(drawing)
|
||||
|
||||
# Or render directly to PDF
|
||||
from reportlab.graphics import renderPDF
|
||||
renderPDF.drawToFile(drawing, 'chart.pdf', 'Chart Title')
|
||||
```
|
||||
|
||||
**Available chart types:** Bar (vertical/horizontal), Line, Pie, Area, Scatter
|
||||
**Detailed charts reference:** See `references/charts_reference.md` for all chart types, styling, legends, and customization.
|
||||
|
||||
### Adding Barcodes and QR Codes
|
||||
|
||||
```python
|
||||
from reportlab.graphics.barcode import code128
|
||||
from reportlab.graphics.barcode.qr import QrCodeWidget
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
from reportlab.graphics import renderPDF
|
||||
|
||||
# Code128 barcode (general purpose)
|
||||
barcode = code128.Code128("ABC123456789", barHeight=0.5*inch)
|
||||
|
||||
# On Canvas
|
||||
barcode.drawOn(c, x, y)
|
||||
|
||||
# QR Code
|
||||
qr = QrCodeWidget("https://example.com")
|
||||
qr.barWidth = 2*inch
|
||||
qr.barHeight = 2*inch
|
||||
|
||||
# Wrap in Drawing for Platypus
|
||||
d = Drawing()
|
||||
d.add(qr)
|
||||
story.append(d)
|
||||
```
|
||||
|
||||
**Supported formats:** Code128, Code39, EAN-13, EAN-8, UPC-A, ISBN, QR, Data Matrix, and 20+ more
|
||||
**Detailed barcode reference:** See `references/barcodes_reference.md` for all formats and usage examples.
|
||||
|
||||
### Working with Text and Fonts
|
||||
|
||||
```python
|
||||
from reportlab.platypus import Paragraph
|
||||
from reportlab.lib.styles import ParagraphStyle
|
||||
from reportlab.lib.enums import TA_JUSTIFY
|
||||
|
||||
# Create custom style
|
||||
custom_style = ParagraphStyle(
|
||||
'CustomStyle',
|
||||
fontSize=12,
|
||||
leading=14, # Line spacing
|
||||
alignment=TA_JUSTIFY,
|
||||
spaceAfter=10,
|
||||
textColor=colors.black,
|
||||
)
|
||||
|
||||
# Paragraph with inline formatting
|
||||
text = """
|
||||
This paragraph has <b>bold</b>, <i>italic</i>, and <u>underlined</u> text.
|
||||
You can also use <font color="blue">colors</font> and <font size="14">different sizes</font>.
|
||||
Chemical formula: H<sub>2</sub>O, Einstein: E=mc<sup>2</sup>
|
||||
"""
|
||||
|
||||
para = Paragraph(text, custom_style)
|
||||
story.append(para)
|
||||
```
|
||||
|
||||
**Using custom fonts:**
|
||||
|
||||
```python
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
|
||||
# Register TrueType font
|
||||
pdfmetrics.registerFont(TTFont('CustomFont', 'CustomFont.ttf'))
|
||||
|
||||
# Use in Canvas
|
||||
c.setFont('CustomFont', 12)
|
||||
|
||||
# Use in Paragraph style
|
||||
style = ParagraphStyle('Custom', fontName='CustomFont', fontSize=12)
|
||||
```
|
||||
|
||||
**Detailed text reference:** See `references/text_and_fonts.md` for paragraph styles, font families, Asian languages, Greek letters, and formatting.
|
||||
|
||||
### Adding Images
|
||||
|
||||
```python
|
||||
from reportlab.platypus import Image
|
||||
from reportlab.lib.units import inch
|
||||
|
||||
# In Platypus
|
||||
img = Image('photo.jpg', width=4*inch, height=3*inch)
|
||||
story.append(img)
|
||||
|
||||
# Maintain aspect ratio
|
||||
img = Image('photo.jpg', width=4*inch, height=3*inch, kind='proportional')
|
||||
|
||||
# In Canvas
|
||||
c.drawImage('photo.jpg', x, y, width=4*inch, height=3*inch)
|
||||
|
||||
# With transparency (mask white background)
|
||||
c.drawImage('logo.png', x, y, mask=[255,255,255,255,255,255])
|
||||
```
|
||||
|
||||
### Creating Forms
|
||||
|
||||
```python
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.colors import black, white, lightgrey
|
||||
|
||||
c = canvas.Canvas("form.pdf")
|
||||
|
||||
# Text field
|
||||
c.acroForm.textfield(
|
||||
name="name",
|
||||
tooltip="Enter your name",
|
||||
x=100, y=700,
|
||||
width=200, height=20,
|
||||
borderColor=black,
|
||||
fillColor=lightgrey,
|
||||
forceBorder=True
|
||||
)
|
||||
|
||||
# Checkbox
|
||||
c.acroForm.checkbox(
|
||||
name="agree",
|
||||
x=100, y=650,
|
||||
size=20,
|
||||
buttonStyle='check',
|
||||
checked=False
|
||||
)
|
||||
|
||||
# Dropdown
|
||||
c.acroForm.choice(
|
||||
name="country",
|
||||
x=100, y=600,
|
||||
width=150, height=20,
|
||||
options=[("United States", "US"), ("Canada", "CA")],
|
||||
forceBorder=True
|
||||
)
|
||||
|
||||
c.save()
|
||||
```
|
||||
|
||||
**Detailed PDF features reference:** See `references/pdf_features.md` for forms, links, bookmarks, encryption, and metadata.
|
||||
|
||||
### Headers and Footers
|
||||
|
||||
For Platypus documents, use page callbacks:
|
||||
|
||||
```python
|
||||
from reportlab.platypus import BaseDocTemplate, PageTemplate, Frame
|
||||
|
||||
def add_header_footer(canvas, doc):
|
||||
"""Called on each page"""
|
||||
canvas.saveState()
|
||||
|
||||
# Header
|
||||
canvas.setFont('Helvetica', 9)
|
||||
canvas.drawString(inch, height - 0.5*inch, "Document Title")
|
||||
|
||||
# Footer
|
||||
canvas.drawRightString(width - inch, 0.5*inch, f"Page {doc.page}")
|
||||
|
||||
canvas.restoreState()
|
||||
|
||||
# Set up document
|
||||
doc = BaseDocTemplate("output.pdf")
|
||||
frame = Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height, id='normal')
|
||||
template = PageTemplate(id='normal', frames=[frame], onPage=add_header_footer)
|
||||
doc.addPageTemplates([template])
|
||||
|
||||
# Build with story
|
||||
doc.build(story)
|
||||
```
|
||||
|
||||
## Helper Scripts
|
||||
|
||||
This skill includes helper scripts for common tasks:
|
||||
|
||||
### Quick Document Generator
|
||||
|
||||
Use `scripts/quick_document.py` for rapid document creation:
|
||||
|
||||
```python
|
||||
from scripts.quick_document import create_simple_document, create_styled_table
|
||||
|
||||
# Simple document from content blocks
|
||||
content = [
|
||||
{'type': 'heading', 'content': 'Introduction'},
|
||||
{'type': 'paragraph', 'content': 'Your text here...'},
|
||||
{'type': 'bullet', 'content': 'Bullet point'},
|
||||
]
|
||||
|
||||
create_simple_document("output.pdf", "My Document", content_blocks=content)
|
||||
|
||||
# Styled tables with presets
|
||||
data = [['Header1', 'Header2'], ['Data1', 'Data2']]
|
||||
table = create_styled_table(data, style_name='striped') # 'default', 'striped', 'minimal', 'report'
|
||||
```
|
||||
|
||||
## Template Examples
|
||||
|
||||
Complete working examples in `assets/`:
|
||||
|
||||
### Invoice Template
|
||||
|
||||
`assets/invoice_template.py` - Professional invoice with:
|
||||
- Company and client information
|
||||
- Itemized table with calculations
|
||||
- Tax and totals
|
||||
- Terms and notes
|
||||
- Logo placement
|
||||
|
||||
```python
|
||||
from assets.invoice_template import create_invoice
|
||||
|
||||
create_invoice(
|
||||
filename="invoice.pdf",
|
||||
invoice_number="INV-2024-001",
|
||||
invoice_date="January 15, 2024",
|
||||
due_date="February 15, 2024",
|
||||
company_info={'name': 'Acme Corp', 'address': '...', 'phone': '...', 'email': '...'},
|
||||
client_info={'name': 'Client Name', ...},
|
||||
items=[
|
||||
{'description': 'Service', 'quantity': 1, 'unit_price': 500.00},
|
||||
...
|
||||
],
|
||||
tax_rate=0.08,
|
||||
notes="Thank you for your business!",
|
||||
)
|
||||
```
|
||||
|
||||
### Report Template
|
||||
|
||||
`assets/report_template.py` - Multi-page business report with:
|
||||
- Cover page
|
||||
- Table of contents
|
||||
- Multiple sections with subsections
|
||||
- Charts and tables
|
||||
- Headers and footers
|
||||
|
||||
```python
|
||||
from assets.report_template import create_report
|
||||
|
||||
report_data = {
|
||||
'title': 'Quarterly Report',
|
||||
'subtitle': 'Q4 2023',
|
||||
'author': 'Analytics Team',
|
||||
'sections': [
|
||||
{
|
||||
'title': 'Executive Summary',
|
||||
'content': 'Report content...',
|
||||
'table_data': {...},
|
||||
'chart_data': {...}
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
create_report("report.pdf", report_data)
|
||||
```
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
Comprehensive API references organized by feature:
|
||||
|
||||
- **`references/canvas_api.md`** - Low-level Canvas: drawing primitives, coordinates, transformations, state management, images, paths
|
||||
- **`references/platypus_guide.md`** - High-level Platypus: document templates, frames, flowables, page layouts, TOC
|
||||
- **`references/text_and_fonts.md`** - Text formatting: paragraph styles, inline markup, custom fonts, Asian languages, bullets, sequences
|
||||
- **`references/tables_reference.md`** - Tables: creation, styling, cell spanning, borders, alignment, colors, gradients
|
||||
- **`references/charts_reference.md`** - Charts: all chart types, data handling, axes, legends, colors, rendering
|
||||
- **`references/barcodes_reference.md`** - Barcodes: Code128, QR codes, EAN, UPC, postal codes, and 20+ formats
|
||||
- **`references/pdf_features.md`** - PDF features: links, bookmarks, forms, encryption, metadata, page transitions
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Coordinate System (Canvas)
|
||||
- Origin (0, 0) is **lower-left corner** (not top-left)
|
||||
- Y-axis points **upward**
|
||||
- Units are in **points** (72 points = 1 inch)
|
||||
- Always specify page size explicitly
|
||||
|
||||
```python
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.lib.units import inch
|
||||
|
||||
width, height = letter
|
||||
margin = inch
|
||||
|
||||
# Top of page
|
||||
y_top = height - margin
|
||||
|
||||
# Bottom of page
|
||||
y_bottom = margin
|
||||
```
|
||||
|
||||
### Choosing Page Size
|
||||
|
||||
```python
|
||||
from reportlab.lib.pagesizes import letter, A4, landscape
|
||||
|
||||
# US Letter (8.5" x 11")
|
||||
pagesize=letter
|
||||
|
||||
# ISO A4 (210mm x 297mm)
|
||||
pagesize=A4
|
||||
|
||||
# Landscape
|
||||
pagesize=landscape(letter)
|
||||
|
||||
# Custom
|
||||
pagesize=(6*inch, 9*inch)
|
||||
```
|
||||
|
||||
### Performance Tips
|
||||
|
||||
1. **Use `drawImage()` over `drawInlineImage()`** - caches images for reuse
|
||||
2. **Enable compression for large files:** `canvas.Canvas("file.pdf", pageCompression=1)`
|
||||
3. **Reuse styles** - create once, use throughout document
|
||||
4. **Use Forms/XObjects** for repeated graphics
|
||||
|
||||
### Common Patterns
|
||||
|
||||
**Centering text on Canvas:**
|
||||
```python
|
||||
text = "Centered Text"
|
||||
text_width = c.stringWidth(text, "Helvetica", 12)
|
||||
x = (width - text_width) / 2
|
||||
c.drawString(x, y, text)
|
||||
|
||||
# Or use built-in
|
||||
c.drawCentredString(width/2, y, text)
|
||||
```
|
||||
|
||||
**Page breaks in Platypus:**
|
||||
```python
|
||||
from reportlab.platypus import PageBreak
|
||||
|
||||
story.append(PageBreak())
|
||||
```
|
||||
|
||||
**Keep content together (no split):**
|
||||
```python
|
||||
from reportlab.platypus import KeepTogether
|
||||
|
||||
story.append(KeepTogether([
|
||||
heading,
|
||||
paragraph1,
|
||||
paragraph2,
|
||||
]))
|
||||
```
|
||||
|
||||
**Alternate row colors:**
|
||||
```python
|
||||
style = TableStyle([
|
||||
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightgrey]),
|
||||
])
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Text overlaps or disappears:**
|
||||
- Check Y-coordinates - remember origin is bottom-left
|
||||
- Ensure text fits within page bounds
|
||||
- Verify `leading` (line spacing) is greater than `fontSize`
|
||||
|
||||
**Table doesn't fit on page:**
|
||||
- Reduce column widths
|
||||
- Decrease font size
|
||||
- Use landscape orientation
|
||||
- Enable table splitting with `repeatRows`
|
||||
|
||||
**Barcode not scanning:**
|
||||
- Increase `barHeight` (try 0.5 inch minimum)
|
||||
- Set `quiet=1` for quiet zones
|
||||
- Test print quality (300+ DPI recommended)
|
||||
- Validate data format for barcode type
|
||||
|
||||
**Font not found:**
|
||||
- Register TrueType fonts with `pdfmetrics.registerFont()`
|
||||
- Use font family name exactly as registered
|
||||
- Check font file path is correct
|
||||
|
||||
**Images have white background:**
|
||||
- Use `mask` parameter to make white transparent
|
||||
- Provide RGB range to mask: `mask=[255,255,255,255,255,255]`
|
||||
- Or use PNG with alpha channel
|
||||
|
||||
## Example Workflows
|
||||
|
||||
### Creating an Invoice
|
||||
|
||||
1. Start with invoice template from `assets/invoice_template.py`
|
||||
2. Customize company info, logo path
|
||||
3. Add items with descriptions, quantities, prices
|
||||
4. Set tax rate if applicable
|
||||
5. Add notes and payment terms
|
||||
6. Generate PDF
|
||||
|
||||
### Creating a Report
|
||||
|
||||
1. Start with report template from `assets/report_template.py`
|
||||
2. Define sections with titles and content
|
||||
3. Add tables for data using `create_styled_table()`
|
||||
4. Add charts using graphics framework
|
||||
5. Build with `doc.multiBuild(story)` for TOC
|
||||
|
||||
### Creating a Certificate
|
||||
|
||||
1. Use Canvas API for precise positioning
|
||||
2. Load custom fonts for elegant typography
|
||||
3. Add border graphics or image background
|
||||
4. Position text elements (name, date, achievement)
|
||||
5. Optional: Add QR code for verification
|
||||
|
||||
### Creating Labels with Barcodes
|
||||
|
||||
1. Use Canvas with custom page size (label dimensions)
|
||||
2. Calculate grid positions for multiple labels per page
|
||||
3. Draw label content (text, images)
|
||||
4. Add barcode at specific position
|
||||
5. Use `showPage()` between labels or grids
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
uv pip install reportlab
|
||||
|
||||
# For image support
|
||||
uv pip install pillow
|
||||
|
||||
# For charts
|
||||
uv pip install reportlab[renderPM]
|
||||
|
||||
# For barcode support (included in reportlab)
|
||||
# QR codes require: uv pip install qrcode
|
||||
```
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
This skill should be used when:
|
||||
- Generating PDF documents programmatically
|
||||
- Creating invoices, receipts, or billing documents
|
||||
- Building reports with tables and charts
|
||||
- Generating certificates, badges, or credentials
|
||||
- Creating shipping labels or product labels with barcodes
|
||||
- Designing forms or fillable PDFs
|
||||
- Producing multi-page documents with consistent formatting
|
||||
- Converting data to PDF format for archival or distribution
|
||||
- Creating custom layouts that require precise positioning
|
||||
|
||||
This skill provides comprehensive guidance for all ReportLab capabilities, from simple documents to complex multi-page reports with charts, tables, and interactive elements.
|
||||
256
skills/reportlab/assets/invoice_template.py
Normal file
256
skills/reportlab/assets/invoice_template.py
Normal file
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Invoice Template - Complete example of a professional invoice
|
||||
|
||||
This template demonstrates:
|
||||
- Company header with logo placement
|
||||
- Client information
|
||||
- Invoice details table
|
||||
- Calculations (subtotal, tax, total)
|
||||
- Professional styling
|
||||
- Terms and conditions footer
|
||||
"""
|
||||
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.lib import colors
|
||||
from reportlab.platypus import (
|
||||
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image
|
||||
)
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def create_invoice(
|
||||
filename,
|
||||
invoice_number,
|
||||
invoice_date,
|
||||
due_date,
|
||||
company_info,
|
||||
client_info,
|
||||
items,
|
||||
tax_rate=0.0,
|
||||
notes="",
|
||||
terms="Payment due within 30 days.",
|
||||
logo_path=None
|
||||
):
|
||||
"""
|
||||
Create a professional invoice PDF.
|
||||
|
||||
Args:
|
||||
filename: Output PDF filename
|
||||
invoice_number: Invoice number (e.g., "INV-2024-001")
|
||||
invoice_date: Date of invoice (datetime or string)
|
||||
due_date: Payment due date (datetime or string)
|
||||
company_info: Dict with company details
|
||||
{'name': 'Company Name', 'address': 'Address', 'phone': 'Phone', 'email': 'Email'}
|
||||
client_info: Dict with client details (same structure as company_info)
|
||||
items: List of dicts with item details
|
||||
[{'description': 'Item', 'quantity': 1, 'unit_price': 100.00}, ...]
|
||||
tax_rate: Tax rate as decimal (e.g., 0.08 for 8%)
|
||||
notes: Additional notes to client
|
||||
terms: Payment terms
|
||||
logo_path: Path to company logo image (optional)
|
||||
"""
|
||||
# Create document
|
||||
doc = SimpleDocTemplate(filename, pagesize=letter,
|
||||
rightMargin=0.5*inch, leftMargin=0.5*inch,
|
||||
topMargin=0.5*inch, bottomMargin=0.5*inch)
|
||||
|
||||
# Container for elements
|
||||
story = []
|
||||
styles = getSampleStyleSheet()
|
||||
|
||||
# Create custom styles
|
||||
title_style = ParagraphStyle(
|
||||
'InvoiceTitle',
|
||||
parent=styles['Heading1'],
|
||||
fontSize=24,
|
||||
textColor=colors.HexColor('#2C3E50'),
|
||||
spaceAfter=12,
|
||||
)
|
||||
|
||||
header_style = ParagraphStyle(
|
||||
'Header',
|
||||
parent=styles['Normal'],
|
||||
fontSize=10,
|
||||
textColor=colors.HexColor('#34495E'),
|
||||
)
|
||||
|
||||
# --- HEADER SECTION ---
|
||||
header_data = []
|
||||
|
||||
# Company info (left side)
|
||||
company_text = f"""
|
||||
<b><font size="14">{company_info['name']}</font></b><br/>
|
||||
{company_info.get('address', '')}<br/>
|
||||
Phone: {company_info.get('phone', '')}<br/>
|
||||
Email: {company_info.get('email', '')}
|
||||
"""
|
||||
|
||||
# Invoice title and number (right side)
|
||||
invoice_text = f"""
|
||||
<b><font size="16" color="#2C3E50">INVOICE</font></b><br/>
|
||||
<font size="10">Invoice #: {invoice_number}</font><br/>
|
||||
<font size="10">Date: {invoice_date}</font><br/>
|
||||
<font size="10">Due Date: {due_date}</font>
|
||||
"""
|
||||
|
||||
if logo_path:
|
||||
logo = Image(logo_path, width=1.5*inch, height=1*inch)
|
||||
header_data = [[logo, Paragraph(company_text, header_style), Paragraph(invoice_text, header_style)]]
|
||||
header_table = Table(header_data, colWidths=[1.5*inch, 3*inch, 2.5*inch])
|
||||
else:
|
||||
header_data = [[Paragraph(company_text, header_style), Paragraph(invoice_text, header_style)]]
|
||||
header_table = Table(header_data, colWidths=[4.5*inch, 2.5*inch])
|
||||
|
||||
header_table.setStyle(TableStyle([
|
||||
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
||||
('ALIGN', (-1, 0), (-1, -1), 'RIGHT'),
|
||||
]))
|
||||
|
||||
story.append(header_table)
|
||||
story.append(Spacer(1, 0.3*inch))
|
||||
|
||||
# --- CLIENT INFORMATION ---
|
||||
client_label = Paragraph("<b>Bill To:</b>", header_style)
|
||||
client_text = f"""
|
||||
<b>{client_info['name']}</b><br/>
|
||||
{client_info.get('address', '')}<br/>
|
||||
Phone: {client_info.get('phone', '')}<br/>
|
||||
Email: {client_info.get('email', '')}
|
||||
"""
|
||||
client_para = Paragraph(client_text, header_style)
|
||||
|
||||
client_table = Table([[client_label, client_para]], colWidths=[1*inch, 6*inch])
|
||||
story.append(client_table)
|
||||
story.append(Spacer(1, 0.3*inch))
|
||||
|
||||
# --- ITEMS TABLE ---
|
||||
# Table header
|
||||
items_data = [['Description', 'Quantity', 'Unit Price', 'Amount']]
|
||||
|
||||
# Calculate items
|
||||
subtotal = 0
|
||||
for item in items:
|
||||
desc = item['description']
|
||||
qty = item['quantity']
|
||||
price = item['unit_price']
|
||||
amount = qty * price
|
||||
subtotal += amount
|
||||
|
||||
items_data.append([
|
||||
desc,
|
||||
str(qty),
|
||||
f"${price:,.2f}",
|
||||
f"${amount:,.2f}"
|
||||
])
|
||||
|
||||
# Create items table
|
||||
items_table = Table(items_data, colWidths=[3.5*inch, 1*inch, 1.5*inch, 1*inch])
|
||||
|
||||
items_table.setStyle(TableStyle([
|
||||
# Header row
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#34495E')),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
||||
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, 0), 11),
|
||||
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
||||
|
||||
# Data rows
|
||||
('BACKGROUND', (0, 1), (-1, -1), colors.white),
|
||||
('ALIGN', (1, 1), (-1, -1), 'RIGHT'),
|
||||
('ALIGN', (0, 1), (0, -1), 'LEFT'),
|
||||
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
|
||||
('FONTSIZE', (0, 1), (-1, -1), 10),
|
||||
('TOPPADDING', (0, 1), (-1, -1), 6),
|
||||
('BOTTOMPADDING', (0, 1), (-1, -1), 6),
|
||||
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
|
||||
]))
|
||||
|
||||
story.append(items_table)
|
||||
story.append(Spacer(1, 0.2*inch))
|
||||
|
||||
# --- TOTALS SECTION ---
|
||||
tax_amount = subtotal * tax_rate
|
||||
total = subtotal + tax_amount
|
||||
|
||||
totals_data = [
|
||||
['Subtotal:', f"${subtotal:,.2f}"],
|
||||
]
|
||||
|
||||
if tax_rate > 0:
|
||||
totals_data.append([f'Tax ({tax_rate*100:.1f}%):', f"${tax_amount:,.2f}"])
|
||||
|
||||
totals_data.append(['<b>Total:</b>', f"<b>${total:,.2f}</b>"])
|
||||
|
||||
totals_table = Table(totals_data, colWidths=[5*inch, 2*inch])
|
||||
totals_table.setStyle(TableStyle([
|
||||
('ALIGN', (0, 0), (-1, -1), 'RIGHT'),
|
||||
('FONTNAME', (0, 0), (-1, -2), 'Helvetica'),
|
||||
('FONTNAME', (0, -1), (-1, -1), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, -1), 11),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 6),
|
||||
('LINEABOVE', (1, -1), (1, -1), 2, colors.HexColor('#34495E')),
|
||||
]))
|
||||
|
||||
story.append(totals_table)
|
||||
story.append(Spacer(1, 0.4*inch))
|
||||
|
||||
# --- NOTES ---
|
||||
if notes:
|
||||
notes_style = ParagraphStyle('Notes', parent=styles['Normal'], fontSize=9)
|
||||
story.append(Paragraph(f"<b>Notes:</b><br/>{notes}", notes_style))
|
||||
story.append(Spacer(1, 0.2*inch))
|
||||
|
||||
# --- TERMS ---
|
||||
terms_style = ParagraphStyle('Terms', parent=styles['Normal'],
|
||||
fontSize=9, textColor=colors.grey)
|
||||
story.append(Paragraph(f"<b>Payment Terms:</b><br/>{terms}", terms_style))
|
||||
|
||||
# Build PDF
|
||||
doc.build(story)
|
||||
return filename
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == "__main__":
|
||||
# Sample data
|
||||
company = {
|
||||
'name': 'Acme Corporation',
|
||||
'address': '123 Business St, Suite 100\nNew York, NY 10001',
|
||||
'phone': '(555) 123-4567',
|
||||
'email': 'info@acme.com'
|
||||
}
|
||||
|
||||
client = {
|
||||
'name': 'John Doe',
|
||||
'address': '456 Client Ave\nLos Angeles, CA 90001',
|
||||
'phone': '(555) 987-6543',
|
||||
'email': 'john@example.com'
|
||||
}
|
||||
|
||||
items = [
|
||||
{'description': 'Web Design Services', 'quantity': 1, 'unit_price': 2500.00},
|
||||
{'description': 'Content Writing (10 pages)', 'quantity': 10, 'unit_price': 50.00},
|
||||
{'description': 'SEO Optimization', 'quantity': 1, 'unit_price': 750.00},
|
||||
{'description': 'Hosting Setup', 'quantity': 1, 'unit_price': 200.00},
|
||||
]
|
||||
|
||||
create_invoice(
|
||||
filename="sample_invoice.pdf",
|
||||
invoice_number="INV-2024-001",
|
||||
invoice_date="January 15, 2024",
|
||||
due_date="February 15, 2024",
|
||||
company_info=company,
|
||||
client_info=client,
|
||||
items=items,
|
||||
tax_rate=0.08,
|
||||
notes="Thank you for your business! We appreciate your prompt payment.",
|
||||
terms="Payment due within 30 days. Late payments subject to 1.5% monthly fee.",
|
||||
logo_path=None # Set to your logo path if available
|
||||
)
|
||||
|
||||
print("Invoice created: sample_invoice.pdf")
|
||||
343
skills/reportlab/assets/report_template.py
Normal file
343
skills/reportlab/assets/report_template.py
Normal file
@@ -0,0 +1,343 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Report Template - Complete example of a professional multi-page report
|
||||
|
||||
This template demonstrates:
|
||||
- Cover page
|
||||
- Table of contents
|
||||
- Multiple sections with headers
|
||||
- Charts and graphs integration
|
||||
- Tables with data
|
||||
- Headers and footers
|
||||
- Professional styling
|
||||
"""
|
||||
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.lib import colors
|
||||
from reportlab.platypus import (
|
||||
BaseDocTemplate, PageTemplate, Frame, Paragraph, Spacer,
|
||||
Table, TableStyle, PageBreak, KeepTogether, TableOfContents
|
||||
)
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
from reportlab.graphics.charts.barcharts import VerticalBarChart
|
||||
from reportlab.graphics.charts.linecharts import HorizontalLineChart
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def header_footer(canvas, doc):
|
||||
"""Draw header and footer on each page (except cover)"""
|
||||
canvas.saveState()
|
||||
|
||||
# Skip header/footer on cover page (page 1)
|
||||
if doc.page > 1:
|
||||
# Header
|
||||
canvas.setFont('Helvetica', 9)
|
||||
canvas.setFillColor(colors.grey)
|
||||
canvas.drawString(inch, letter[1] - 0.5*inch, "Quarterly Business Report")
|
||||
canvas.line(inch, letter[1] - 0.55*inch, letter[0] - inch, letter[1] - 0.55*inch)
|
||||
|
||||
# Footer
|
||||
canvas.drawString(inch, 0.5*inch, f"Generated: {datetime.now().strftime('%B %d, %Y')}")
|
||||
canvas.drawRightString(letter[0] - inch, 0.5*inch, f"Page {doc.page - 1}")
|
||||
|
||||
canvas.restoreState()
|
||||
|
||||
|
||||
def create_report(filename, report_data):
|
||||
"""
|
||||
Create a comprehensive business report.
|
||||
|
||||
Args:
|
||||
filename: Output PDF filename
|
||||
report_data: Dict containing report information
|
||||
{
|
||||
'title': 'Report Title',
|
||||
'subtitle': 'Report Subtitle',
|
||||
'author': 'Author Name',
|
||||
'date': 'Date',
|
||||
'sections': [
|
||||
{
|
||||
'title': 'Section Title',
|
||||
'content': 'Section content...',
|
||||
'subsections': [...],
|
||||
'table': {...},
|
||||
'chart': {...}
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
"""
|
||||
# Create document with custom page template
|
||||
doc = BaseDocTemplate(filename, pagesize=letter,
|
||||
rightMargin=72, leftMargin=72,
|
||||
topMargin=inch, bottomMargin=inch)
|
||||
|
||||
# Define frame for content
|
||||
frame = Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height - 0.5*inch, id='normal')
|
||||
|
||||
# Create page template with header/footer
|
||||
template = PageTemplate(id='normal', frames=[frame], onPage=header_footer)
|
||||
doc.addPageTemplates([template])
|
||||
|
||||
# Get styles
|
||||
styles = getSampleStyleSheet()
|
||||
|
||||
# Custom styles
|
||||
title_style = ParagraphStyle(
|
||||
'ReportTitle',
|
||||
parent=styles['Title'],
|
||||
fontSize=28,
|
||||
textColor=colors.HexColor('#2C3E50'),
|
||||
spaceAfter=20,
|
||||
alignment=TA_CENTER,
|
||||
)
|
||||
|
||||
subtitle_style = ParagraphStyle(
|
||||
'ReportSubtitle',
|
||||
parent=styles['Normal'],
|
||||
fontSize=14,
|
||||
textColor=colors.grey,
|
||||
alignment=TA_CENTER,
|
||||
spaceAfter=30,
|
||||
)
|
||||
|
||||
heading1_style = ParagraphStyle(
|
||||
'CustomHeading1',
|
||||
parent=styles['Heading1'],
|
||||
fontSize=18,
|
||||
textColor=colors.HexColor('#2C3E50'),
|
||||
spaceAfter=12,
|
||||
spaceBefore=12,
|
||||
)
|
||||
|
||||
heading2_style = ParagraphStyle(
|
||||
'CustomHeading2',
|
||||
parent=styles['Heading2'],
|
||||
fontSize=14,
|
||||
textColor=colors.HexColor('#34495E'),
|
||||
spaceAfter=10,
|
||||
spaceBefore=10,
|
||||
)
|
||||
|
||||
body_style = ParagraphStyle(
|
||||
'ReportBody',
|
||||
parent=styles['BodyText'],
|
||||
fontSize=11,
|
||||
alignment=TA_JUSTIFY,
|
||||
spaceAfter=12,
|
||||
leading=14,
|
||||
)
|
||||
|
||||
# Build story
|
||||
story = []
|
||||
|
||||
# --- COVER PAGE ---
|
||||
story.append(Spacer(1, 2*inch))
|
||||
story.append(Paragraph(report_data['title'], title_style))
|
||||
story.append(Paragraph(report_data.get('subtitle', ''), subtitle_style))
|
||||
story.append(Spacer(1, inch))
|
||||
|
||||
# Cover info table
|
||||
cover_info = [
|
||||
['Prepared by:', report_data.get('author', '')],
|
||||
['Date:', report_data.get('date', datetime.now().strftime('%B %d, %Y'))],
|
||||
['Period:', report_data.get('period', 'Q4 2023')],
|
||||
]
|
||||
|
||||
cover_table = Table(cover_info, colWidths=[2*inch, 4*inch])
|
||||
cover_table.setStyle(TableStyle([
|
||||
('ALIGN', (0, 0), (0, -1), 'RIGHT'),
|
||||
('ALIGN', (1, 0), (1, -1), 'LEFT'),
|
||||
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, -1), 11),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 6),
|
||||
]))
|
||||
|
||||
story.append(cover_table)
|
||||
story.append(PageBreak())
|
||||
|
||||
# --- TABLE OF CONTENTS ---
|
||||
toc = TableOfContents()
|
||||
toc.levelStyles = [
|
||||
ParagraphStyle(name='TOCHeading1', fontSize=14, leftIndent=20, spaceBefore=10, spaceAfter=5),
|
||||
ParagraphStyle(name='TOCHeading2', fontSize=12, leftIndent=40, spaceBefore=3, spaceAfter=3),
|
||||
]
|
||||
|
||||
story.append(Paragraph("Table of Contents", heading1_style))
|
||||
story.append(toc)
|
||||
story.append(PageBreak())
|
||||
|
||||
# --- SECTIONS ---
|
||||
for section in report_data.get('sections', []):
|
||||
# Section heading
|
||||
section_title = section['title']
|
||||
story.append(Paragraph(f'<a name="{section_title}"/>{section_title}', heading1_style))
|
||||
|
||||
# Add to TOC
|
||||
toc.addEntry(0, section_title, doc.page)
|
||||
|
||||
# Section content
|
||||
if 'content' in section:
|
||||
for para in section['content'].split('\n\n'):
|
||||
if para.strip():
|
||||
story.append(Paragraph(para.strip(), body_style))
|
||||
|
||||
story.append(Spacer(1, 0.2*inch))
|
||||
|
||||
# Subsections
|
||||
for subsection in section.get('subsections', []):
|
||||
story.append(Paragraph(subsection['title'], heading2_style))
|
||||
|
||||
if 'content' in subsection:
|
||||
story.append(Paragraph(subsection['content'], body_style))
|
||||
|
||||
story.append(Spacer(1, 0.1*inch))
|
||||
|
||||
# Add table if provided
|
||||
if 'table_data' in section:
|
||||
table = create_section_table(section['table_data'])
|
||||
story.append(table)
|
||||
story.append(Spacer(1, 0.2*inch))
|
||||
|
||||
# Add chart if provided
|
||||
if 'chart_data' in section:
|
||||
chart = create_section_chart(section['chart_data'])
|
||||
story.append(chart)
|
||||
story.append(Spacer(1, 0.2*inch))
|
||||
|
||||
story.append(Spacer(1, 0.3*inch))
|
||||
|
||||
# Build PDF (twice for TOC to populate)
|
||||
doc.multiBuild(story)
|
||||
return filename
|
||||
|
||||
|
||||
def create_section_table(table_data):
|
||||
"""Create a styled table for report sections"""
|
||||
data = table_data['data']
|
||||
table = Table(data, colWidths=table_data.get('colWidths'))
|
||||
|
||||
table.setStyle(TableStyle([
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#34495E')),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, 0), 11),
|
||||
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
||||
('BACKGROUND', (0, 1), (-1, -1), colors.white),
|
||||
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightgrey]),
|
||||
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
|
||||
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
|
||||
('FONTSIZE', (0, 1), (-1, -1), 10),
|
||||
]))
|
||||
|
||||
return table
|
||||
|
||||
|
||||
def create_section_chart(chart_data):
|
||||
"""Create a chart for report sections"""
|
||||
chart_type = chart_data.get('type', 'bar')
|
||||
drawing = Drawing(400, 200)
|
||||
|
||||
if chart_type == 'bar':
|
||||
chart = VerticalBarChart()
|
||||
chart.x = 50
|
||||
chart.y = 30
|
||||
chart.width = 300
|
||||
chart.height = 150
|
||||
chart.data = chart_data['data']
|
||||
chart.categoryAxis.categoryNames = chart_data.get('categories', [])
|
||||
chart.valueAxis.valueMin = 0
|
||||
|
||||
# Style bars
|
||||
for i in range(len(chart_data['data'])):
|
||||
chart.bars[i].fillColor = colors.HexColor(['#3498db', '#e74c3c', '#2ecc71'][i % 3])
|
||||
|
||||
elif chart_type == 'line':
|
||||
chart = HorizontalLineChart()
|
||||
chart.x = 50
|
||||
chart.y = 30
|
||||
chart.width = 300
|
||||
chart.height = 150
|
||||
chart.data = chart_data['data']
|
||||
chart.categoryAxis.categoryNames = chart_data.get('categories', [])
|
||||
|
||||
# Style lines
|
||||
for i in range(len(chart_data['data'])):
|
||||
chart.lines[i].strokeColor = colors.HexColor(['#3498db', '#e74c3c', '#2ecc71'][i % 3])
|
||||
chart.lines[i].strokeWidth = 2
|
||||
|
||||
drawing.add(chart)
|
||||
return drawing
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == "__main__":
|
||||
report = {
|
||||
'title': 'Quarterly Business Report',
|
||||
'subtitle': 'Q4 2023 Performance Analysis',
|
||||
'author': 'Analytics Team',
|
||||
'date': 'January 15, 2024',
|
||||
'period': 'October - December 2023',
|
||||
'sections': [
|
||||
{
|
||||
'title': 'Executive Summary',
|
||||
'content': """
|
||||
This report provides a comprehensive analysis of our Q4 2023 performance.
|
||||
Overall, the quarter showed strong growth across all key metrics, with
|
||||
revenue increasing by 25% year-over-year and customer satisfaction
|
||||
scores reaching an all-time high of 4.8/5.0.
|
||||
|
||||
Key highlights include the successful launch of three new products,
|
||||
expansion into two new markets, and the completion of our digital
|
||||
transformation initiative.
|
||||
""",
|
||||
'subsections': [
|
||||
{
|
||||
'title': 'Key Achievements',
|
||||
'content': 'Successfully launched Product X with 10,000 units sold in first month.'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'title': 'Financial Performance',
|
||||
'content': """
|
||||
The financial results for Q4 exceeded expectations across all categories.
|
||||
Revenue growth was driven primarily by strong product sales and increased
|
||||
market share in key regions.
|
||||
""",
|
||||
'table_data': {
|
||||
'data': [
|
||||
['Metric', 'Q3 2023', 'Q4 2023', 'Change'],
|
||||
['Revenue', '$2.5M', '$3.1M', '+24%'],
|
||||
['Profit', '$500K', '$680K', '+36%'],
|
||||
['Expenses', '$2.0M', '$2.4M', '+20%'],
|
||||
],
|
||||
'colWidths': [2*inch, 1.5*inch, 1.5*inch, 1*inch]
|
||||
},
|
||||
'chart_data': {
|
||||
'type': 'bar',
|
||||
'data': [[2.5, 3.1], [0.5, 0.68], [2.0, 2.4]],
|
||||
'categories': ['Q3', 'Q4']
|
||||
}
|
||||
},
|
||||
{
|
||||
'title': 'Market Analysis',
|
||||
'content': """
|
||||
Market conditions remained favorable throughout the quarter, with
|
||||
strong consumer confidence and increasing demand for our products.
|
||||
""",
|
||||
'chart_data': {
|
||||
'type': 'line',
|
||||
'data': [[100, 120, 115, 140, 135, 150]],
|
||||
'categories': ['Oct', 'Nov', 'Dec', 'Oct', 'Nov', 'Dec']
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
create_report("sample_report.pdf", report)
|
||||
print("Report created: sample_report.pdf")
|
||||
504
skills/reportlab/references/barcodes_reference.md
Normal file
504
skills/reportlab/references/barcodes_reference.md
Normal file
@@ -0,0 +1,504 @@
|
||||
# Barcodes Reference
|
||||
|
||||
Comprehensive guide to creating barcodes and QR codes in ReportLab.
|
||||
|
||||
## Available Barcode Types
|
||||
|
||||
ReportLab supports a wide range of 1D and 2D barcode formats.
|
||||
|
||||
### 1D Barcodes (Linear)
|
||||
|
||||
- **Code128** - Compact, encodes full ASCII
|
||||
- **Code39** (Standard39) - Alphanumeric, widely supported
|
||||
- **Code93** (Standard93) - Compressed Code39
|
||||
- **EAN-13** - European Article Number (retail)
|
||||
- **EAN-8** - Short form of EAN
|
||||
- **EAN-5** - 5-digit add-on (pricing)
|
||||
- **UPC-A** - Universal Product Code (North America)
|
||||
- **ISBN** - International Standard Book Number
|
||||
- **Code11** - Telecommunications
|
||||
- **Codabar** - Blood banks, FedEx, libraries
|
||||
- **I2of5** (Interleaved 2 of 5) - Warehouse/distribution
|
||||
- **MSI** - Inventory control
|
||||
- **POSTNET** - US Postal Service
|
||||
- **USPS_4State** - US Postal Service
|
||||
- **FIM** (A, B, C, D) - Facing Identification Mark (mail sorting)
|
||||
|
||||
### 2D Barcodes
|
||||
|
||||
- **QR** - QR Code (widely used for URLs, contact info)
|
||||
- **ECC200DataMatrix** - Data Matrix format
|
||||
|
||||
## Using Barcodes with Canvas
|
||||
|
||||
### Code128 (Recommended for General Use)
|
||||
|
||||
Code128 is versatile and compact - encodes full ASCII character set with mandatory checksum.
|
||||
|
||||
```python
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.graphics.barcode import code128
|
||||
from reportlab.lib.units import inch
|
||||
|
||||
c = canvas.Canvas("barcode.pdf")
|
||||
|
||||
# Create barcode
|
||||
barcode = code128.Code128("HELLO123")
|
||||
|
||||
# Draw on canvas
|
||||
barcode.drawOn(c, 1*inch, 5*inch)
|
||||
|
||||
c.save()
|
||||
```
|
||||
|
||||
### Code128 Options
|
||||
|
||||
```python
|
||||
barcode = code128.Code128(
|
||||
value="ABC123", # Required: data to encode
|
||||
barWidth=0.01*inch, # Width of narrowest bar
|
||||
barHeight=0.5*inch, # Height of bars
|
||||
quiet=1, # Add quiet zones (margins)
|
||||
lquiet=None, # Left quiet zone width
|
||||
rquiet=None, # Right quiet zone width
|
||||
stop=1, # Show stop symbol
|
||||
)
|
||||
|
||||
# Draw with specific size
|
||||
barcode.drawOn(canvas, x, y)
|
||||
|
||||
# Get dimensions
|
||||
width = barcode.width
|
||||
height = barcode.height
|
||||
```
|
||||
|
||||
### Code39 (Standard39)
|
||||
|
||||
Supports: 0-9, A-Z (uppercase), space, and special chars (-.$/+%*).
|
||||
|
||||
```python
|
||||
from reportlab.graphics.barcode import code39
|
||||
|
||||
barcode = code39.Standard39(
|
||||
value="HELLO",
|
||||
barWidth=0.01*inch,
|
||||
barHeight=0.5*inch,
|
||||
quiet=1,
|
||||
checksum=0, # 0 or 1
|
||||
)
|
||||
|
||||
barcode.drawOn(canvas, x, y)
|
||||
```
|
||||
|
||||
### Extended Code39
|
||||
|
||||
Encodes full ASCII (pairs of Code39 characters).
|
||||
|
||||
```python
|
||||
from reportlab.graphics.barcode import code39
|
||||
|
||||
barcode = code39.Extended39(
|
||||
value="Hello World!", # Can include lowercase and symbols
|
||||
barWidth=0.01*inch,
|
||||
barHeight=0.5*inch,
|
||||
)
|
||||
|
||||
barcode.drawOn(canvas, x, y)
|
||||
```
|
||||
|
||||
### Code93
|
||||
|
||||
```python
|
||||
from reportlab.graphics.barcode import code93
|
||||
|
||||
# Standard93 - uppercase, digits, some symbols
|
||||
barcode = code93.Standard93(
|
||||
value="HELLO93",
|
||||
barWidth=0.01*inch,
|
||||
barHeight=0.5*inch,
|
||||
)
|
||||
|
||||
# Extended93 - full ASCII
|
||||
barcode = code93.Extended93(
|
||||
value="Hello 93!",
|
||||
barWidth=0.01*inch,
|
||||
barHeight=0.5*inch,
|
||||
)
|
||||
|
||||
barcode.drawOn(canvas, x, y)
|
||||
```
|
||||
|
||||
### EAN-13 (European Article Number)
|
||||
|
||||
13-digit barcode for retail products.
|
||||
|
||||
```python
|
||||
from reportlab.graphics.barcode import eanbc
|
||||
|
||||
# Must be exactly 12 digits (13th is calculated checksum)
|
||||
barcode = eanbc.Ean13BarcodeWidget(
|
||||
value="123456789012"
|
||||
)
|
||||
|
||||
# Draw
|
||||
from reportlab.graphics import renderPDF
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
|
||||
d = Drawing()
|
||||
d.add(barcode)
|
||||
renderPDF.draw(d, canvas, x, y)
|
||||
```
|
||||
|
||||
### EAN-8
|
||||
|
||||
Short form, 8 digits.
|
||||
|
||||
```python
|
||||
from reportlab.graphics.barcode import eanbc
|
||||
|
||||
# Must be exactly 7 digits (8th is calculated)
|
||||
barcode = eanbc.Ean8BarcodeWidget(
|
||||
value="1234567"
|
||||
)
|
||||
```
|
||||
|
||||
### UPC-A
|
||||
|
||||
12-digit barcode used in North America.
|
||||
|
||||
```python
|
||||
from reportlab.graphics.barcode import usps
|
||||
|
||||
# 11 digits (12th is checksum)
|
||||
barcode = usps.UPCA(
|
||||
value="01234567890"
|
||||
)
|
||||
|
||||
barcode.drawOn(canvas, x, y)
|
||||
```
|
||||
|
||||
### ISBN (Books)
|
||||
|
||||
```python
|
||||
from reportlab.graphics.barcode.widgets import ISBNBarcodeWidget
|
||||
|
||||
# 10 or 13 digit ISBN
|
||||
barcode = ISBNBarcodeWidget(
|
||||
value="978-0-123456-78-9"
|
||||
)
|
||||
|
||||
# With pricing (EAN-5 add-on)
|
||||
barcode = ISBNBarcodeWidget(
|
||||
value="978-0-123456-78-9",
|
||||
price=True,
|
||||
)
|
||||
```
|
||||
|
||||
### QR Codes
|
||||
|
||||
Most versatile 2D barcode - can encode URLs, text, contact info, etc.
|
||||
|
||||
```python
|
||||
from reportlab.graphics.barcode.qr import QrCodeWidget
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
from reportlab.graphics import renderPDF
|
||||
|
||||
# Create QR code
|
||||
qr = QrCodeWidget("https://example.com")
|
||||
|
||||
# Size in pixels (QR codes are square)
|
||||
qr.barWidth = 100 # Width in points
|
||||
qr.barHeight = 100 # Height in points
|
||||
|
||||
# Error correction level
|
||||
# L = 7% recovery, M = 15%, Q = 25%, H = 30%
|
||||
qr.qrVersion = 1 # Auto-size (1-40, or None for auto)
|
||||
qr.errorLevel = 'M' # L, M, Q, H
|
||||
|
||||
# Draw
|
||||
d = Drawing()
|
||||
d.add(qr)
|
||||
renderPDF.draw(d, canvas, x, y)
|
||||
```
|
||||
|
||||
### QR Code - More Options
|
||||
|
||||
```python
|
||||
# URL QR Code
|
||||
qr = QrCodeWidget("https://example.com")
|
||||
|
||||
# Contact information (vCard)
|
||||
vcard_data = """BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
FN:John Doe
|
||||
TEL:+1-555-1234
|
||||
EMAIL:john@example.com
|
||||
END:VCARD"""
|
||||
qr = QrCodeWidget(vcard_data)
|
||||
|
||||
# WiFi credentials
|
||||
wifi_data = "WIFI:T:WPA;S:NetworkName;P:Password;;"
|
||||
qr = QrCodeWidget(wifi_data)
|
||||
|
||||
# Plain text
|
||||
qr = QrCodeWidget("Any text here")
|
||||
```
|
||||
|
||||
### Data Matrix (ECC200)
|
||||
|
||||
Compact 2D barcode for small items.
|
||||
|
||||
```python
|
||||
from reportlab.graphics.barcode.datamatrix import DataMatrixWidget
|
||||
|
||||
barcode = DataMatrixWidget(
|
||||
value="DATA123"
|
||||
)
|
||||
|
||||
d = Drawing()
|
||||
d.add(barcode)
|
||||
renderPDF.draw(d, canvas, x, y)
|
||||
```
|
||||
|
||||
### Postal Barcodes
|
||||
|
||||
```python
|
||||
from reportlab.graphics.barcode import usps
|
||||
|
||||
# POSTNET (older format)
|
||||
barcode = usps.POSTNET(
|
||||
value="55555-1234", # ZIP or ZIP+4
|
||||
)
|
||||
|
||||
# USPS 4-State (newer)
|
||||
barcode = usps.USPS_4State(
|
||||
value="12345678901234567890", # 20-digit routing code
|
||||
routing="12345678901"
|
||||
)
|
||||
|
||||
barcode.drawOn(canvas, x, y)
|
||||
```
|
||||
|
||||
### FIM (Facing Identification Mark)
|
||||
|
||||
Used for mail sorting.
|
||||
|
||||
```python
|
||||
from reportlab.graphics.barcode import usps
|
||||
|
||||
# FIM-A, FIM-B, FIM-C, or FIM-D
|
||||
barcode = usps.FIM(
|
||||
value="A" # A, B, C, or D
|
||||
)
|
||||
|
||||
barcode.drawOn(canvas, x, y)
|
||||
```
|
||||
|
||||
## Using Barcodes with Platypus
|
||||
|
||||
For flowing documents, wrap barcodes in Flowables.
|
||||
|
||||
### Simple Approach - Drawing Flowable
|
||||
|
||||
```python
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
from reportlab.graphics.barcode.qr import QrCodeWidget
|
||||
from reportlab.lib.units import inch
|
||||
|
||||
# Create drawing
|
||||
d = Drawing(2*inch, 2*inch)
|
||||
|
||||
# Create barcode
|
||||
qr = QrCodeWidget("https://example.com")
|
||||
qr.barWidth = 2*inch
|
||||
qr.barHeight = 2*inch
|
||||
qr.x = 0
|
||||
qr.y = 0
|
||||
|
||||
d.add(qr)
|
||||
|
||||
# Add to story
|
||||
story.append(d)
|
||||
```
|
||||
|
||||
### Custom Flowable Wrapper
|
||||
|
||||
```python
|
||||
from reportlab.platypus import Flowable
|
||||
from reportlab.graphics.barcode import code128
|
||||
from reportlab.lib.units import inch
|
||||
|
||||
class BarcodeFlowable(Flowable):
|
||||
def __init__(self, code, barcode_type='code128', width=2*inch, height=0.5*inch):
|
||||
Flowable.__init__(self)
|
||||
self.code = code
|
||||
self.barcode_type = barcode_type
|
||||
self.width_val = width
|
||||
self.height_val = height
|
||||
|
||||
# Create barcode
|
||||
if barcode_type == 'code128':
|
||||
self.barcode = code128.Code128(code, barWidth=width/100, barHeight=height)
|
||||
# Add other types as needed
|
||||
|
||||
def draw(self):
|
||||
self.barcode.drawOn(self.canv, 0, 0)
|
||||
|
||||
def wrap(self, availWidth, availHeight):
|
||||
return (self.barcode.width, self.barcode.height)
|
||||
|
||||
# Use in story
|
||||
story.append(BarcodeFlowable("PRODUCT123"))
|
||||
```
|
||||
|
||||
## Complete Examples
|
||||
|
||||
### Product Label with Barcode
|
||||
|
||||
```python
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.graphics.barcode import code128
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.lib.units import inch
|
||||
|
||||
def create_product_label(filename, product_code, product_name):
|
||||
c = canvas.Canvas(filename, pagesize=(4*inch, 2*inch))
|
||||
|
||||
# Product name
|
||||
c.setFont("Helvetica-Bold", 14)
|
||||
c.drawCentredString(2*inch, 1.5*inch, product_name)
|
||||
|
||||
# Barcode
|
||||
barcode = code128.Code128(product_code)
|
||||
barcode_width = barcode.width
|
||||
barcode_height = barcode.height
|
||||
|
||||
# Center barcode
|
||||
x = (4*inch - barcode_width) / 2
|
||||
y = 0.5*inch
|
||||
|
||||
barcode.drawOn(c, x, y)
|
||||
|
||||
# Code text
|
||||
c.setFont("Courier", 10)
|
||||
c.drawCentredString(2*inch, 0.3*inch, product_code)
|
||||
|
||||
c.save()
|
||||
|
||||
create_product_label("label.pdf", "ABC123456789", "Premium Widget")
|
||||
```
|
||||
|
||||
### QR Code Contact Card
|
||||
|
||||
```python
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.graphics.barcode.qr import QrCodeWidget
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
from reportlab.graphics import renderPDF
|
||||
from reportlab.lib.units import inch
|
||||
|
||||
def create_contact_card(filename, name, phone, email):
|
||||
c = canvas.Canvas(filename, pagesize=(3.5*inch, 2*inch))
|
||||
|
||||
# Contact info
|
||||
c.setFont("Helvetica-Bold", 12)
|
||||
c.drawString(0.5*inch, 1.5*inch, name)
|
||||
c.setFont("Helvetica", 10)
|
||||
c.drawString(0.5*inch, 1.3*inch, phone)
|
||||
c.drawString(0.5*inch, 1.1*inch, email)
|
||||
|
||||
# Create vCard data
|
||||
vcard = f"""BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
FN:{name}
|
||||
TEL:{phone}
|
||||
EMAIL:{email}
|
||||
END:VCARD"""
|
||||
|
||||
# QR code
|
||||
qr = QrCodeWidget(vcard)
|
||||
qr.barWidth = 1.5*inch
|
||||
qr.barHeight = 1.5*inch
|
||||
|
||||
d = Drawing()
|
||||
d.add(qr)
|
||||
|
||||
renderPDF.draw(d, c, 1.8*inch, 0.2*inch)
|
||||
|
||||
c.save()
|
||||
|
||||
create_contact_card("contact.pdf", "John Doe", "+1-555-1234", "john@example.com")
|
||||
```
|
||||
|
||||
### Shipping Label with Multiple Barcodes
|
||||
|
||||
```python
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.graphics.barcode import code128
|
||||
from reportlab.lib.units import inch
|
||||
|
||||
def create_shipping_label(filename, tracking_code, zip_code):
|
||||
c = canvas.Canvas(filename, pagesize=(6*inch, 4*inch))
|
||||
|
||||
# Title
|
||||
c.setFont("Helvetica-Bold", 16)
|
||||
c.drawString(0.5*inch, 3.5*inch, "SHIPPING LABEL")
|
||||
|
||||
# Tracking barcode
|
||||
c.setFont("Helvetica", 10)
|
||||
c.drawString(0.5*inch, 2.8*inch, "Tracking Number:")
|
||||
|
||||
tracking_barcode = code128.Code128(tracking_code, barHeight=0.5*inch)
|
||||
tracking_barcode.drawOn(c, 0.5*inch, 2*inch)
|
||||
|
||||
c.setFont("Courier", 9)
|
||||
c.drawString(0.5*inch, 1.8*inch, tracking_code)
|
||||
|
||||
# Additional info can be added
|
||||
|
||||
c.save()
|
||||
|
||||
create_shipping_label("shipping.pdf", "1Z999AA10123456784", "12345")
|
||||
```
|
||||
|
||||
## Barcode Selection Guide
|
||||
|
||||
**Choose Code128 when:**
|
||||
- General purpose encoding
|
||||
- Need to encode numbers and letters
|
||||
- Want compact size
|
||||
- Widely supported
|
||||
|
||||
**Choose Code39 when:**
|
||||
- Older systems require it
|
||||
- Don't need lowercase letters
|
||||
- Want maximum compatibility
|
||||
|
||||
**Choose QR Code when:**
|
||||
- Need to encode URLs
|
||||
- Want mobile device scanning
|
||||
- Need high data capacity
|
||||
- Want error correction
|
||||
|
||||
**Choose EAN/UPC when:**
|
||||
- Retail product identification
|
||||
- Need industry-standard format
|
||||
- Global distribution
|
||||
|
||||
**Choose Data Matrix when:**
|
||||
- Very limited space
|
||||
- Small items (PCB, electronics)
|
||||
- Need 2D compact format
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Test scanning** early with actual barcode scanners/readers
|
||||
2. **Add quiet zones** (white space) around barcodes - set `quiet=1`
|
||||
3. **Choose appropriate height** - taller barcodes are easier to scan
|
||||
4. **Include human-readable text** below barcode for manual entry
|
||||
5. **Use Code128** as default for general purpose - it's compact and versatile
|
||||
6. **For URLs, use QR codes** - much easier for mobile users
|
||||
7. **Check barcode standards** for your industry (retail uses EAN/UPC)
|
||||
8. **Test print quality** - low DPI can make barcodes unscannable
|
||||
9. **Validate data** before encoding - wrong check digits cause issues
|
||||
10. **Consider error correction** for QR codes - use 'M' or 'H' for important data
|
||||
241
skills/reportlab/references/canvas_api.md
Normal file
241
skills/reportlab/references/canvas_api.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# Canvas API Reference
|
||||
|
||||
The Canvas API provides low-level, precise control over PDF generation using coordinate-based drawing.
|
||||
|
||||
## Coordinate System
|
||||
|
||||
- Origin (0, 0) is at the **lower-left corner** (not top-left like web graphics)
|
||||
- X-axis points right, Y-axis points upward
|
||||
- Units are in points (72 points = 1 inch)
|
||||
- Default page size is A4; explicitly specify page size for consistency
|
||||
|
||||
## Basic Setup
|
||||
|
||||
```python
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.pagesizes import letter, A4
|
||||
from reportlab.lib.units import inch
|
||||
|
||||
# Create canvas
|
||||
c = canvas.Canvas("output.pdf", pagesize=letter)
|
||||
|
||||
# Get page dimensions
|
||||
width, height = letter
|
||||
|
||||
# Draw content
|
||||
c.drawString(100, 100, "Hello World")
|
||||
|
||||
# Finish page and save
|
||||
c.showPage() # Complete current page
|
||||
c.save() # Write PDF to disk
|
||||
```
|
||||
|
||||
## Text Drawing
|
||||
|
||||
### Basic String Methods
|
||||
```python
|
||||
# Basic text placement
|
||||
c.drawString(x, y, text) # Left-aligned at x, y
|
||||
c.drawRightString(x, y, text) # Right-aligned at x, y
|
||||
c.drawCentredString(x, y, text) # Center-aligned at x, y
|
||||
|
||||
# Font control
|
||||
c.setFont(fontname, size) # e.g., "Helvetica", 12
|
||||
c.setFillColor(color) # Text color
|
||||
```
|
||||
|
||||
### Text Objects (Advanced)
|
||||
For complex text operations with multiple lines and precise control:
|
||||
|
||||
```python
|
||||
t = c.beginText(x, y)
|
||||
t.setFont("Times-Roman", 14)
|
||||
t.textLine("First line")
|
||||
t.textLine("Second line")
|
||||
t.setTextOrigin(x, y) # Reset position
|
||||
c.drawText(t)
|
||||
```
|
||||
|
||||
## Drawing Primitives
|
||||
|
||||
### Lines
|
||||
```python
|
||||
c.line(x1, y1, x2, y2) # Single line
|
||||
c.lines([(x1,y1,x2,y2), (x3,y3,x4,y4)]) # Multiple lines
|
||||
c.grid(xlist, ylist) # Grid from coordinate lists
|
||||
```
|
||||
|
||||
### Shapes
|
||||
```python
|
||||
c.rect(x, y, width, height, stroke=1, fill=0)
|
||||
c.roundRect(x, y, width, height, radius, stroke=1, fill=0)
|
||||
c.circle(x_ctr, y_ctr, r, stroke=1, fill=0)
|
||||
c.ellipse(x1, y1, x2, y2, stroke=1, fill=0)
|
||||
c.wedge(x, y, radius, startAng, extent, stroke=1, fill=0)
|
||||
```
|
||||
|
||||
### Bezier Curves
|
||||
```python
|
||||
c.bezier(x1, y1, x2, y2, x3, y3, x4, y4)
|
||||
```
|
||||
|
||||
## Path Objects
|
||||
|
||||
For complex shapes, use path objects:
|
||||
|
||||
```python
|
||||
p = c.beginPath()
|
||||
p.moveTo(x, y) # Move without drawing
|
||||
p.lineTo(x, y) # Draw line to point
|
||||
p.curveTo(x1, y1, x2, y2, x3, y3) # Bezier curve
|
||||
p.arc(x1, y1, x2, y2, startAng, extent)
|
||||
p.arcTo(x1, y1, x2, y2, startAng, extent)
|
||||
p.close() # Close path to start point
|
||||
|
||||
# Draw the path
|
||||
c.drawPath(p, stroke=1, fill=0)
|
||||
```
|
||||
|
||||
## Colors
|
||||
|
||||
### RGB (Screen Display)
|
||||
```python
|
||||
from reportlab.lib.colors import red, blue, Color
|
||||
|
||||
c.setFillColorRGB(r, g, b) # r, g, b are 0-1
|
||||
c.setStrokeColorRGB(r, g, b)
|
||||
c.setFillColor(red) # Named colors
|
||||
c.setStrokeColor(blue)
|
||||
|
||||
# Custom with alpha transparency
|
||||
c.setFillColor(Color(0.5, 0, 0, alpha=0.5))
|
||||
```
|
||||
|
||||
### CMYK (Professional Printing)
|
||||
```python
|
||||
from reportlab.lib.colors import CMYKColor, PCMYKColor
|
||||
|
||||
c.setFillColorCMYK(c, m, y, k) # 0-1 range
|
||||
c.setStrokeColorCMYK(c, m, y, k)
|
||||
|
||||
# Integer percentages (0-100)
|
||||
c.setFillColor(PCMYKColor(100, 50, 0, 0))
|
||||
```
|
||||
|
||||
## Line Styling
|
||||
|
||||
```python
|
||||
c.setLineWidth(width) # Thickness in points
|
||||
c.setLineCap(mode) # 0=butt, 1=round, 2=square
|
||||
c.setLineJoin(mode) # 0=miter, 1=round, 2=bevel
|
||||
c.setDash(array, phase) # e.g., [3, 3] for dotted line
|
||||
```
|
||||
|
||||
## Coordinate Transformations
|
||||
|
||||
**IMPORTANT:** Transformations are incremental and cumulative.
|
||||
|
||||
```python
|
||||
# Translation (move origin)
|
||||
c.translate(dx, dy)
|
||||
|
||||
# Rotation (in degrees, counterclockwise)
|
||||
c.rotate(theta)
|
||||
|
||||
# Scaling
|
||||
c.scale(xscale, yscale)
|
||||
|
||||
# Skewing
|
||||
c.skew(alpha, beta)
|
||||
```
|
||||
|
||||
### State Management
|
||||
```python
|
||||
# Save current graphics state
|
||||
c.saveState()
|
||||
|
||||
# ... apply transformations and draw ...
|
||||
|
||||
# Restore previous state
|
||||
c.restoreState()
|
||||
```
|
||||
|
||||
**Note:** State cannot be preserved across `showPage()` calls.
|
||||
|
||||
## Images
|
||||
|
||||
```python
|
||||
from reportlab.lib.utils import ImageReader
|
||||
|
||||
# Preferred method (with caching)
|
||||
c.drawImage(image_source, x, y, width=None, height=None,
|
||||
mask=None, preserveAspectRatio=False)
|
||||
|
||||
# image_source can be:
|
||||
# - Filename string
|
||||
# - PIL Image object
|
||||
# - ImageReader object
|
||||
|
||||
# For transparency, specify RGB mask range
|
||||
c.drawImage("logo.png", 100, 500, mask=[255, 255, 255, 255, 255, 255])
|
||||
|
||||
# Inline (inefficient, no caching)
|
||||
c.drawInlineImage(image_source, x, y, width=None, height=None)
|
||||
```
|
||||
|
||||
## Page Management
|
||||
|
||||
```python
|
||||
# Complete current page
|
||||
c.showPage()
|
||||
|
||||
# Set page size for next page
|
||||
c.setPageSize(size) # e.g., letter, A4
|
||||
|
||||
# Page compression (smaller files, slower generation)
|
||||
c = canvas.Canvas("output.pdf", pageCompression=1)
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Margins and Layout
|
||||
```python
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.lib.pagesizes import letter
|
||||
|
||||
width, height = letter
|
||||
margin = inch
|
||||
|
||||
# Draw within margins
|
||||
content_width = width - 2*margin
|
||||
content_height = height - 2*margin
|
||||
|
||||
# Text at top margin
|
||||
c.drawString(margin, height - margin, "Header")
|
||||
|
||||
# Text at bottom margin
|
||||
c.drawString(margin, margin, "Footer")
|
||||
```
|
||||
|
||||
### Headers and Footers
|
||||
```python
|
||||
def draw_header_footer(c, width, height):
|
||||
c.saveState()
|
||||
c.setFont("Helvetica", 9)
|
||||
c.drawString(inch, height - 0.5*inch, "Company Name")
|
||||
c.drawRightString(width - inch, 0.5*inch, f"Page {c.getPageNumber()}")
|
||||
c.restoreState()
|
||||
|
||||
# Call on each page
|
||||
draw_header_footer(c, width, height)
|
||||
c.showPage()
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always specify page size** - Different platforms have different defaults
|
||||
2. **Use variables for measurements** - `margin = inch` instead of hardcoded values
|
||||
3. **Match saveState/restoreState** - Always balance these calls
|
||||
4. **Apply transformations externally** for engineering drawings to prevent line width scaling
|
||||
5. **Use drawImage over drawInlineImage** for better performance with repeated images
|
||||
6. **Draw from bottom-up** - Remember Y-axis points upward
|
||||
624
skills/reportlab/references/charts_reference.md
Normal file
624
skills/reportlab/references/charts_reference.md
Normal file
@@ -0,0 +1,624 @@
|
||||
# Charts and Graphics Reference
|
||||
|
||||
Comprehensive guide to creating charts and data visualizations in ReportLab.
|
||||
|
||||
## Graphics Architecture
|
||||
|
||||
ReportLab's graphics system provides platform-independent drawing:
|
||||
|
||||
- **Drawings** - Container for shapes and charts
|
||||
- **Shapes** - Primitives (rectangles, circles, lines, polygons, paths)
|
||||
- **Renderers** - Convert to PDF, PostScript, SVG, or bitmaps (PNG, GIF, JPG)
|
||||
- **Coordinate System** - Y-axis points upward (like PDF, unlike web graphics)
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
from reportlab.graphics.charts.barcharts import VerticalBarChart
|
||||
from reportlab.graphics import renderPDF
|
||||
|
||||
# Create drawing (canvas for chart)
|
||||
drawing = Drawing(400, 200)
|
||||
|
||||
# Create chart
|
||||
chart = VerticalBarChart()
|
||||
chart.x = 50
|
||||
chart.y = 50
|
||||
chart.width = 300
|
||||
chart.height = 125
|
||||
chart.data = [[100, 150, 130, 180]]
|
||||
chart.categoryAxis.categoryNames = ['Q1', 'Q2', 'Q3', 'Q4']
|
||||
|
||||
# Add chart to drawing
|
||||
drawing.add(chart)
|
||||
|
||||
# Render to PDF
|
||||
renderPDF.drawToFile(drawing, 'chart.pdf', 'Chart Title')
|
||||
|
||||
# Or add as flowable to Platypus document
|
||||
story.append(drawing)
|
||||
```
|
||||
|
||||
## Available Chart Types
|
||||
|
||||
### Bar Charts
|
||||
|
||||
```python
|
||||
from reportlab.graphics.charts.barcharts import (
|
||||
VerticalBarChart,
|
||||
HorizontalBarChart,
|
||||
)
|
||||
|
||||
# Vertical bar chart
|
||||
chart = VerticalBarChart()
|
||||
chart.x = 50
|
||||
chart.y = 50
|
||||
chart.width = 300
|
||||
chart.height = 150
|
||||
|
||||
# Single series
|
||||
chart.data = [[100, 150, 130, 180, 140]]
|
||||
|
||||
# Multiple series (grouped bars)
|
||||
chart.data = [
|
||||
[100, 150, 130, 180], # Series 1
|
||||
[80, 120, 110, 160], # Series 2
|
||||
]
|
||||
|
||||
# Categories
|
||||
chart.categoryAxis.categoryNames = ['Q1', 'Q2', 'Q3', 'Q4']
|
||||
|
||||
# Colors for each series
|
||||
chart.bars[0].fillColor = colors.blue
|
||||
chart.bars[1].fillColor = colors.red
|
||||
|
||||
# Bar spacing
|
||||
chart.barWidth = 10
|
||||
chart.groupSpacing = 10
|
||||
chart.barSpacing = 2
|
||||
```
|
||||
|
||||
### Stacked Bar Charts
|
||||
|
||||
```python
|
||||
from reportlab.graphics.charts.barcharts import VerticalBarChart
|
||||
|
||||
chart = VerticalBarChart()
|
||||
# ... set position and size ...
|
||||
|
||||
chart.data = [
|
||||
[100, 150, 130, 180], # Bottom layer
|
||||
[50, 70, 60, 90], # Top layer
|
||||
]
|
||||
chart.categoryAxis.categoryNames = ['Q1', 'Q2', 'Q3', 'Q4']
|
||||
|
||||
# Enable stacking
|
||||
chart.barLabelFormat = 'values'
|
||||
chart.valueAxis.visible = 1
|
||||
```
|
||||
|
||||
### Horizontal Bar Charts
|
||||
|
||||
```python
|
||||
from reportlab.graphics.charts.barcharts import HorizontalBarChart
|
||||
|
||||
chart = HorizontalBarChart()
|
||||
chart.x = 50
|
||||
chart.y = 50
|
||||
chart.width = 300
|
||||
chart.height = 150
|
||||
|
||||
chart.data = [[100, 150, 130, 180]]
|
||||
chart.categoryAxis.categoryNames = ['Product A', 'Product B', 'Product C', 'Product D']
|
||||
|
||||
# Horizontal charts use valueAxis horizontally
|
||||
chart.valueAxis.valueMin = 0
|
||||
chart.valueAxis.valueMax = 200
|
||||
```
|
||||
|
||||
### Line Charts
|
||||
|
||||
```python
|
||||
from reportlab.graphics.charts.linecharts import HorizontalLineChart
|
||||
|
||||
chart = HorizontalLineChart()
|
||||
chart.x = 50
|
||||
chart.y = 50
|
||||
chart.width = 300
|
||||
chart.height = 150
|
||||
|
||||
# Multiple lines
|
||||
chart.data = [
|
||||
[100, 150, 130, 180, 140], # Line 1
|
||||
[80, 120, 110, 160, 130], # Line 2
|
||||
]
|
||||
|
||||
chart.categoryAxis.categoryNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May']
|
||||
|
||||
# Line styling
|
||||
chart.lines[0].strokeColor = colors.blue
|
||||
chart.lines[0].strokeWidth = 2
|
||||
chart.lines[1].strokeColor = colors.red
|
||||
chart.lines[1].strokeWidth = 2
|
||||
|
||||
# Show/hide points
|
||||
chart.lines[0].symbol = None # No symbols
|
||||
# Or use symbols from makeMarker()
|
||||
```
|
||||
|
||||
### Line Plots (X-Y Plots)
|
||||
|
||||
```python
|
||||
from reportlab.graphics.charts.lineplots import LinePlot
|
||||
|
||||
chart = LinePlot()
|
||||
chart.x = 50
|
||||
chart.y = 50
|
||||
chart.width = 300
|
||||
chart.height = 150
|
||||
|
||||
# Data as (x, y) tuples
|
||||
chart.data = [
|
||||
[(0, 0), (1, 1), (2, 4), (3, 9), (4, 16)], # y = x^2
|
||||
[(0, 0), (1, 2), (2, 4), (3, 6), (4, 8)], # y = 2x
|
||||
]
|
||||
|
||||
# Both axes are value axes (not category)
|
||||
chart.xValueAxis.valueMin = 0
|
||||
chart.xValueAxis.valueMax = 5
|
||||
chart.yValueAxis.valueMin = 0
|
||||
chart.yValueAxis.valueMax = 20
|
||||
|
||||
# Line styling
|
||||
chart.lines[0].strokeColor = colors.blue
|
||||
chart.lines[1].strokeColor = colors.red
|
||||
```
|
||||
|
||||
### Pie Charts
|
||||
|
||||
```python
|
||||
from reportlab.graphics.charts.piecharts import Pie
|
||||
|
||||
chart = Pie()
|
||||
chart.x = 100
|
||||
chart.y = 50
|
||||
chart.width = 200
|
||||
chart.height = 200
|
||||
|
||||
chart.data = [25, 35, 20, 20]
|
||||
chart.labels = ['Q1', 'Q2', 'Q3', 'Q4']
|
||||
|
||||
# Slice colors
|
||||
chart.slices[0].fillColor = colors.blue
|
||||
chart.slices[1].fillColor = colors.red
|
||||
chart.slices[2].fillColor = colors.green
|
||||
chart.slices[3].fillColor = colors.yellow
|
||||
|
||||
# Pop out a slice
|
||||
chart.slices[1].popout = 10
|
||||
|
||||
# Label positioning
|
||||
chart.slices.strokeColor = colors.white
|
||||
chart.slices.strokeWidth = 2
|
||||
```
|
||||
|
||||
### Pie Chart with Side Labels
|
||||
|
||||
```python
|
||||
from reportlab.graphics.charts.piecharts import Pie
|
||||
|
||||
chart = Pie()
|
||||
# ... set position, data, labels ...
|
||||
|
||||
# Side label mode (labels in columns beside pie)
|
||||
chart.sideLabels = 1
|
||||
chart.sideLabelsOffset = 0.1 # Distance from pie
|
||||
|
||||
# Simple labels (not fancy layout)
|
||||
chart.simpleLabels = 1
|
||||
```
|
||||
|
||||
### Area Charts
|
||||
|
||||
```python
|
||||
from reportlab.graphics.charts.areacharts import HorizontalAreaChart
|
||||
|
||||
chart = HorizontalAreaChart()
|
||||
chart.x = 50
|
||||
chart.y = 50
|
||||
chart.width = 300
|
||||
chart.height = 150
|
||||
|
||||
# Areas stack on top of each other
|
||||
chart.data = [
|
||||
[100, 150, 130, 180], # Bottom area
|
||||
[50, 70, 60, 90], # Top area
|
||||
]
|
||||
|
||||
chart.categoryAxis.categoryNames = ['Q1', 'Q2', 'Q3', 'Q4']
|
||||
|
||||
# Area colors
|
||||
chart.strands[0].fillColor = colors.lightblue
|
||||
chart.strands[1].fillColor = colors.pink
|
||||
```
|
||||
|
||||
### Scatter Charts
|
||||
|
||||
```python
|
||||
from reportlab.graphics.charts.lineplots import ScatterPlot
|
||||
|
||||
chart = ScatterPlot()
|
||||
chart.x = 50
|
||||
chart.y = 50
|
||||
chart.width = 300
|
||||
chart.height = 150
|
||||
|
||||
# Data points
|
||||
chart.data = [
|
||||
[(1, 2), (2, 3), (3, 5), (4, 4), (5, 6)], # Series 1
|
||||
[(1, 1), (2, 2), (3, 3), (4, 3), (5, 4)], # Series 2
|
||||
]
|
||||
|
||||
# Hide lines, show points only
|
||||
chart.lines[0].strokeColor = None
|
||||
chart.lines[1].strokeColor = None
|
||||
|
||||
# Marker symbols
|
||||
from reportlab.graphics.widgets.markers import makeMarker
|
||||
chart.lines[0].symbol = makeMarker('Circle')
|
||||
chart.lines[1].symbol = makeMarker('Square')
|
||||
```
|
||||
|
||||
## Axes Configuration
|
||||
|
||||
### Category Axis (XCategoryAxis)
|
||||
|
||||
For categorical data (labels, not numbers):
|
||||
|
||||
```python
|
||||
# Access via chart
|
||||
axis = chart.categoryAxis
|
||||
|
||||
# Labels
|
||||
axis.categoryNames = ['Jan', 'Feb', 'Mar', 'Apr']
|
||||
|
||||
# Label angle (for long labels)
|
||||
axis.labels.angle = 45
|
||||
axis.labels.dx = 0
|
||||
axis.labels.dy = -5
|
||||
|
||||
# Label formatting
|
||||
axis.labels.fontSize = 10
|
||||
axis.labels.fontName = 'Helvetica'
|
||||
|
||||
# Visibility
|
||||
axis.visible = 1
|
||||
```
|
||||
|
||||
### Value Axis (YValueAxis)
|
||||
|
||||
For numeric data:
|
||||
|
||||
```python
|
||||
# Access via chart
|
||||
axis = chart.valueAxis
|
||||
|
||||
# Range
|
||||
axis.valueMin = 0
|
||||
axis.valueMax = 200
|
||||
axis.valueStep = 50 # Tick interval
|
||||
|
||||
# Or auto-configure
|
||||
axis.valueSteps = [0, 50, 100, 150, 200] # Explicit steps
|
||||
|
||||
# Label formatting
|
||||
axis.labels.fontSize = 10
|
||||
axis.labelTextFormat = '%d%%' # Add percentage sign
|
||||
|
||||
# Grid lines
|
||||
axis.strokeWidth = 1
|
||||
axis.strokeColor = colors.black
|
||||
```
|
||||
|
||||
## Styling and Customization
|
||||
|
||||
### Colors
|
||||
|
||||
```python
|
||||
from reportlab.lib import colors
|
||||
|
||||
# Named colors
|
||||
colors.blue, colors.red, colors.green, colors.yellow
|
||||
|
||||
# RGB
|
||||
colors.Color(0.5, 0.5, 0.5) # Grey
|
||||
|
||||
# With alpha
|
||||
colors.Color(1, 0, 0, alpha=0.5) # Semi-transparent red
|
||||
|
||||
# Hex colors
|
||||
colors.HexColor('#FF5733')
|
||||
```
|
||||
|
||||
### Line Styling
|
||||
|
||||
```python
|
||||
# For line charts
|
||||
chart.lines[0].strokeColor = colors.blue
|
||||
chart.lines[0].strokeWidth = 2
|
||||
chart.lines[0].strokeDashArray = [2, 2] # Dashed line
|
||||
```
|
||||
|
||||
### Bar Labels
|
||||
|
||||
```python
|
||||
# Show values on bars
|
||||
chart.barLabels.nudge = 5 # Offset from bar top
|
||||
chart.barLabels.fontSize = 8
|
||||
chart.barLabelFormat = '%d' # Number format
|
||||
|
||||
# For negative values
|
||||
chart.barLabels.dy = -5 # Position below bar
|
||||
```
|
||||
|
||||
## Legends
|
||||
|
||||
Charts can have associated legends:
|
||||
|
||||
```python
|
||||
from reportlab.graphics.charts.legends import Legend
|
||||
|
||||
# Create legend
|
||||
legend = Legend()
|
||||
legend.x = 350
|
||||
legend.y = 150
|
||||
legend.columnMaximum = 10
|
||||
|
||||
# Link to chart (share colors)
|
||||
legend.colorNamePairs = [
|
||||
(chart.bars[0].fillColor, 'Series 1'),
|
||||
(chart.bars[1].fillColor, 'Series 2'),
|
||||
]
|
||||
|
||||
# Add to drawing
|
||||
drawing.add(legend)
|
||||
```
|
||||
|
||||
## Drawing Shapes
|
||||
|
||||
### Basic Shapes
|
||||
|
||||
```python
|
||||
from reportlab.graphics.shapes import (
|
||||
Drawing, Rect, Circle, Ellipse, Line, Polygon, String
|
||||
)
|
||||
from reportlab.lib import colors
|
||||
|
||||
drawing = Drawing(400, 200)
|
||||
|
||||
# Rectangle
|
||||
rect = Rect(50, 50, 100, 50)
|
||||
rect.fillColor = colors.blue
|
||||
rect.strokeColor = colors.black
|
||||
rect.strokeWidth = 1
|
||||
drawing.add(rect)
|
||||
|
||||
# Circle
|
||||
circle = Circle(200, 100, 30)
|
||||
circle.fillColor = colors.red
|
||||
drawing.add(circle)
|
||||
|
||||
# Line
|
||||
line = Line(50, 150, 350, 150)
|
||||
line.strokeColor = colors.black
|
||||
line.strokeWidth = 2
|
||||
drawing.add(line)
|
||||
|
||||
# Text
|
||||
text = String(50, 175, "Label Text")
|
||||
text.fontSize = 12
|
||||
text.fontName = 'Helvetica'
|
||||
drawing.add(text)
|
||||
```
|
||||
|
||||
### Paths (Complex Shapes)
|
||||
|
||||
```python
|
||||
from reportlab.graphics.shapes import Path
|
||||
|
||||
path = Path()
|
||||
path.moveTo(50, 50)
|
||||
path.lineTo(100, 100)
|
||||
path.curveTo(120, 120, 140, 100, 150, 50)
|
||||
path.closePath()
|
||||
|
||||
path.fillColor = colors.lightblue
|
||||
path.strokeColor = colors.blue
|
||||
path.strokeWidth = 2
|
||||
|
||||
drawing.add(path)
|
||||
```
|
||||
|
||||
## Rendering Options
|
||||
|
||||
### Render to PDF
|
||||
|
||||
```python
|
||||
from reportlab.graphics import renderPDF
|
||||
|
||||
# Direct to file
|
||||
renderPDF.drawToFile(drawing, 'output.pdf', 'Chart Title')
|
||||
|
||||
# As flowable in Platypus
|
||||
story.append(drawing)
|
||||
```
|
||||
|
||||
### Render to Image
|
||||
|
||||
```python
|
||||
from reportlab.graphics import renderPM
|
||||
|
||||
# PNG
|
||||
renderPM.drawToFile(drawing, 'chart.png', fmt='PNG')
|
||||
|
||||
# GIF
|
||||
renderPM.drawToFile(drawing, 'chart.gif', fmt='GIF')
|
||||
|
||||
# JPG
|
||||
renderPM.drawToFile(drawing, 'chart.jpg', fmt='JPG')
|
||||
|
||||
# With specific DPI
|
||||
renderPM.drawToFile(drawing, 'chart.png', fmt='PNG', dpi=150)
|
||||
```
|
||||
|
||||
### Render to SVG
|
||||
|
||||
```python
|
||||
from reportlab.graphics import renderSVG
|
||||
|
||||
renderSVG.drawToFile(drawing, 'chart.svg')
|
||||
```
|
||||
|
||||
## Advanced Customization
|
||||
|
||||
### Inspect Properties
|
||||
|
||||
```python
|
||||
# List all properties
|
||||
print(chart.getProperties())
|
||||
|
||||
# Dump properties (for debugging)
|
||||
chart.dumpProperties()
|
||||
|
||||
# Set multiple properties
|
||||
chart.setProperties({
|
||||
'width': 400,
|
||||
'height': 200,
|
||||
'data': [[100, 150, 130]],
|
||||
})
|
||||
```
|
||||
|
||||
### Custom Colors for Series
|
||||
|
||||
```python
|
||||
# Define color scheme
|
||||
from reportlab.lib.colors import PCMYKColor
|
||||
|
||||
colors_list = [
|
||||
PCMYKColor(100, 67, 0, 23), # Blue
|
||||
PCMYKColor(0, 100, 100, 0), # Red
|
||||
PCMYKColor(66, 13, 0, 22), # Green
|
||||
]
|
||||
|
||||
# Apply to chart
|
||||
for i, color in enumerate(colors_list):
|
||||
chart.bars[i].fillColor = color
|
||||
```
|
||||
|
||||
## Complete Examples
|
||||
|
||||
### Sales Report Bar Chart
|
||||
|
||||
```python
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
from reportlab.graphics.charts.barcharts import VerticalBarChart
|
||||
from reportlab.graphics.charts.legends import Legend
|
||||
from reportlab.lib import colors
|
||||
|
||||
drawing = Drawing(400, 250)
|
||||
|
||||
# Create chart
|
||||
chart = VerticalBarChart()
|
||||
chart.x = 50
|
||||
chart.y = 50
|
||||
chart.width = 300
|
||||
chart.height = 150
|
||||
|
||||
# Data
|
||||
chart.data = [
|
||||
[120, 150, 180, 200], # 2023
|
||||
[100, 130, 160, 190], # 2022
|
||||
]
|
||||
chart.categoryAxis.categoryNames = ['Q1', 'Q2', 'Q3', 'Q4']
|
||||
|
||||
# Styling
|
||||
chart.bars[0].fillColor = colors.HexColor('#3498db')
|
||||
chart.bars[1].fillColor = colors.HexColor('#e74c3c')
|
||||
chart.valueAxis.valueMin = 0
|
||||
chart.valueAxis.valueMax = 250
|
||||
chart.categoryAxis.labels.fontSize = 10
|
||||
chart.valueAxis.labels.fontSize = 10
|
||||
|
||||
# Add legend
|
||||
legend = Legend()
|
||||
legend.x = 325
|
||||
legend.y = 200
|
||||
legend.columnMaximum = 2
|
||||
legend.colorNamePairs = [
|
||||
(chart.bars[0].fillColor, '2023'),
|
||||
(chart.bars[1].fillColor, '2022'),
|
||||
]
|
||||
|
||||
drawing.add(chart)
|
||||
drawing.add(legend)
|
||||
|
||||
# Add to story or save
|
||||
story.append(drawing)
|
||||
```
|
||||
|
||||
### Multi-Line Trend Chart
|
||||
|
||||
```python
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
from reportlab.graphics.charts.linecharts import HorizontalLineChart
|
||||
from reportlab.lib import colors
|
||||
|
||||
drawing = Drawing(400, 250)
|
||||
|
||||
chart = HorizontalLineChart()
|
||||
chart.x = 50
|
||||
chart.y = 50
|
||||
chart.width = 320
|
||||
chart.height = 170
|
||||
|
||||
# Data
|
||||
chart.data = [
|
||||
[10, 15, 12, 18, 20, 25], # Product A
|
||||
[8, 10, 14, 16, 18, 22], # Product B
|
||||
[12, 11, 13, 15, 17, 19], # Product C
|
||||
]
|
||||
|
||||
chart.categoryAxis.categoryNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
|
||||
|
||||
# Line styling
|
||||
chart.lines[0].strokeColor = colors.blue
|
||||
chart.lines[0].strokeWidth = 2
|
||||
chart.lines[1].strokeColor = colors.red
|
||||
chart.lines[1].strokeWidth = 2
|
||||
chart.lines[2].strokeColor = colors.green
|
||||
chart.lines[2].strokeWidth = 2
|
||||
|
||||
# Axes
|
||||
chart.valueAxis.valueMin = 0
|
||||
chart.valueAxis.valueMax = 30
|
||||
chart.categoryAxis.labels.angle = 0
|
||||
chart.categoryAxis.labels.fontSize = 9
|
||||
chart.valueAxis.labels.fontSize = 9
|
||||
|
||||
drawing.add(chart)
|
||||
story.append(drawing)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Set explicit dimensions** for Drawing to ensure consistent sizing
|
||||
2. **Position charts** with enough margin (x, y at least 30-50 from edge)
|
||||
3. **Use consistent color schemes** throughout document
|
||||
4. **Set valueMin and valueMax** explicitly for consistent scales
|
||||
5. **Test with realistic data** to ensure labels fit and don't overlap
|
||||
6. **Add legends** for multi-series charts
|
||||
7. **Angle category labels** if they're long (45° works well)
|
||||
8. **Keep it simple** - fewer data series are easier to read
|
||||
9. **Use appropriate chart types** - bars for comparisons, lines for trends, pies for proportions
|
||||
10. **Consider colorblind-friendly palettes** - avoid red/green combinations
|
||||
561
skills/reportlab/references/pdf_features.md
Normal file
561
skills/reportlab/references/pdf_features.md
Normal file
@@ -0,0 +1,561 @@
|
||||
# PDF Features Reference
|
||||
|
||||
Advanced PDF capabilities: links, bookmarks, forms, encryption, and metadata.
|
||||
|
||||
## Document Metadata
|
||||
|
||||
Set PDF document properties viewable in PDF readers.
|
||||
|
||||
```python
|
||||
from reportlab.pdfgen import canvas
|
||||
|
||||
c = canvas.Canvas("output.pdf")
|
||||
|
||||
# Set metadata
|
||||
c.setAuthor("John Doe")
|
||||
c.setTitle("Annual Report 2024")
|
||||
c.setSubject("Financial Analysis")
|
||||
c.setKeywords("finance, annual, report, 2024")
|
||||
c.setCreator("MyApp v1.0")
|
||||
|
||||
# ... draw content ...
|
||||
|
||||
c.save()
|
||||
```
|
||||
|
||||
With Platypus:
|
||||
|
||||
```python
|
||||
from reportlab.platypus import SimpleDocTemplate
|
||||
|
||||
doc = SimpleDocTemplate(
|
||||
"output.pdf",
|
||||
title="Annual Report 2024",
|
||||
author="John Doe",
|
||||
subject="Financial Analysis",
|
||||
)
|
||||
|
||||
doc.build(story)
|
||||
```
|
||||
|
||||
## Bookmarks and Destinations
|
||||
|
||||
Create internal navigation structure.
|
||||
|
||||
### Simple Bookmarks
|
||||
|
||||
```python
|
||||
from reportlab.pdfgen import canvas
|
||||
|
||||
c = canvas.Canvas("output.pdf")
|
||||
|
||||
# Create bookmark for current page
|
||||
c.bookmarkPage("intro") # Internal key
|
||||
c.addOutlineEntry("Introduction", "intro", level=0)
|
||||
|
||||
c.showPage()
|
||||
|
||||
# Another bookmark
|
||||
c.bookmarkPage("chapter1")
|
||||
c.addOutlineEntry("Chapter 1", "chapter1", level=0)
|
||||
|
||||
# Sub-sections
|
||||
c.bookmarkPage("section1_1")
|
||||
c.addOutlineEntry("Section 1.1", "section1_1", level=1) # Nested
|
||||
|
||||
c.save()
|
||||
```
|
||||
|
||||
### Bookmark Levels
|
||||
|
||||
```python
|
||||
# Create hierarchical outline
|
||||
c.bookmarkPage("ch1")
|
||||
c.addOutlineEntry("Chapter 1", "ch1", level=0)
|
||||
|
||||
c.bookmarkPage("ch1_s1")
|
||||
c.addOutlineEntry("Section 1.1", "ch1_s1", level=1)
|
||||
|
||||
c.bookmarkPage("ch1_s1_1")
|
||||
c.addOutlineEntry("Subsection 1.1.1", "ch1_s1_1", level=2)
|
||||
|
||||
c.bookmarkPage("ch2")
|
||||
c.addOutlineEntry("Chapter 2", "ch2", level=0)
|
||||
```
|
||||
|
||||
### Destination Fit Modes
|
||||
|
||||
Control how the page displays when navigating:
|
||||
|
||||
```python
|
||||
# bookmarkPage with fit mode
|
||||
c.bookmarkPage(
|
||||
key="chapter1",
|
||||
fit="Fit" # Fit entire page in window
|
||||
)
|
||||
|
||||
# Or use bookmarkHorizontalAbsolute
|
||||
c.bookmarkHorizontalAbsolute(key="section", top=500)
|
||||
|
||||
# Available fit modes:
|
||||
# "Fit" - Fit whole page
|
||||
# "FitH" - Fit horizontally
|
||||
# "FitV" - Fit vertically
|
||||
# "FitR" - Fit rectangle
|
||||
# "XYZ" - Specific position and zoom
|
||||
```
|
||||
|
||||
## Hyperlinks
|
||||
|
||||
### External Links
|
||||
|
||||
```python
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.units import inch
|
||||
|
||||
c = canvas.Canvas("output.pdf")
|
||||
|
||||
# Draw link rectangle
|
||||
c.linkURL(
|
||||
"https://www.example.com",
|
||||
rect=(1*inch, 5*inch, 3*inch, 5.5*inch), # (x1, y1, x2, y2)
|
||||
relative=0, # 0 for absolute positioning
|
||||
thickness=1,
|
||||
color=(0, 0, 1), # Blue
|
||||
dashArray=None
|
||||
)
|
||||
|
||||
# Draw text over link area
|
||||
c.setFillColorRGB(0, 0, 1) # Blue text
|
||||
c.drawString(1*inch, 5.2*inch, "Click here to visit example.com")
|
||||
|
||||
c.save()
|
||||
```
|
||||
|
||||
### Internal Links
|
||||
|
||||
Link to bookmarked locations within the document:
|
||||
|
||||
```python
|
||||
# Create destination
|
||||
c.bookmarkPage("target_section")
|
||||
|
||||
# Later, create link to that destination
|
||||
c.linkRect(
|
||||
"Link Text",
|
||||
"target_section", # Bookmark key
|
||||
rect=(1*inch, 3*inch, 2*inch, 3.2*inch),
|
||||
relative=0
|
||||
)
|
||||
```
|
||||
|
||||
### Links in Paragraphs
|
||||
|
||||
For Platypus documents:
|
||||
|
||||
```python
|
||||
from reportlab.platypus import Paragraph
|
||||
|
||||
# External link
|
||||
text = '<link href="https://example.com" color="blue">Visit our website</link>'
|
||||
para = Paragraph(text, style)
|
||||
|
||||
# Internal link (to anchor)
|
||||
text = '<link href="#section1" color="blue">Go to Section 1</link>'
|
||||
para1 = Paragraph(text, style)
|
||||
|
||||
# Create anchor
|
||||
text = '<a name="section1"/>Section 1 Heading'
|
||||
para2 = Paragraph(text, heading_style)
|
||||
|
||||
story.append(para1)
|
||||
story.append(para2)
|
||||
```
|
||||
|
||||
## Interactive Forms
|
||||
|
||||
Create fillable PDF forms.
|
||||
|
||||
### Text Fields
|
||||
|
||||
```python
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.pdfbase import pdfform
|
||||
from reportlab.lib.colors import black, white
|
||||
|
||||
c = canvas.Canvas("form.pdf")
|
||||
|
||||
# Create text field
|
||||
c.acroForm.textfield(
|
||||
name="name",
|
||||
tooltip="Enter your name",
|
||||
x=100,
|
||||
y=700,
|
||||
width=200,
|
||||
height=20,
|
||||
borderColor=black,
|
||||
fillColor=white,
|
||||
textColor=black,
|
||||
forceBorder=True,
|
||||
fontSize=12,
|
||||
maxlen=100, # Maximum character length
|
||||
)
|
||||
|
||||
# Label
|
||||
c.drawString(100, 725, "Name:")
|
||||
|
||||
c.save()
|
||||
```
|
||||
|
||||
### Checkboxes
|
||||
|
||||
```python
|
||||
# Create checkbox
|
||||
c.acroForm.checkbox(
|
||||
name="agree",
|
||||
tooltip="I agree to terms",
|
||||
x=100,
|
||||
y=650,
|
||||
size=20,
|
||||
buttonStyle='check', # 'check', 'circle', 'cross', 'diamond', 'square', 'star'
|
||||
borderColor=black,
|
||||
fillColor=white,
|
||||
textColor=black,
|
||||
forceBorder=True,
|
||||
checked=False, # Initial state
|
||||
)
|
||||
|
||||
c.drawString(130, 655, "I agree to the terms and conditions")
|
||||
```
|
||||
|
||||
### Radio Buttons
|
||||
|
||||
```python
|
||||
# Radio button group - only one can be selected
|
||||
c.acroForm.radio(
|
||||
name="payment", # Same name for group
|
||||
tooltip="Credit Card",
|
||||
value="credit", # Value when selected
|
||||
x=100,
|
||||
y=600,
|
||||
size=15,
|
||||
selected=False,
|
||||
)
|
||||
c.drawString(125, 603, "Credit Card")
|
||||
|
||||
c.acroForm.radio(
|
||||
name="payment", # Same name
|
||||
tooltip="PayPal",
|
||||
value="paypal",
|
||||
x=100,
|
||||
y=580,
|
||||
size=15,
|
||||
selected=False,
|
||||
)
|
||||
c.drawString(125, 583, "PayPal")
|
||||
```
|
||||
|
||||
### List Boxes
|
||||
|
||||
```python
|
||||
# Listbox with multiple options
|
||||
c.acroForm.listbox(
|
||||
name="country",
|
||||
tooltip="Select your country",
|
||||
value="US", # Default selected
|
||||
x=100,
|
||||
y=500,
|
||||
width=150,
|
||||
height=80,
|
||||
borderColor=black,
|
||||
fillColor=white,
|
||||
textColor=black,
|
||||
forceBorder=True,
|
||||
options=[
|
||||
("United States", "US"),
|
||||
("Canada", "CA"),
|
||||
("Mexico", "MX"),
|
||||
("Other", "OTHER"),
|
||||
], # List of (label, value) tuples
|
||||
multiple=False, # Allow multiple selections
|
||||
)
|
||||
```
|
||||
|
||||
### Choice (Dropdown)
|
||||
|
||||
```python
|
||||
# Dropdown menu
|
||||
c.acroForm.choice(
|
||||
name="state",
|
||||
tooltip="Select state",
|
||||
value="CA",
|
||||
x=100,
|
||||
y=450,
|
||||
width=150,
|
||||
height=20,
|
||||
borderColor=black,
|
||||
fillColor=white,
|
||||
textColor=black,
|
||||
forceBorder=True,
|
||||
options=[
|
||||
("California", "CA"),
|
||||
("New York", "NY"),
|
||||
("Texas", "TX"),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### Complete Form Example
|
||||
|
||||
```python
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.lib.colors import black, white, lightgrey
|
||||
from reportlab.lib.units import inch
|
||||
|
||||
def create_registration_form(filename):
|
||||
c = canvas.Canvas(filename, pagesize=letter)
|
||||
c.setFont("Helvetica-Bold", 16)
|
||||
c.drawString(inch, 10*inch, "Registration Form")
|
||||
|
||||
y = 9*inch
|
||||
c.setFont("Helvetica", 12)
|
||||
|
||||
# Name field
|
||||
c.drawString(inch, y, "Full Name:")
|
||||
c.acroForm.textfield(
|
||||
name="fullname",
|
||||
x=2*inch, y=y-5, width=4*inch, height=20,
|
||||
borderColor=black, fillColor=lightgrey, forceBorder=True
|
||||
)
|
||||
|
||||
# Email field
|
||||
y -= 0.5*inch
|
||||
c.drawString(inch, y, "Email:")
|
||||
c.acroForm.textfield(
|
||||
name="email",
|
||||
x=2*inch, y=y-5, width=4*inch, height=20,
|
||||
borderColor=black, fillColor=lightgrey, forceBorder=True
|
||||
)
|
||||
|
||||
# Age dropdown
|
||||
y -= 0.5*inch
|
||||
c.drawString(inch, y, "Age Group:")
|
||||
c.acroForm.choice(
|
||||
name="age_group",
|
||||
x=2*inch, y=y-5, width=2*inch, height=20,
|
||||
borderColor=black, fillColor=lightgrey, forceBorder=True,
|
||||
options=[("18-25", "18-25"), ("26-35", "26-35"),
|
||||
("36-50", "36-50"), ("51+", "51+")]
|
||||
)
|
||||
|
||||
# Newsletter checkbox
|
||||
y -= 0.5*inch
|
||||
c.acroForm.checkbox(
|
||||
name="newsletter",
|
||||
x=inch, y=y-5, size=15,
|
||||
buttonStyle='check', borderColor=black, forceBorder=True
|
||||
)
|
||||
c.drawString(inch + 25, y, "Subscribe to newsletter")
|
||||
|
||||
c.save()
|
||||
|
||||
create_registration_form("registration.pdf")
|
||||
```
|
||||
|
||||
## Encryption and Security
|
||||
|
||||
Protect PDFs with passwords and permissions.
|
||||
|
||||
### Basic Encryption
|
||||
|
||||
```python
|
||||
from reportlab.pdfgen import canvas
|
||||
|
||||
c = canvas.Canvas("secure.pdf")
|
||||
|
||||
# Encrypt with user password
|
||||
c.encrypt(
|
||||
userPassword="user123", # Password to open
|
||||
ownerPassword="owner456", # Password to change permissions
|
||||
canPrint=1, # Allow printing
|
||||
canModify=0, # Disallow modifications
|
||||
canCopy=1, # Allow text copying
|
||||
canAnnotate=0, # Disallow annotations
|
||||
strength=128, # 40 or 128 bit encryption
|
||||
)
|
||||
|
||||
# ... draw content ...
|
||||
|
||||
c.save()
|
||||
```
|
||||
|
||||
### Permission Settings
|
||||
|
||||
```python
|
||||
c.encrypt(
|
||||
userPassword="user123",
|
||||
ownerPassword="owner456",
|
||||
canPrint=1, # 1 = allow, 0 = deny
|
||||
canModify=0, # Prevent content modification
|
||||
canCopy=1, # Allow text/graphics copying
|
||||
canAnnotate=0, # Prevent comments/annotations
|
||||
strength=128, # Use 128-bit encryption
|
||||
)
|
||||
```
|
||||
|
||||
### Advanced Encryption
|
||||
|
||||
```python
|
||||
from reportlab.lib.pdfencrypt import StandardEncryption
|
||||
|
||||
# Create encryption object
|
||||
encrypt = StandardEncryption(
|
||||
userPassword="user123",
|
||||
ownerPassword="owner456",
|
||||
canPrint=1,
|
||||
canModify=0,
|
||||
canCopy=1,
|
||||
canAnnotate=1,
|
||||
strength=128,
|
||||
)
|
||||
|
||||
# Use with canvas
|
||||
c = canvas.Canvas("secure.pdf")
|
||||
c._doc.encrypt = encrypt
|
||||
|
||||
# ... draw content ...
|
||||
|
||||
c.save()
|
||||
```
|
||||
|
||||
### Platypus with Encryption
|
||||
|
||||
```python
|
||||
from reportlab.platypus import SimpleDocTemplate
|
||||
|
||||
doc = SimpleDocTemplate("secure.pdf")
|
||||
|
||||
# Set encryption
|
||||
doc.encrypt = True
|
||||
doc.canPrint = 1
|
||||
doc.canModify = 0
|
||||
|
||||
# Or use encrypt() method
|
||||
doc.encrypt = encrypt_object
|
||||
|
||||
doc.build(story)
|
||||
```
|
||||
|
||||
## Page Transitions
|
||||
|
||||
Add visual effects for presentations.
|
||||
|
||||
```python
|
||||
from reportlab.pdfgen import canvas
|
||||
|
||||
c = canvas.Canvas("presentation.pdf")
|
||||
|
||||
# Set transition for current page
|
||||
c.setPageTransition(
|
||||
effectname="Wipe", # Transition effect
|
||||
duration=1, # Duration in seconds
|
||||
direction=0 # Direction (effect-specific)
|
||||
)
|
||||
|
||||
# Available effects:
|
||||
# "Split", "Blinds", "Box", "Wipe", "Dissolve",
|
||||
# "Glitter", "R" (Replace), "Fly", "Push", "Cover",
|
||||
# "Uncover", "Fade"
|
||||
|
||||
# Direction values (effect-dependent):
|
||||
# 0, 90, 180, 270 for most directional effects
|
||||
|
||||
# Example: Slide with fade transition
|
||||
c.setFont("Helvetica-Bold", 24)
|
||||
c.drawString(100, 400, "Slide 1")
|
||||
c.setPageTransition("Fade", 0.5)
|
||||
c.showPage()
|
||||
|
||||
c.drawString(100, 400, "Slide 2")
|
||||
c.setPageTransition("Wipe", 1, 90)
|
||||
c.showPage()
|
||||
|
||||
c.save()
|
||||
```
|
||||
|
||||
## PDF/A Compliance
|
||||
|
||||
Create archival-quality PDFs.
|
||||
|
||||
```python
|
||||
from reportlab.pdfgen import canvas
|
||||
|
||||
c = canvas.Canvas("pdfa.pdf")
|
||||
|
||||
# Enable PDF/A-1b compliance
|
||||
c.setPageCompression(0) # PDF/A requires uncompressed
|
||||
# Note: Full PDF/A requires additional XMP metadata
|
||||
# This is simplified - full compliance needs more setup
|
||||
|
||||
# ... draw content ...
|
||||
|
||||
c.save()
|
||||
```
|
||||
|
||||
## Compression
|
||||
|
||||
Control file size vs generation speed.
|
||||
|
||||
```python
|
||||
# Enable page compression
|
||||
c = canvas.Canvas("output.pdf", pageCompression=1)
|
||||
|
||||
# Compression reduces file size but slows generation
|
||||
# 0 = no compression (faster, larger files)
|
||||
# 1 = compression (slower, smaller files)
|
||||
```
|
||||
|
||||
## Forms and XObjects
|
||||
|
||||
Reusable graphics elements.
|
||||
|
||||
```python
|
||||
from reportlab.pdfgen import canvas
|
||||
|
||||
c = canvas.Canvas("output.pdf")
|
||||
|
||||
# Begin form (reusable object)
|
||||
c.beginForm("logo")
|
||||
c.setFillColorRGB(0, 0, 1)
|
||||
c.rect(0, 0, 100, 50, fill=1)
|
||||
c.setFillColorRGB(1, 1, 1)
|
||||
c.drawString(10, 20, "LOGO")
|
||||
c.endForm()
|
||||
|
||||
# Use form multiple times
|
||||
c.doForm("logo") # At current position
|
||||
c.translate(200, 0)
|
||||
c.doForm("logo") # At translated position
|
||||
c.translate(200, 0)
|
||||
c.doForm("logo")
|
||||
|
||||
c.save()
|
||||
|
||||
# Benefits: Smaller file size, faster rendering
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always set metadata** for professional documents
|
||||
2. **Use bookmarks** for documents > 10 pages
|
||||
3. **Make links visually distinct** (blue, underlined)
|
||||
4. **Test forms** in multiple PDF readers (behavior varies)
|
||||
5. **Use strong encryption (128-bit)** for sensitive data
|
||||
6. **Set both user and owner passwords** for full security
|
||||
7. **Enable printing** unless specifically restricted
|
||||
8. **Test page transitions** - some readers don't support all effects
|
||||
9. **Use meaningful bookmark titles** for navigation
|
||||
10. **Consider PDF/A** for long-term archival needs
|
||||
11. **Validate form field names** - must be unique and valid identifiers
|
||||
12. **Add tooltips** to form fields for better UX
|
||||
343
skills/reportlab/references/platypus_guide.md
Normal file
343
skills/reportlab/references/platypus_guide.md
Normal file
@@ -0,0 +1,343 @@
|
||||
# Platypus Guide - High-Level Page Layout
|
||||
|
||||
Platypus ("Page Layout and Typography Using Scripts") provides high-level document layout for complex, flowing documents with minimal code.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Platypus uses a layered design:
|
||||
|
||||
1. **DocTemplates** - Document container with page formatting rules
|
||||
2. **PageTemplates** - Specifications for different page layouts
|
||||
3. **Frames** - Regions where content flows
|
||||
4. **Flowables** - Content elements (paragraphs, tables, images, spacers)
|
||||
5. **Canvas** - Underlying rendering engine (usually hidden)
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
|
||||
from reportlab.lib.styles import getSampleStyleSheet
|
||||
from reportlab.lib.units import inch
|
||||
|
||||
# Create document
|
||||
doc = SimpleDocTemplate("output.pdf", pagesize=letter,
|
||||
rightMargin=72, leftMargin=72,
|
||||
topMargin=72, bottomMargin=18)
|
||||
|
||||
# Create story (list of flowables)
|
||||
story = []
|
||||
styles = getSampleStyleSheet()
|
||||
|
||||
# Add content
|
||||
story.append(Paragraph("Title", styles['Title']))
|
||||
story.append(Spacer(1, 0.2*inch))
|
||||
story.append(Paragraph("Body text here", styles['BodyText']))
|
||||
story.append(PageBreak())
|
||||
|
||||
# Build PDF
|
||||
doc.build(story)
|
||||
```
|
||||
|
||||
## Core Components
|
||||
|
||||
### DocTemplates
|
||||
|
||||
#### SimpleDocTemplate
|
||||
Most common template for standard documents:
|
||||
|
||||
```python
|
||||
doc = SimpleDocTemplate(
|
||||
filename,
|
||||
pagesize=letter,
|
||||
rightMargin=72, # 1 inch = 72 points
|
||||
leftMargin=72,
|
||||
topMargin=72,
|
||||
bottomMargin=18,
|
||||
title=None, # PDF metadata
|
||||
author=None,
|
||||
subject=None
|
||||
)
|
||||
```
|
||||
|
||||
#### BaseDocTemplate (Advanced)
|
||||
For complex documents with multiple page layouts:
|
||||
|
||||
```python
|
||||
from reportlab.platypus import BaseDocTemplate, PageTemplate, Frame
|
||||
from reportlab.lib.pagesizes import letter
|
||||
|
||||
doc = BaseDocTemplate("output.pdf", pagesize=letter)
|
||||
|
||||
# Define frames (content regions)
|
||||
frame1 = Frame(doc.leftMargin, doc.bottomMargin,
|
||||
doc.width/2-6, doc.height, id='col1')
|
||||
frame2 = Frame(doc.leftMargin+doc.width/2+6, doc.bottomMargin,
|
||||
doc.width/2-6, doc.height, id='col2')
|
||||
|
||||
# Create page template
|
||||
template = PageTemplate(id='TwoCol', frames=[frame1, frame2])
|
||||
doc.addPageTemplates([template])
|
||||
|
||||
# Build with story
|
||||
doc.build(story)
|
||||
```
|
||||
|
||||
### Frames
|
||||
|
||||
Frames define regions where content flows:
|
||||
|
||||
```python
|
||||
from reportlab.platypus import Frame
|
||||
|
||||
frame = Frame(
|
||||
x1, y1, # Lower-left corner
|
||||
width, height, # Dimensions
|
||||
leftPadding=6, # Internal padding
|
||||
bottomPadding=6,
|
||||
rightPadding=6,
|
||||
topPadding=6,
|
||||
id=None, # Optional identifier
|
||||
showBoundary=0 # 1 to show frame border (debugging)
|
||||
)
|
||||
```
|
||||
|
||||
### PageTemplates
|
||||
|
||||
Define page layouts with frames and optional functions:
|
||||
|
||||
```python
|
||||
def header_footer(canvas, doc):
|
||||
"""Called on each page for headers/footers"""
|
||||
canvas.saveState()
|
||||
canvas.setFont('Helvetica', 9)
|
||||
canvas.drawString(inch, 0.75*inch, f"Page {doc.page}")
|
||||
canvas.restoreState()
|
||||
|
||||
template = PageTemplate(
|
||||
id='Normal',
|
||||
frames=[frame],
|
||||
onPage=header_footer, # Function called for each page
|
||||
onPageEnd=None,
|
||||
pagesize=letter
|
||||
)
|
||||
```
|
||||
|
||||
## Flowables
|
||||
|
||||
Flowables are content elements that flow through frames.
|
||||
|
||||
### Common Flowables
|
||||
|
||||
```python
|
||||
from reportlab.platypus import (
|
||||
Paragraph, Spacer, PageBreak, FrameBreak,
|
||||
Image, Table, KeepTogether, CondPageBreak
|
||||
)
|
||||
|
||||
# Spacer - vertical whitespace
|
||||
Spacer(width, height)
|
||||
|
||||
# Page break - force new page
|
||||
PageBreak()
|
||||
|
||||
# Frame break - move to next frame
|
||||
FrameBreak()
|
||||
|
||||
# Conditional page break - break if less than N space remaining
|
||||
CondPageBreak(height)
|
||||
|
||||
# Keep together - prevent splitting across pages
|
||||
KeepTogether([flowable1, flowable2, ...])
|
||||
```
|
||||
|
||||
### Paragraph Flowable
|
||||
See `text_and_fonts.md` for detailed Paragraph usage.
|
||||
|
||||
```python
|
||||
from reportlab.platypus import Paragraph
|
||||
from reportlab.lib.styles import ParagraphStyle
|
||||
|
||||
style = ParagraphStyle(
|
||||
'CustomStyle',
|
||||
fontSize=12,
|
||||
leading=14,
|
||||
alignment=0 # 0=left, 1=center, 2=right, 4=justify
|
||||
)
|
||||
|
||||
para = Paragraph("Text with <b>bold</b> and <i>italic</i>", style)
|
||||
story.append(para)
|
||||
```
|
||||
|
||||
### Image Flowable
|
||||
|
||||
```python
|
||||
from reportlab.platypus import Image
|
||||
|
||||
# Auto-size to fit
|
||||
img = Image('photo.jpg')
|
||||
|
||||
# Fixed size
|
||||
img = Image('photo.jpg', width=2*inch, height=2*inch)
|
||||
|
||||
# Maintain aspect ratio with max width
|
||||
img = Image('photo.jpg', width=4*inch, height=3*inch,
|
||||
kind='proportional')
|
||||
|
||||
story.append(img)
|
||||
```
|
||||
|
||||
### Table Flowable
|
||||
See `tables_reference.md` for detailed Table usage.
|
||||
|
||||
```python
|
||||
from reportlab.platypus import Table
|
||||
|
||||
data = [['Header1', 'Header2'],
|
||||
['Row1Col1', 'Row1Col2'],
|
||||
['Row2Col1', 'Row2Col2']]
|
||||
|
||||
table = Table(data, colWidths=[2*inch, 2*inch])
|
||||
story.append(table)
|
||||
```
|
||||
|
||||
## Page Layouts
|
||||
|
||||
### Single Column Document
|
||||
|
||||
```python
|
||||
doc = SimpleDocTemplate("output.pdf", pagesize=letter)
|
||||
story = []
|
||||
# Add flowables...
|
||||
doc.build(story)
|
||||
```
|
||||
|
||||
### Two-Column Layout
|
||||
|
||||
```python
|
||||
from reportlab.platypus import BaseDocTemplate, PageTemplate, Frame
|
||||
|
||||
doc = BaseDocTemplate("output.pdf", pagesize=letter)
|
||||
width, height = letter
|
||||
margin = inch
|
||||
|
||||
# Two side-by-side frames
|
||||
frame1 = Frame(margin, margin, width/2 - 1.5*margin, height - 2*margin, id='col1')
|
||||
frame2 = Frame(width/2 + 0.5*margin, margin, width/2 - 1.5*margin, height - 2*margin, id='col2')
|
||||
|
||||
template = PageTemplate(id='TwoCol', frames=[frame1, frame2])
|
||||
doc.addPageTemplates([template])
|
||||
|
||||
story = []
|
||||
# Content flows left column first, then right column
|
||||
# Add flowables...
|
||||
doc.build(story)
|
||||
```
|
||||
|
||||
### Multiple Page Templates
|
||||
|
||||
```python
|
||||
from reportlab.platypus import NextPageTemplate
|
||||
|
||||
# Define templates
|
||||
cover_template = PageTemplate(id='Cover', frames=[cover_frame])
|
||||
body_template = PageTemplate(id='Body', frames=[body_frame])
|
||||
|
||||
doc.addPageTemplates([cover_template, body_template])
|
||||
|
||||
story = []
|
||||
# Cover page content
|
||||
story.append(Paragraph("Cover", title_style))
|
||||
story.append(NextPageTemplate('Body')) # Switch to body template
|
||||
story.append(PageBreak())
|
||||
|
||||
# Body content
|
||||
story.append(Paragraph("Chapter 1", heading_style))
|
||||
# ...
|
||||
|
||||
doc.build(story)
|
||||
```
|
||||
|
||||
## Headers and Footers
|
||||
|
||||
Headers and footers are added via `onPage` callback functions:
|
||||
|
||||
```python
|
||||
def header_footer(canvas, doc):
|
||||
"""Draw header and footer on each page"""
|
||||
canvas.saveState()
|
||||
|
||||
# Header
|
||||
canvas.setFont('Helvetica-Bold', 12)
|
||||
canvas.drawCentredString(letter[0]/2, letter[1] - 0.5*inch,
|
||||
"Document Title")
|
||||
|
||||
# Footer
|
||||
canvas.setFont('Helvetica', 9)
|
||||
canvas.drawString(inch, 0.75*inch, "Left Footer")
|
||||
canvas.drawRightString(letter[0] - inch, 0.75*inch,
|
||||
f"Page {doc.page}")
|
||||
|
||||
canvas.restoreState()
|
||||
|
||||
# Apply to template
|
||||
template = PageTemplate(id='Normal', frames=[frame], onPage=header_footer)
|
||||
```
|
||||
|
||||
## Table of Contents
|
||||
|
||||
```python
|
||||
from reportlab.platypus import TableOfContents
|
||||
from reportlab.lib.styles import ParagraphStyle
|
||||
|
||||
# Create TOC
|
||||
toc = TableOfContents()
|
||||
toc.levelStyles = [
|
||||
ParagraphStyle(name='TOC1', fontSize=14, leftIndent=0),
|
||||
ParagraphStyle(name='TOC2', fontSize=12, leftIndent=20),
|
||||
]
|
||||
|
||||
story = []
|
||||
story.append(toc)
|
||||
story.append(PageBreak())
|
||||
|
||||
# Add entries
|
||||
story.append(Paragraph("Chapter 1<a name='ch1'/>", heading_style))
|
||||
toc.addEntry(0, "Chapter 1", doc.page, 'ch1')
|
||||
|
||||
# Must call build twice for TOC to populate
|
||||
doc.build(story)
|
||||
```
|
||||
|
||||
## Document Properties
|
||||
|
||||
```python
|
||||
from reportlab.lib.pagesizes import letter, A4
|
||||
from reportlab.lib.units import inch, cm, mm
|
||||
|
||||
# Page sizes
|
||||
letter # US Letter (8.5" x 11")
|
||||
A4 # ISO A4 (210mm x 297mm)
|
||||
landscape(letter) # Rotate to landscape
|
||||
|
||||
# Units
|
||||
inch # 72 points
|
||||
cm # 28.35 points
|
||||
mm # 2.835 points
|
||||
|
||||
# Custom page size
|
||||
custom_size = (6*inch, 9*inch)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use SimpleDocTemplate** for most documents - it handles common layouts
|
||||
2. **Build story list** completely before calling `doc.build(story)`
|
||||
3. **Use Spacer** for vertical spacing instead of empty Paragraphs
|
||||
4. **Group related content** with KeepTogether to prevent awkward page breaks
|
||||
5. **Test page breaks** early with realistic content amounts
|
||||
6. **Use styles consistently** - create style once, reuse throughout document
|
||||
7. **Set showBoundary=1** on Frames during development to visualize layout
|
||||
8. **Headers/footers go in onPage** callback, not in story
|
||||
9. **For long documents**, use BaseDocTemplate with multiple page templates
|
||||
10. **Build TOC documents twice** to properly populate table of contents
|
||||
442
skills/reportlab/references/tables_reference.md
Normal file
442
skills/reportlab/references/tables_reference.md
Normal file
@@ -0,0 +1,442 @@
|
||||
# Tables Reference
|
||||
|
||||
Comprehensive guide to creating and styling tables in ReportLab.
|
||||
|
||||
## Basic Table Creation
|
||||
|
||||
```python
|
||||
from reportlab.platypus import Table, TableStyle
|
||||
from reportlab.lib import colors
|
||||
|
||||
# Simple data (list of lists or tuples)
|
||||
data = [
|
||||
['Header 1', 'Header 2', 'Header 3'],
|
||||
['Row 1, Col 1', 'Row 1, Col 2', 'Row 1, Col 3'],
|
||||
['Row 2, Col 1', 'Row 2, Col 2', 'Row 2, Col 3'],
|
||||
]
|
||||
|
||||
# Create table
|
||||
table = Table(data)
|
||||
|
||||
# Add to story
|
||||
story.append(table)
|
||||
```
|
||||
|
||||
## Table Constructor
|
||||
|
||||
```python
|
||||
table = Table(
|
||||
data, # Required: list of lists/tuples
|
||||
colWidths=None, # List of column widths or single value
|
||||
rowHeights=None, # List of row heights or single value
|
||||
style=None, # TableStyle object
|
||||
splitByRow=1, # Split across pages by rows (not columns)
|
||||
repeatRows=0, # Number of header rows to repeat
|
||||
repeatCols=0, # Number of header columns to repeat
|
||||
rowSplitRange=None, # Tuple (start, end) of splittable rows
|
||||
spaceBefore=None, # Space before table
|
||||
spaceAfter=None, # Space after table
|
||||
cornerRadii=None, # [TL, TR, BL, BR] for rounded corners
|
||||
)
|
||||
```
|
||||
|
||||
### Column Widths
|
||||
|
||||
```python
|
||||
from reportlab.lib.units import inch
|
||||
|
||||
# Equal widths
|
||||
table = Table(data, colWidths=2*inch)
|
||||
|
||||
# Different widths per column
|
||||
table = Table(data, colWidths=[1.5*inch, 2*inch, 1*inch])
|
||||
|
||||
# Auto-calculate widths (default)
|
||||
table = Table(data)
|
||||
|
||||
# Percentage-based (of available width)
|
||||
table = Table(data, colWidths=[None, None, None]) # Equal auto-sizing
|
||||
```
|
||||
|
||||
## Cell Content Types
|
||||
|
||||
### Text and Newlines
|
||||
|
||||
```python
|
||||
# Newlines work in cells
|
||||
data = [
|
||||
['Line 1\nLine 2', 'Single line'],
|
||||
['Another\nmulti-line\ncell', 'Text'],
|
||||
]
|
||||
```
|
||||
|
||||
### Paragraph Objects
|
||||
|
||||
```python
|
||||
from reportlab.platypus import Paragraph
|
||||
from reportlab.lib.styles import getSampleStyleSheet
|
||||
|
||||
styles = getSampleStyleSheet()
|
||||
|
||||
data = [
|
||||
[Paragraph("Formatted <b>bold</b> text", styles['Normal']),
|
||||
Paragraph("More <i>italic</i> text", styles['Normal'])],
|
||||
]
|
||||
|
||||
table = Table(data)
|
||||
```
|
||||
|
||||
### Images
|
||||
|
||||
```python
|
||||
from reportlab.platypus import Image
|
||||
|
||||
data = [
|
||||
['Description', Image('logo.png', width=1*inch, height=1*inch)],
|
||||
['Product', Image('product.jpg', width=2*inch, height=1.5*inch)],
|
||||
]
|
||||
|
||||
table = Table(data)
|
||||
```
|
||||
|
||||
### Nested Tables
|
||||
|
||||
```python
|
||||
# Create inner table
|
||||
inner_data = [['A', 'B'], ['C', 'D']]
|
||||
inner_table = Table(inner_data)
|
||||
|
||||
# Use in outer table
|
||||
outer_data = [
|
||||
['Label', inner_table],
|
||||
['Other', 'Content'],
|
||||
]
|
||||
|
||||
outer_table = Table(outer_data)
|
||||
```
|
||||
|
||||
## TableStyle
|
||||
|
||||
Styles are applied using command lists:
|
||||
|
||||
```python
|
||||
from reportlab.platypus import TableStyle
|
||||
from reportlab.lib import colors
|
||||
|
||||
style = TableStyle([
|
||||
# Command format: ('COMMAND', (startcol, startrow), (endcol, endrow), *args)
|
||||
('GRID', (0, 0), (-1, -1), 1, colors.black), # Grid over all cells
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.grey), # Header background
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), # Header text color
|
||||
])
|
||||
|
||||
table = Table(data)
|
||||
table.setStyle(style)
|
||||
```
|
||||
|
||||
### Cell Coordinate System
|
||||
|
||||
- Columns and rows are 0-indexed: `(col, row)`
|
||||
- Negative indices count from end: `-1` is last column/row
|
||||
- `(0, 0)` is top-left cell
|
||||
- `(-1, -1)` is bottom-right cell
|
||||
|
||||
```python
|
||||
# Examples:
|
||||
(0, 0), (2, 0) # First three cells of header row
|
||||
(0, 1), (-1, -1) # All cells except header
|
||||
(0, 0), (-1, -1) # Entire table
|
||||
```
|
||||
|
||||
## Styling Commands
|
||||
|
||||
### Text Formatting
|
||||
|
||||
```python
|
||||
style = TableStyle([
|
||||
# Font name
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
|
||||
# Font size
|
||||
('FONTSIZE', (0, 0), (-1, -1), 10),
|
||||
|
||||
# Text color
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
||||
('TEXTCOLOR', (0, 1), (-1, -1), colors.black),
|
||||
|
||||
# Combined font command
|
||||
('FONT', (0, 0), (-1, 0), 'Helvetica-Bold', 12), # name, size
|
||||
])
|
||||
```
|
||||
|
||||
### Alignment
|
||||
|
||||
```python
|
||||
style = TableStyle([
|
||||
# Horizontal alignment: LEFT, CENTER, RIGHT, DECIMAL
|
||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||
('ALIGN', (0, 1), (0, -1), 'LEFT'), # First column left
|
||||
('ALIGN', (1, 1), (-1, -1), 'RIGHT'), # Other columns right
|
||||
|
||||
# Vertical alignment: TOP, MIDDLE, BOTTOM
|
||||
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
||||
('VALIGN', (0, 0), (-1, 0), 'BOTTOM'), # Header bottom-aligned
|
||||
])
|
||||
```
|
||||
|
||||
### Cell Padding
|
||||
|
||||
```python
|
||||
style = TableStyle([
|
||||
# Individual padding
|
||||
('LEFTPADDING', (0, 0), (-1, -1), 12),
|
||||
('RIGHTPADDING', (0, 0), (-1, -1), 12),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 6),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
|
||||
|
||||
# Or set all at once by setting each
|
||||
])
|
||||
```
|
||||
|
||||
### Background Colors
|
||||
|
||||
```python
|
||||
style = TableStyle([
|
||||
# Solid background
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.blue),
|
||||
('BACKGROUND', (0, 1), (-1, -1), colors.lightgrey),
|
||||
|
||||
# Alternating row colors
|
||||
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightblue]),
|
||||
|
||||
# Alternating column colors
|
||||
('COLBACKGROUNDS', (0, 0), (-1, -1), [colors.white, colors.lightgrey]),
|
||||
])
|
||||
```
|
||||
|
||||
### Gradient Backgrounds
|
||||
|
||||
```python
|
||||
from reportlab.lib.colors import Color
|
||||
|
||||
style = TableStyle([
|
||||
# Vertical gradient (top to bottom)
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.blue),
|
||||
('VERTICALGRADIENT', (0, 0), (-1, 0),
|
||||
[colors.blue, colors.lightblue]),
|
||||
|
||||
# Horizontal gradient (left to right)
|
||||
('HORIZONTALGRADIENT', (0, 1), (-1, 1),
|
||||
[colors.red, colors.yellow]),
|
||||
])
|
||||
```
|
||||
|
||||
### Lines and Borders
|
||||
|
||||
```python
|
||||
style = TableStyle([
|
||||
# Complete grid
|
||||
('GRID', (0, 0), (-1, -1), 1, colors.black),
|
||||
|
||||
# Box/outline only
|
||||
('BOX', (0, 0), (-1, -1), 2, colors.black),
|
||||
('OUTLINE', (0, 0), (-1, -1), 2, colors.black), # Same as BOX
|
||||
|
||||
# Inner grid only
|
||||
('INNERGRID', (0, 0), (-1, -1), 0.5, colors.grey),
|
||||
|
||||
# Directional lines
|
||||
('LINEABOVE', (0, 0), (-1, 0), 2, colors.black), # Header border
|
||||
('LINEBELOW', (0, 0), (-1, 0), 1, colors.black), # Header bottom
|
||||
('LINEBEFORE', (0, 0), (0, -1), 1, colors.black), # Left border
|
||||
('LINEAFTER', (-1, 0), (-1, -1), 1, colors.black), # Right border
|
||||
|
||||
# Thickness and color
|
||||
('LINEABOVE', (0, 1), (-1, 1), 0.5, colors.grey), # Thin grey line
|
||||
])
|
||||
```
|
||||
|
||||
### Cell Spanning
|
||||
|
||||
```python
|
||||
data = [
|
||||
['Spanning Header', '', ''], # Span will merge these
|
||||
['A', 'B', 'C'],
|
||||
['D', 'E', 'F'],
|
||||
]
|
||||
|
||||
style = TableStyle([
|
||||
# Span 3 columns in first row
|
||||
('SPAN', (0, 0), (2, 0)),
|
||||
|
||||
# Center the spanning cell
|
||||
('ALIGN', (0, 0), (2, 0), 'CENTER'),
|
||||
])
|
||||
|
||||
table = Table(data)
|
||||
table.setStyle(style)
|
||||
```
|
||||
|
||||
**Important:** Cells that are spanned over must contain empty strings `''`.
|
||||
|
||||
### Advanced Spanning Examples
|
||||
|
||||
```python
|
||||
# Span multiple rows and columns
|
||||
data = [
|
||||
['A', 'B', 'B', 'C'],
|
||||
['A', 'D', 'E', 'F'],
|
||||
['A', 'G', 'H', 'I'],
|
||||
]
|
||||
|
||||
style = TableStyle([
|
||||
# Span rows in column 0
|
||||
('SPAN', (0, 0), (0, 2)), # Merge A cells vertically
|
||||
|
||||
# Span columns in row 0
|
||||
('SPAN', (1, 0), (2, 0)), # Merge B cells horizontally
|
||||
|
||||
('GRID', (0, 0), (-1, -1), 1, colors.black),
|
||||
])
|
||||
```
|
||||
|
||||
## Special Commands
|
||||
|
||||
### Rounded Corners
|
||||
|
||||
```python
|
||||
table = Table(data, cornerRadii=[5, 5, 5, 5]) # [TL, TR, BL, BR]
|
||||
|
||||
# Or in style
|
||||
style = TableStyle([
|
||||
('ROUNDEDCORNERS', [10, 10, 0, 0]), # Rounded top corners only
|
||||
])
|
||||
```
|
||||
|
||||
### No Split
|
||||
|
||||
Prevent table from splitting at specific locations:
|
||||
|
||||
```python
|
||||
style = TableStyle([
|
||||
# Don't split between rows 0 and 2
|
||||
('NOSPLIT', (0, 0), (-1, 2)),
|
||||
])
|
||||
```
|
||||
|
||||
### Split-Specific Styling
|
||||
|
||||
Apply styles only to first or last part when table splits:
|
||||
|
||||
```python
|
||||
style = TableStyle([
|
||||
# Style for first part after split
|
||||
('LINEBELOW', (0, 'splitfirst'), (-1, 'splitfirst'), 2, colors.red),
|
||||
|
||||
# Style for last part after split
|
||||
('LINEABOVE', (0, 'splitlast'), (-1, 'splitlast'), 2, colors.blue),
|
||||
])
|
||||
```
|
||||
|
||||
## Repeating Headers
|
||||
|
||||
```python
|
||||
# Repeat first row on each page
|
||||
table = Table(data, repeatRows=1)
|
||||
|
||||
# Repeat first 2 rows
|
||||
table = Table(data, repeatRows=2)
|
||||
```
|
||||
|
||||
## Complete Examples
|
||||
|
||||
### Styled Report Table
|
||||
|
||||
```python
|
||||
from reportlab.platypus import Table, TableStyle
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.units import inch
|
||||
|
||||
data = [
|
||||
['Product', 'Quantity', 'Unit Price', 'Total'],
|
||||
['Widget A', '10', '$5.00', '$50.00'],
|
||||
['Widget B', '5', '$12.00', '$60.00'],
|
||||
['Widget C', '20', '$3.00', '$60.00'],
|
||||
['', '', 'Subtotal:', '$170.00'],
|
||||
]
|
||||
|
||||
table = Table(data, colWidths=[2.5*inch, 1*inch, 1*inch, 1*inch])
|
||||
|
||||
style = TableStyle([
|
||||
# Header row
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.darkblue),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, 0), 12),
|
||||
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
|
||||
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
||||
|
||||
# Data rows
|
||||
('BACKGROUND', (0, 1), (-1, -2), colors.beige),
|
||||
('GRID', (0, 0), (-1, -2), 0.5, colors.grey),
|
||||
('ALIGN', (1, 1), (-1, -1), 'RIGHT'),
|
||||
('ALIGN', (0, 1), (0, -1), 'LEFT'),
|
||||
|
||||
# Total row
|
||||
('BACKGROUND', (0, -1), (-1, -1), colors.lightgrey),
|
||||
('LINEABOVE', (0, -1), (-1, -1), 2, colors.black),
|
||||
('FONTNAME', (2, -1), (-1, -1), 'Helvetica-Bold'),
|
||||
])
|
||||
|
||||
table.setStyle(style)
|
||||
```
|
||||
|
||||
### Alternating Row Colors
|
||||
|
||||
```python
|
||||
data = [
|
||||
['Name', 'Age', 'City'],
|
||||
['Alice', '30', 'New York'],
|
||||
['Bob', '25', 'Boston'],
|
||||
['Charlie', '35', 'Chicago'],
|
||||
['Diana', '28', 'Denver'],
|
||||
]
|
||||
|
||||
table = Table(data, colWidths=[2*inch, 1*inch, 1.5*inch])
|
||||
|
||||
style = TableStyle([
|
||||
# Header
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.darkslategray),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
|
||||
# Alternating rows (zebra striping)
|
||||
('ROWBACKGROUNDS', (0, 1), (-1, -1),
|
||||
[colors.white, colors.lightgrey]),
|
||||
|
||||
# Borders
|
||||
('BOX', (0, 0), (-1, -1), 2, colors.black),
|
||||
('LINEBELOW', (0, 0), (-1, 0), 2, colors.black),
|
||||
|
||||
# Padding
|
||||
('LEFTPADDING', (0, 0), (-1, -1), 12),
|
||||
('RIGHTPADDING', (0, 0), (-1, -1), 12),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 6),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
|
||||
])
|
||||
|
||||
table.setStyle(style)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Set colWidths explicitly** for consistent layout
|
||||
2. **Use repeatRows** for multi-page tables with headers
|
||||
3. **Apply padding** for better readability (especially LEFTPADDING and RIGHTPADDING)
|
||||
4. **Use ROWBACKGROUNDS** for alternating colors instead of styling each row
|
||||
5. **Put empty strings** in cells that will be spanned
|
||||
6. **Test page breaks** early with realistic data amounts
|
||||
7. **Use Paragraph objects** in cells for complex formatted text
|
||||
8. **Set VALIGN to MIDDLE** for better appearance with varying row heights
|
||||
9. **Keep tables simple** - complex nested tables are hard to maintain
|
||||
10. **Use consistent styling** - define once, apply to all tables
|
||||
394
skills/reportlab/references/text_and_fonts.md
Normal file
394
skills/reportlab/references/text_and_fonts.md
Normal file
@@ -0,0 +1,394 @@
|
||||
# Text and Fonts Reference
|
||||
|
||||
Comprehensive guide to text formatting, paragraph styles, and font handling in ReportLab.
|
||||
|
||||
## Text Encoding
|
||||
|
||||
**IMPORTANT:** All text input should be UTF-8 encoded or Python Unicode objects (since ReportLab 2.0).
|
||||
|
||||
```python
|
||||
# Correct - UTF-8 strings
|
||||
text = "Hello 世界 مرحبا"
|
||||
para = Paragraph(text, style)
|
||||
|
||||
# For legacy data, convert first
|
||||
import codecs
|
||||
decoded_text = codecs.decode(legacy_bytes, 'latin-1')
|
||||
```
|
||||
|
||||
## Paragraph Styles
|
||||
|
||||
### Creating Styles
|
||||
|
||||
```python
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT, TA_JUSTIFY
|
||||
from reportlab.lib.colors import black, blue, red
|
||||
from reportlab.lib.units import inch
|
||||
|
||||
# Get default styles
|
||||
styles = getSampleStyleSheet()
|
||||
normal = styles['Normal']
|
||||
heading = styles['Heading1']
|
||||
|
||||
# Create custom style
|
||||
custom_style = ParagraphStyle(
|
||||
'CustomStyle',
|
||||
parent=normal, # Inherit from another style
|
||||
|
||||
# Font properties
|
||||
fontName='Helvetica',
|
||||
fontSize=12,
|
||||
leading=14, # Line spacing (should be > fontSize)
|
||||
|
||||
# Indentation (in points)
|
||||
leftIndent=0,
|
||||
rightIndent=0,
|
||||
firstLineIndent=0, # Positive = indent, negative = outdent
|
||||
|
||||
# Spacing
|
||||
spaceBefore=0,
|
||||
spaceAfter=0,
|
||||
|
||||
# Alignment
|
||||
alignment=TA_LEFT, # TA_LEFT, TA_CENTER, TA_RIGHT, TA_JUSTIFY
|
||||
|
||||
# Colors
|
||||
textColor=black,
|
||||
backColor=None, # Background color
|
||||
|
||||
# Borders
|
||||
borderWidth=0,
|
||||
borderColor=None,
|
||||
borderPadding=0,
|
||||
borderRadius=None,
|
||||
|
||||
# Bullets
|
||||
bulletFontName='Helvetica',
|
||||
bulletFontSize=12,
|
||||
bulletIndent=0,
|
||||
bulletText=None, # Text for bullets (e.g., '•')
|
||||
|
||||
# Advanced
|
||||
wordWrap=None, # 'CJK' for Asian languages
|
||||
allowWidows=1, # Allow widow lines
|
||||
allowOrphans=0, # Prevent orphan lines
|
||||
endDots=None, # Trailing dots for TOC entries
|
||||
splitLongWords=1,
|
||||
hyphenationLang=None, # 'en_US', etc. (requires pyphen)
|
||||
)
|
||||
|
||||
# Add to stylesheet
|
||||
styles.add(custom_style)
|
||||
```
|
||||
|
||||
### Built-in Styles
|
||||
|
||||
```python
|
||||
styles = getSampleStyleSheet()
|
||||
|
||||
# Common styles
|
||||
styles['Normal'] # Body text
|
||||
styles['BodyText'] # Similar to Normal
|
||||
styles['Heading1'] # Top-level heading
|
||||
styles['Heading2'] # Second-level heading
|
||||
styles['Heading3'] # Third-level heading
|
||||
styles['Title'] # Document title
|
||||
styles['Bullet'] # Bulleted list items
|
||||
styles['Definition'] # Definition text
|
||||
styles['Code'] # Code samples
|
||||
```
|
||||
|
||||
## Paragraph Formatting
|
||||
|
||||
### Basic Paragraph
|
||||
|
||||
```python
|
||||
from reportlab.platypus import Paragraph
|
||||
|
||||
para = Paragraph("This is a paragraph.", style)
|
||||
story.append(para)
|
||||
```
|
||||
|
||||
### Inline Formatting Tags
|
||||
|
||||
```python
|
||||
text = """
|
||||
<b>Bold text</b>
|
||||
<i>Italic text</i>
|
||||
<u>Underlined text</u>
|
||||
<strike>Strikethrough text</strike>
|
||||
<strong>Strong (bold) text</strong>
|
||||
"""
|
||||
|
||||
para = Paragraph(text, normal_style)
|
||||
```
|
||||
|
||||
### Font Control
|
||||
|
||||
```python
|
||||
text = """
|
||||
<font face="Courier" size="14" color="blue">
|
||||
Custom font, size, and color
|
||||
</font>
|
||||
|
||||
<font color="#FF0000">Hex color codes work too</font>
|
||||
"""
|
||||
|
||||
para = Paragraph(text, normal_style)
|
||||
```
|
||||
|
||||
### Superscripts and Subscripts
|
||||
|
||||
```python
|
||||
text = """
|
||||
H<sub>2</sub>O is water.
|
||||
E=mc<super>2</super> or E=mc<sup>2</sup>
|
||||
X<sub><i>i</i></sub> for subscripted variables
|
||||
"""
|
||||
|
||||
para = Paragraph(text, normal_style)
|
||||
```
|
||||
|
||||
### Greek Letters
|
||||
|
||||
```python
|
||||
text = """
|
||||
<greek>alpha</greek>, <greek>beta</greek>, <greek>gamma</greek>
|
||||
<greek>epsilon</greek>, <greek>pi</greek>, <greek>omega</greek>
|
||||
"""
|
||||
|
||||
para = Paragraph(text, normal_style)
|
||||
```
|
||||
|
||||
### Links
|
||||
|
||||
```python
|
||||
# External link
|
||||
text = '<link href="https://example.com" color="blue">Click here</link>'
|
||||
|
||||
# Internal link (to bookmark)
|
||||
text = '<link href="#section1" color="blue">Go to Section 1</link>'
|
||||
|
||||
# Anchor for internal links
|
||||
text = '<a name="section1"/>Section 1 Heading'
|
||||
|
||||
para = Paragraph(text, normal_style)
|
||||
```
|
||||
|
||||
### Inline Images
|
||||
|
||||
```python
|
||||
text = """
|
||||
Here is an inline image: <img src="icon.png" width="12" height="12" valign="middle"/>
|
||||
"""
|
||||
|
||||
para = Paragraph(text, normal_style)
|
||||
```
|
||||
|
||||
### Line Breaks
|
||||
|
||||
```python
|
||||
text = """
|
||||
First line<br/>
|
||||
Second line<br/>
|
||||
Third line
|
||||
"""
|
||||
|
||||
para = Paragraph(text, normal_style)
|
||||
```
|
||||
|
||||
## Font Handling
|
||||
|
||||
### Standard Fonts
|
||||
|
||||
ReportLab includes 14 standard PDF fonts (no embedding needed):
|
||||
|
||||
```python
|
||||
# Helvetica family
|
||||
'Helvetica'
|
||||
'Helvetica-Bold'
|
||||
'Helvetica-Oblique'
|
||||
'Helvetica-BoldOblique'
|
||||
|
||||
# Times family
|
||||
'Times-Roman'
|
||||
'Times-Bold'
|
||||
'Times-Italic'
|
||||
'Times-BoldItalic'
|
||||
|
||||
# Courier family
|
||||
'Courier'
|
||||
'Courier-Bold'
|
||||
'Courier-Oblique'
|
||||
'Courier-BoldOblique'
|
||||
|
||||
# Symbol and Dingbats
|
||||
'Symbol'
|
||||
'ZapfDingbats'
|
||||
```
|
||||
|
||||
### TrueType Fonts
|
||||
|
||||
```python
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
|
||||
# Register single font
|
||||
pdfmetrics.registerFont(TTFont('CustomFont', 'CustomFont.ttf'))
|
||||
|
||||
# Use in Canvas
|
||||
canvas.setFont('CustomFont', 12)
|
||||
|
||||
# Use in Paragraph style
|
||||
style = ParagraphStyle('Custom', fontName='CustomFont', fontSize=12)
|
||||
```
|
||||
|
||||
### Font Families
|
||||
|
||||
Register related fonts as a family for bold/italic support:
|
||||
|
||||
```python
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.lib.fonts import addMapping
|
||||
|
||||
# Register fonts
|
||||
pdfmetrics.registerFont(TTFont('Vera', 'Vera.ttf'))
|
||||
pdfmetrics.registerFont(TTFont('VeraBd', 'VeraBd.ttf'))
|
||||
pdfmetrics.registerFont(TTFont('VeraIt', 'VeraIt.ttf'))
|
||||
pdfmetrics.registerFont(TTFont('VeraBI', 'VeraBI.ttf'))
|
||||
|
||||
# Map family (normal, bold, italic, bold-italic)
|
||||
addMapping('Vera', 0, 0, 'Vera') # normal
|
||||
addMapping('Vera', 1, 0, 'VeraBd') # bold
|
||||
addMapping('Vera', 0, 1, 'VeraIt') # italic
|
||||
addMapping('Vera', 1, 1, 'VeraBI') # bold-italic
|
||||
|
||||
# Now <b> and <i> tags work with this family
|
||||
style = ParagraphStyle('VeraStyle', fontName='Vera', fontSize=12)
|
||||
para = Paragraph("Normal <b>Bold</b> <i>Italic</i> <b><i>Both</i></b>", style)
|
||||
```
|
||||
|
||||
### Font Search Paths
|
||||
|
||||
```python
|
||||
from reportlab.pdfbase.ttfonts import TTFSearchPath
|
||||
|
||||
# Add custom font directory
|
||||
TTFSearchPath.append('/path/to/fonts/')
|
||||
|
||||
# Now fonts in this directory can be found by name
|
||||
pdfmetrics.registerFont(TTFont('MyFont', 'MyFont.ttf'))
|
||||
```
|
||||
|
||||
### Asian Language Support
|
||||
|
||||
#### Using Adobe Language Packs (no embedding)
|
||||
|
||||
```python
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
|
||||
|
||||
# Register CID fonts
|
||||
pdfmetrics.registerFont(UnicodeCIDFont('HeiseiMin-W3')) # Japanese
|
||||
pdfmetrics.registerFont(UnicodeCIDFont('STSong-Light')) # Chinese (Simplified)
|
||||
pdfmetrics.registerFont(UnicodeCIDFont('MSung-Light')) # Chinese (Traditional)
|
||||
pdfmetrics.registerFont(UnicodeCIDFont('HYSMyeongJo-Medium')) # Korean
|
||||
|
||||
# Use in styles
|
||||
style = ParagraphStyle('Japanese', fontName='HeiseiMin-W3', fontSize=12)
|
||||
para = Paragraph("日本語テキスト", style)
|
||||
```
|
||||
|
||||
#### Using TrueType Fonts with Asian Characters
|
||||
|
||||
```python
|
||||
# Register TrueType font with full Unicode support
|
||||
pdfmetrics.registerFont(TTFont('SimSun', 'simsun.ttc'))
|
||||
|
||||
style = ParagraphStyle('Chinese', fontName='SimSun', fontSize=12, wordWrap='CJK')
|
||||
para = Paragraph("中文文本", style)
|
||||
```
|
||||
|
||||
Note: Set `wordWrap='CJK'` for proper line breaking in Asian languages.
|
||||
|
||||
## Numbering and Sequences
|
||||
|
||||
Auto-numbering using `<seq>` tags:
|
||||
|
||||
```python
|
||||
# Simple numbering
|
||||
text = "<seq id='chapter'/> Introduction" # Outputs: 1 Introduction
|
||||
text = "<seq id='chapter'/> Methods" # Outputs: 2 Methods
|
||||
|
||||
# Reset counter
|
||||
text = "<seq id='figure' reset='yes'/>"
|
||||
|
||||
# Formatting templates
|
||||
text = "Figure <seq template='%(chapter)s-%(figure+)s' id='figure'/>"
|
||||
# Outputs: Figure 1-1, Figure 1-2, etc.
|
||||
|
||||
# Multi-level numbering
|
||||
text = "Section <seq template='%(chapter)s.%(section+)s' id='section'/>"
|
||||
```
|
||||
|
||||
## Bullets and Lists
|
||||
|
||||
### Using Bullet Style
|
||||
|
||||
```python
|
||||
bullet_style = ParagraphStyle(
|
||||
'Bullet',
|
||||
parent=normal_style,
|
||||
leftIndent=20,
|
||||
bulletIndent=10,
|
||||
bulletText='•', # Unicode bullet
|
||||
bulletFontName='Helvetica',
|
||||
)
|
||||
|
||||
story.append(Paragraph("First item", bullet_style))
|
||||
story.append(Paragraph("Second item", bullet_style))
|
||||
story.append(Paragraph("Third item", bullet_style))
|
||||
```
|
||||
|
||||
### Custom Bullet Characters
|
||||
|
||||
```python
|
||||
# Different bullet styles
|
||||
bulletText='•' # Filled circle
|
||||
bulletText='◦' # Open circle
|
||||
bulletText='▪' # Square
|
||||
bulletText='▸' # Triangle
|
||||
bulletText='→' # Arrow
|
||||
bulletText='1.' # Numbers
|
||||
bulletText='a)' # Letters
|
||||
```
|
||||
|
||||
## Text Measurement
|
||||
|
||||
```python
|
||||
from reportlab.pdfbase.pdfmetrics import stringWidth
|
||||
|
||||
# Measure string width
|
||||
width = stringWidth("Hello World", "Helvetica", 12)
|
||||
|
||||
# Check if text fits in available width
|
||||
max_width = 200
|
||||
if stringWidth(text, font_name, font_size) > max_width:
|
||||
# Text is too wide
|
||||
pass
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use UTF-8** for text input
|
||||
2. **Set leading > fontSize** for readability (typically fontSize + 2)
|
||||
3. **Register font families** for proper bold/italic support
|
||||
4. **Escape HTML** if displaying user content: use `<` for < and `>` for >
|
||||
5. **Use getSampleStyleSheet()** as a starting point, don't create all styles from scratch
|
||||
6. **Test Asian fonts** early if supporting multi-language content
|
||||
7. **Set wordWrap='CJK'** for Chinese/Japanese/Korean text
|
||||
8. **Use stringWidth()** to check if text fits before rendering
|
||||
9. **Define styles once** at document start, reuse throughout
|
||||
10. **Enable hyphenation** for justified text: `hyphenationLang='en_US'` (requires pyphen package)
|
||||
229
skills/reportlab/scripts/quick_document.py
Normal file
229
skills/reportlab/scripts/quick_document.py
Normal file
@@ -0,0 +1,229 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Quick Document Generator - Helper for creating simple ReportLab documents
|
||||
|
||||
This script provides utility functions for quickly creating common document types
|
||||
without writing boilerplate code.
|
||||
"""
|
||||
|
||||
from reportlab.lib.pagesizes import letter, A4
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.lib import colors
|
||||
from reportlab.platypus import (
|
||||
SimpleDocTemplate, Paragraph, Spacer, PageBreak,
|
||||
Table, TableStyle, Image, KeepTogether
|
||||
)
|
||||
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT, TA_JUSTIFY
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def create_simple_document(filename, title, author="", content_blocks=None, pagesize=letter):
|
||||
"""
|
||||
Create a simple document with title and content blocks.
|
||||
|
||||
Args:
|
||||
filename: Output PDF filename
|
||||
title: Document title
|
||||
author: Document author (optional)
|
||||
content_blocks: List of dicts with 'type' and 'content' keys
|
||||
type can be: 'heading', 'paragraph', 'bullet', 'space'
|
||||
pagesize: Page size (default: letter)
|
||||
|
||||
Example content_blocks:
|
||||
[
|
||||
{'type': 'heading', 'content': 'Introduction'},
|
||||
{'type': 'paragraph', 'content': 'This is a paragraph.'},
|
||||
{'type': 'bullet', 'content': 'Bullet point item'},
|
||||
{'type': 'space', 'height': 0.2}, # height in inches
|
||||
]
|
||||
"""
|
||||
if content_blocks is None:
|
||||
content_blocks = []
|
||||
|
||||
# Create document
|
||||
doc = SimpleDocTemplate(
|
||||
filename,
|
||||
pagesize=pagesize,
|
||||
rightMargin=72,
|
||||
leftMargin=72,
|
||||
topMargin=72,
|
||||
bottomMargin=18,
|
||||
title=title,
|
||||
author=author
|
||||
)
|
||||
|
||||
# Get styles
|
||||
styles = getSampleStyleSheet()
|
||||
story = []
|
||||
|
||||
# Add title
|
||||
story.append(Paragraph(title, styles['Title']))
|
||||
story.append(Spacer(1, 0.3*inch))
|
||||
|
||||
# Process content blocks
|
||||
for block in content_blocks:
|
||||
block_type = block.get('type', 'paragraph')
|
||||
content = block.get('content', '')
|
||||
|
||||
if block_type == 'heading':
|
||||
story.append(Paragraph(content, styles['Heading1']))
|
||||
story.append(Spacer(1, 0.1*inch))
|
||||
|
||||
elif block_type == 'heading2':
|
||||
story.append(Paragraph(content, styles['Heading2']))
|
||||
story.append(Spacer(1, 0.1*inch))
|
||||
|
||||
elif block_type == 'paragraph':
|
||||
story.append(Paragraph(content, styles['BodyText']))
|
||||
story.append(Spacer(1, 0.1*inch))
|
||||
|
||||
elif block_type == 'bullet':
|
||||
story.append(Paragraph(content, styles['Bullet']))
|
||||
|
||||
elif block_type == 'space':
|
||||
height = block.get('height', 0.2)
|
||||
story.append(Spacer(1, height*inch))
|
||||
|
||||
elif block_type == 'pagebreak':
|
||||
story.append(PageBreak())
|
||||
|
||||
# Build PDF
|
||||
doc.build(story)
|
||||
return filename
|
||||
|
||||
|
||||
def create_styled_table(data, col_widths=None, style_name='default'):
|
||||
"""
|
||||
Create a styled table with common styling presets.
|
||||
|
||||
Args:
|
||||
data: List of lists containing table data
|
||||
col_widths: List of column widths (None for auto)
|
||||
style_name: 'default', 'striped', 'minimal', 'report'
|
||||
|
||||
Returns:
|
||||
Table object ready to add to story
|
||||
"""
|
||||
table = Table(data, colWidths=col_widths)
|
||||
|
||||
if style_name == 'striped':
|
||||
style = TableStyle([
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.darkblue),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, 0), 12),
|
||||
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
||||
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightgrey]),
|
||||
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
|
||||
])
|
||||
|
||||
elif style_name == 'minimal':
|
||||
style = TableStyle([
|
||||
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('LINEABOVE', (0, 0), (-1, 0), 2, colors.black),
|
||||
('LINEBELOW', (0, 0), (-1, 0), 1, colors.black),
|
||||
('LINEBELOW', (0, -1), (-1, -1), 2, colors.black),
|
||||
])
|
||||
|
||||
elif style_name == 'report':
|
||||
style = TableStyle([
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.black),
|
||||
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, 0), 11),
|
||||
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
|
||||
('GRID', (0, 0), (-1, -1), 1, colors.grey),
|
||||
('LEFTPADDING', (0, 0), (-1, -1), 12),
|
||||
('RIGHTPADDING', (0, 0), (-1, -1), 12),
|
||||
])
|
||||
|
||||
else: # default
|
||||
style = TableStyle([
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, 0), 12),
|
||||
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
||||
('BACKGROUND', (0, 1), (-1, -1), colors.white),
|
||||
('GRID', (0, 0), (-1, -1), 1, colors.black),
|
||||
])
|
||||
|
||||
table.setStyle(style)
|
||||
return table
|
||||
|
||||
|
||||
def add_header_footer(canvas, doc, header_text="", footer_text=""):
|
||||
"""
|
||||
Callback function to add headers and footers to each page.
|
||||
|
||||
Usage:
|
||||
from functools import partial
|
||||
callback = partial(add_header_footer, header_text="My Document", footer_text="Confidential")
|
||||
template = PageTemplate(id='normal', frames=[frame], onPage=callback)
|
||||
"""
|
||||
canvas.saveState()
|
||||
|
||||
# Header
|
||||
if header_text:
|
||||
canvas.setFont('Helvetica', 9)
|
||||
canvas.drawString(inch, doc.pagesize[1] - 0.5*inch, header_text)
|
||||
|
||||
# Footer
|
||||
if footer_text:
|
||||
canvas.setFont('Helvetica', 9)
|
||||
canvas.drawString(inch, 0.5*inch, footer_text)
|
||||
|
||||
# Page number
|
||||
canvas.drawRightString(doc.pagesize[0] - inch, 0.5*inch, f"Page {doc.page}")
|
||||
|
||||
canvas.restoreState()
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == "__main__":
|
||||
# Example 1: Simple document
|
||||
content = [
|
||||
{'type': 'heading', 'content': 'Introduction'},
|
||||
{'type': 'paragraph', 'content': 'This is a sample paragraph with some text.'},
|
||||
{'type': 'space', 'height': 0.2},
|
||||
{'type': 'heading', 'content': 'Main Content'},
|
||||
{'type': 'paragraph', 'content': 'More content here with <b>bold</b> and <i>italic</i> text.'},
|
||||
{'type': 'bullet', 'content': 'First bullet point'},
|
||||
{'type': 'bullet', 'content': 'Second bullet point'},
|
||||
]
|
||||
|
||||
create_simple_document(
|
||||
"example_document.pdf",
|
||||
"Sample Document",
|
||||
author="John Doe",
|
||||
content_blocks=content
|
||||
)
|
||||
|
||||
print("Created: example_document.pdf")
|
||||
|
||||
# Example 2: Document with styled table
|
||||
doc = SimpleDocTemplate("table_example.pdf", pagesize=letter)
|
||||
story = []
|
||||
styles = getSampleStyleSheet()
|
||||
|
||||
story.append(Paragraph("Sales Report", styles['Title']))
|
||||
story.append(Spacer(1, 0.3*inch))
|
||||
|
||||
# Create table
|
||||
data = [
|
||||
['Product', 'Q1', 'Q2', 'Q3', 'Q4'],
|
||||
['Widget A', '100', '150', '130', '180'],
|
||||
['Widget B', '80', '120', '110', '160'],
|
||||
['Widget C', '90', '110', '100', '140'],
|
||||
]
|
||||
|
||||
table = create_styled_table(data, col_widths=[2*inch, 1*inch, 1*inch, 1*inch, 1*inch], style_name='striped')
|
||||
story.append(table)
|
||||
|
||||
doc.build(story)
|
||||
print("Created: table_example.pdf")
|
||||
Reference in New Issue
Block a user