WeasyPrint converts HTML + CSS to PDF. pip install weasyprint. Basic: from weasyprint import HTML; HTML(string="<h1>Hi</h1>").write_pdf("out.pdf"). Bytes: pdf = HTML(string=html).write_pdf(). From file: HTML(filename="doc.html").write_pdf("out.pdf"). From URL: HTML(url="https://example.com").write_pdf(). Base URL: HTML(string=html, base_url="/path/to/assets/").write_pdf(). CSS: from weasyprint import CSS; HTML(string=html).write_pdf(stylesheets=[CSS(string="body{font-size:12pt}")]). CSS file: CSS(filename="style.css"). @page { size: A4; margin: 2cm }. Landscape: @page { size: A4 landscape }. Page numbers: @page { @bottom-center { content: counter(page) } }. Running elements: @page { @top-left { content: element(header) } }. Fonts: from weasyprint.text.fonts import FontConfiguration; font_config = FontConfiguration(); CSS(string="@font-face {...}", font_config=font_config). Custom fetcher: HTML(string=html, url_fetcher=my_fetcher). presentational_hints=True — respect style=, align=, border= HTML attributes. Zoom: HTML(string=html).write_pdf(zoom=1.5). Attachments: write_pdf(attachments=[Attachment(url="...")]). Metadata: write_pdf(uncompressed_pdf=True). Claude Code generates WeasyPrint Jinja2 invoice renderers, CSS-styled reports, and HTML-to-PDF pipelines.
CLAUDE.md for WeasyPrint
## WeasyPrint Stack
- Version: weasyprint >= 60 | pip install weasyprint
- Render: HTML(string=html_str, base_url=".").write_pdf() → bytes
- CSS: pass stylesheets=[CSS(string="...")] or CSS(filename="style.css")
- Page rules: @page { size: A4; margin: 1.5cm } in stylesheet
- Fonts: FontConfiguration() passed to CSS(..., font_config=fc)
- Jinja2: render_template() → html_str → HTML(string=...).write_pdf()
WeasyPrint PDF Generation Pipeline
# app/pdf_weasy.py — WeasyPrint invoice, report, and template PDF generators
from __future__ import annotations
import io
from dataclasses import dataclass, field
from datetime import date
from pathlib import Path
from typing import Any
from weasyprint import CSS, HTML
from weasyprint.text.fonts import FontConfiguration
# ─────────────────────────────────────────────────────────────────────────────
# 1. Core render helpers
# ─────────────────────────────────────────────────────────────────────────────
BASE_CSS = """
@page {
size: A4;
margin: 2cm 1.5cm 2.5cm 1.5cm;
@bottom-center {
content: "Page " counter(page) " of " counter(pages);
font-size: 8pt;
color: #888;
}
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 10pt;
color: #333;
line-height: 1.4;
}
h1 { font-size: 22pt; color: #222; margin-bottom: 0.4cm; }
h2 { font-size: 13pt; color: #444; margin: 0.5cm 0 0.2cm; border-bottom: 1px solid #ddd; padding-bottom: 2pt; }
h3 { font-size: 11pt; color: #555; margin: 0.3cm 0 0.15cm; }
table { width: 100%; border-collapse: collapse; margin: 0.3cm 0; }
th { background: #333; color: #fff; padding: 5pt 6pt; text-align: left; font-size: 9pt; }
td { padding: 4pt 6pt; font-size: 9pt; border-bottom: 1px solid #eee; }
tr:nth-child(even) td { background: #f9f9f9; }
.right { text-align: right; }
.center { text-align: center; }
.label { color: #888; font-size: 8pt; text-transform: uppercase; letter-spacing: 0.05em; }
.total-row td { font-weight: bold; border-top: 2px solid #333; }
.amount { font-variant-numeric: tabular-nums; }
"""
def render_html(
html: str,
base_url: str | None = None,
extra_css: str | None = None,
presentational_hints: bool = True,
) -> bytes:
"""
Render an HTML string to PDF bytes using WeasyPrint.
base_url: resolves relative links/images (use str(Path.cwd()) for local files).
extra_css: additional CSS to layer on top of BASE_CSS.
"""
stylesheets = [CSS(string=BASE_CSS)]
if extra_css:
stylesheets.append(CSS(string=extra_css))
return HTML(
string=html,
base_url=base_url or str(Path.cwd()),
presentational_hints=presentational_hints,
).write_pdf(stylesheets=stylesheets)
def render_file(
html_path: str | Path,
extra_css: str | None = None,
) -> bytes:
"""Render an HTML file to PDF bytes."""
p = Path(html_path)
return render_html(
p.read_text(encoding="utf-8"),
base_url=str(p.parent),
extra_css=extra_css,
)
# ─────────────────────────────────────────────────────────────────────────────
# 2. Invoice generator (HTML template → PDF)
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class LineItem:
description: str
quantity: float
unit_price: float
@property
def total(self) -> float:
return self.quantity * self.unit_price
def _invoice_html(
invoice_number: str,
invoice_date: date,
due_date: date,
client: dict[str, str],
items: list[LineItem],
tax_rate: float,
company: str,
notes: str,
) -> str:
subtotal = sum(i.total for i in items)
tax = subtotal * tax_rate
total = subtotal + tax
rows_html = "".join(
f"""<tr>
<td>{item.description}</td>
<td class="right">{item.quantity:g}</td>
<td class="right amount">${item.unit_price:,.2f}</td>
<td class="right amount">${item.total:,.2f}</td>
</tr>"""
for item in items
)
notes_html = f"<h2>Notes</h2><p>{notes}</p>" if notes else ""
return f"""<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Invoice {invoice_number}</title></head>
<body>
<table style="margin-bottom: 0.6cm">
<tr>
<td style="width:60%"><h1>INVOICE</h1></td>
<td class="right label" style="vertical-align:bottom">
<strong>{company}</strong>
</td>
</tr>
</table>
<table style="margin-bottom: 0.5cm; width: 100%">
<tr>
<td>
<span class="label">Invoice #</span><br>{invoice_number}<br>
<span class="label">Date</span><br>{invoice_date:%B %d, %Y}<br>
<span class="label">Due</span><br>{due_date:%B %d, %Y}
</td>
<td class="right">
<span class="label">Bill To</span><br>
<strong>{client.get('name','')}</strong><br>
{client.get('address','')}<br>
{client.get('city','')}
</td>
</tr>
</table>
<table>
<thead>
<tr>
<th style="width:52%">Description</th>
<th class="right" style="width:10%">Qty</th>
<th class="right" style="width:18%">Unit Price</th>
<th class="right" style="width:20%">Amount</th>
</tr>
</thead>
<tbody>
{rows_html}
</tbody>
</table>
<table style="margin-left: auto; width: 40%; margin-top: 0.3cm">
<tr><td>Subtotal</td><td class="right amount">${subtotal:,.2f}</td></tr>
<tr><td>Tax ({tax_rate:.0%})</td><td class="right amount">${tax:,.2f}</td></tr>
<tr class="total-row"><td>Total</td><td class="right amount">${total:,.2f}</td></tr>
</table>
{notes_html}
</body>
</html>"""
def generate_invoice(
invoice_number: str,
invoice_date: date,
due_date: date,
client: dict[str, str],
items: list[LineItem],
tax_rate: float = 0.10,
company: str = "My Company",
notes: str = "",
) -> bytes:
"""Generate a professional invoice PDF from structured data."""
html = _invoice_html(invoice_number, invoice_date, due_date,
client, items, tax_rate, company, notes)
return render_html(html)
# ─────────────────────────────────────────────────────────────────────────────
# 3. Table report generator
# ─────────────────────────────────────────────────────────────────────────────
def generate_table_report(
title: str,
columns: list[str],
rows: list[list[Any]],
subtitle: str = "",
landscape: bool = False,
company: str = "My Company",
) -> bytes:
"""Generate a multi-page table report PDF."""
orientation_css = "@page { size: A4 landscape; }" if landscape else ""
rows_html = "".join(
"<tr>" + "".join(f"<td>{cell}</td>" for cell in row) + "</tr>"
for row in rows
)
headers_html = "".join(f"<th>{col}</th>" for col in columns)
subtitle_html = f"<p style='color:#888; font-size:9pt'>{subtitle}</p>" if subtitle else ""
date_html = f"<p class='label' style='margin-bottom:0.4cm'>Generated: {date.today():%B %d, %Y} — {company}</p>"
html = f"""<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>{title}</title></head>
<body>
<h1>{title}</h1>
{subtitle_html}
{date_html}
<table>
<thead><tr>{headers_html}</tr></thead>
<tbody>{rows_html}</tbody>
</table>
<p style="margin-top:0.3cm; font-size:8pt; color:#aaa">{len(rows)} records</p>
</body>
</html>"""
return render_html(html, extra_css=orientation_css)
# ─────────────────────────────────────────────────────────────────────────────
# 4. Jinja2 integration helper
# ─────────────────────────────────────────────────────────────────────────────
def render_jinja2_template(
template_dir: str | Path,
template_name: str,
context: dict[str, Any],
extra_css: str | None = None,
) -> bytes:
"""
Render a Jinja2 HTML template to PDF.
Requires: pip install jinja2
Usage:
pdf = render_jinja2_template("templates", "invoice.html", {"title": "..."})
"""
try:
from jinja2 import Environment, FileSystemLoader
except ImportError as e:
raise ImportError("pip install jinja2") from e
env = Environment(
loader=FileSystemLoader(str(template_dir)),
autoescape=True,
)
html = env.get_template(template_name).render(**context)
return render_html(html, base_url=str(Path(template_dir).resolve()), extra_css=extra_css)
# ─────────────────────────────────────────────────────────────────────────────
# 5. FastAPI / Flask response helpers
# ─────────────────────────────────────────────────────────────────────────────
FASTAPI_EXAMPLE = '''
from fastapi import FastAPI
from fastapi.responses import Response
from app.pdf_weasy import generate_invoice, LineItem
from datetime import date
app = FastAPI()
@app.get("/invoice/{invoice_id}")
def invoice_pdf(invoice_id: int):
items = [
LineItem("Consulting services", 8, 150.00),
LineItem("Code review", 2, 95.00),
]
pdf_bytes = generate_invoice(
invoice_number=f"INV-{invoice_id:04d}",
invoice_date=date.today(),
due_date=date.today().replace(day=28),
client={"name": "Acme Corp", "address": "123 Main St", "city": "Springfield"},
items=items,
)
return Response(
pdf_bytes,
media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename=invoice-{invoice_id}.pdf"},
)
'''
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
from pathlib import Path
print("=== Generating invoice.pdf ===")
items = [
LineItem("Python consulting", 10, 200.00),
LineItem("Code review & refactoring", 4, 150.00),
LineItem("Documentation", 2, 80.00),
]
pdf = generate_invoice(
invoice_number="INV-0042",
invoice_date=date(2024, 1, 15),
due_date=date(2024, 2, 15),
client={
"name": "Acme Corporation",
"address": "456 Commerce Blvd",
"city": "San Francisco, CA 94102",
},
items=items,
tax_rate=0.085,
company="Claude Skills LLC",
notes="Thank you for your business! Payment due within 30 days.",
)
Path("/tmp/weasy_invoice.pdf").write_bytes(pdf)
print(f" Written {len(pdf):,} bytes")
print("\n=== Generating report.pdf ===")
report = generate_table_report(
"Q4 Sales Performance",
columns=["Name", "Department", "Sales", "Target", "% Target"],
rows=[
["Alice Johnson", "West", "$142,000", "$130,000", "109%"],
["Bob Martinez", "East", "$98,500", "$120,000", "82%"],
["Carol Williams", "Central", "$175,200", "$150,000", "117%"],
["David Chen", "North", "$61,000", "$90,000", "68%"],
],
subtitle="Annual performance review — Confidential",
company="Sales Analytics Inc",
)
Path("/tmp/weasy_report.pdf").write_bytes(report)
print(f" Written {len(report):,} bytes")
For the fpdf2 alternative — fpdf2 uses an immediate-mode drawing API (cell, multi_cell, set_font) giving precise pixel-level control but requiring manual layout math; WeasyPrint renders HTML+CSS so you can use flexbox, tables, @page margin boxes, and :nth-child alternating rows — the HTML/CSS approach is far faster to iterate on and produces print-accurate results matching browser print output. For the pdfkit / wkhtmltopdf alternative — pdfkit wraps the wkhtmltopdf CLI binary (Chromium-era WebKit) while WeasyPrint is a pure-Python implementation of CSS Paged Media (no binary dependency after install); both render HTML, but WeasyPrint handles @page rules, string-set, and running() elements for repeating headers/footers that wkhtmltopdf does not support. The Claude Skills 360 bundle includes WeasyPrint skill sets covering render_html()/render_file() core API, BASE_CSS with @page rules and counter(page)/counter(pages), generate_invoice() HTML template builder, generate_table_report() with landscape support, render_jinja2_template() for template-driven PDFs, FastAPI/Flask response pattern, and font configuration with FontConfiguration(). Start with the free tier to try HTML-to-PDF generation code generation.