| name | weasyprint |
| description | Generate PDF documents and reports using WeasyPrint with Jinja2 templates, CSS styling, and embedded charts/graphics. Use when the user needs to (1) create PDF reports, invoices, letters, or any printable document from HTML/CSS, (2) render Jinja2 templates to PDF, (3) embed matplotlib or other charts as PNG/SVG in PDF documents, (4) control PDF pagination with CSS @page rules, page breaks, running headers/footers, (5) style PDF output with CSS including tables, typography, and layout, (6) work with WeasyPrint's Python API or command-line tool. |
WeasyPrint PDF Generation
Generate PDF documents from Jinja2 HTML templates styled with CSS. WeasyPrint is a Python visual rendering engine for HTML/CSS that exports to PDF.
Core Workflow
- Prepare data (Python dicts, Pandas DataFrames, chart images)
- Render Jinja2 template to HTML string
- Pass HTML + CSS to WeasyPrint to produce PDF
from jinja2 import Environment, FileSystemLoader
from weasyprint import HTML, CSS
env = Environment(loader=FileSystemLoader('templates'))
template = env.get_template('report.html.j2')
html_content = template.render(title="Q4 Report", sections=data)
HTML(string=html_content, base_url='templates/').write_pdf(
'report.pdf',
stylesheets=[CSS(filename='report.css')]
)
Critical: Always set base_url when using HTML(string=...) so relative image/font paths resolve correctly.
Quick Reference
HTML Input Methods
from weasyprint import HTML
HTML(string='<h1>Hello</h1>')
HTML(filename='report.html')
HTML(url='https://example.com')
HTML(file_obj=open('report.html'))
Adding Stylesheets
from weasyprint import CSS
html.write_pdf('out.pdf', stylesheets=[
CSS(filename='base.css'),
CSS(string='@page { size: A4 landscape; }'),
])
Key Options
html.write_pdf('out.pdf',
stylesheets=[CSS(filename='style.css')],
optimize_images=True,
jpeg_quality=85,
dpi=150,
pdf_forms=True,
)
Pagination with CSS
Use @page rules to control page layout, and break-before/break-after for page breaks:
@page {
size: A4;
margin: 25mm 20mm;
@bottom-center {
content: counter(page) " / " counter(pages);
font-size: 8pt;
}
}
.chapter { break-before: page; }
.keep-together { break-inside: avoid; }
thead { display: table-header-group; }
Running Headers and Footers
.running-header {
position: running(pageHeader);
}
@page {
margin-top: 3cm;
@top-center {
content: element(pageHeader);
}
}
Named Pages
Apply different layouts to different sections (cover page, landscape tables, etc.):
@page cover { margin: 0; @bottom-center { content: none; } }
@page landscape { size: A4 landscape; }
.cover-page { page: cover; }
.wide-table-section { page: landscape; }
Embedding Charts
Matplotlib to Base64 (No Temp Files)
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import io, base64
def chart_to_base64(fig, fmt='png', dpi=150):
buf = io.BytesIO()
fig.savefig(buf, format=fmt, bbox_inches='tight', dpi=dpi)
buf.seek(0)
b64 = base64.b64encode(buf.read()).decode('utf-8')
plt.close(fig)
return f"data:image/{fmt};base64,{b64}"
fig, ax = plt.subplots(figsize=(6, 4))
ax.bar(['Q1', 'Q2', 'Q3', 'Q4'], [100, 150, 130, 180])
chart_uri = chart_to_base64(fig)
SVG Charts (Best Quality)
Use format='svg' for vector output that stays sharp at any zoom. Inline SVG directly in the template with {{ svg_string | safe }} or use base64 data URIs.
Wrap Charts to Avoid Page Breaks
.chart-container { break-inside: avoid; }
Jinja2 Template Patterns
Template Includes
Split complex reports into partials:
{# report.html.j2 #}
{% include "_header.html.j2" %}
{% for section in sections %}
{% include "_section.html.j2" %}
{% endfor %}
Custom Filters
env = Environment(loader=FileSystemLoader('templates'))
env.filters['currency'] = lambda v: f"${v:,.2f}"
env.filters['format_date'] = lambda d: d.strftime('%B %d, %Y')
Pandas DataFrame Tables
template_vars = {'data_table': df.to_html(classes='data-table', index=False)}
Custom Fonts
from weasyprint.text.fonts import FontConfiguration
font_config = FontConfiguration()
css = CSS(string='''
@font-face {
font-family: "CustomFont";
src: url("fonts/CustomFont.woff2");
}
body { font-family: "CustomFont", sans-serif; }
''', font_config=font_config)
html.write_pdf('out.pdf', stylesheets=[css], font_config=font_config)
Bundled Resources
scripts/render_pdf.py
Reusable CLI script to render a Jinja2 template to PDF:
python scripts/render_pdf.py template.html.j2 output.pdf --data data.json --css style.css
assets/report-template/
Starter template and CSS for a multi-page report with cover page, table of contents, running headers/footers, tables, and chart placeholders. Copy and customize for new reports.
report.html.j2 - Jinja2 template with sections for cover, TOC, content, charts, tables
report.css - CSS with @page rules, pagination, typography, table styles, named pages
references/
Read these as needed for detailed information:
- api-reference.md - Complete WeasyPrint Python API: HTML/CSS/Document classes, URLFetcher, FontConfiguration, DEFAULT_OPTIONS, CLI
- css-paged-media.md - @page rules, margin boxes, named pages, page breaks, running elements, bookmarks/TOC, supported CSS modules and limitations
- images-and-charts.md - Matplotlib embedding (file, base64, SVG), custom URL fetcher for dynamic charts, image optimization, common pitfalls
Key Gotchas
base_url is essential when using HTML(string=...) - without it, relative image/font paths fail silently
matplotlib.use('Agg') must be called before importing pyplot for headless rendering
- CSS
calc() is not supported - use fixed values or CSS variables
box-shadow is not supported - use borders instead
- Interactive pseudo-classes (
:hover, :focus) never match in PDF context
- Pass CSS as stylesheets parameter, not inline in the HTML - inline
<style> tags can be unreliable
- Call
plt.close(fig) after generating each chart to prevent memory leaks
- Use
optimize_images=True and set dpi=150 to keep PDF file sizes reasonable