Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchPC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Use ReportLab when you want to place invoice elements directly on a PDF canvas. Use WeasyPrint when you want to write an HTML/CSS invoice template and render it to PDF. In both cases, keep invoice data, calculations, and rendering separate. That design makes totals testable, templates replaceable, and output easier to audit.
This guide gives complete Python examples for both routes, explains their trade-offs, and covers layout, pagination, security, installation, troubleshooting, and the legal limits of a PDF library. The required invoice fields and tax rules depend on your jurisdiction and transaction type, so verify them with the relevant tax authority before using an invoice operationally.
Choose the rendering approach first
| Decision | ReportLab | WeasyPrint |
|---|---|---|
| Layout authoring | Python drawing calls on a PDF canvas | HTML and CSS rendered through the Python API |
| Coordinate model | Direct placement; the default origin is the lower-left corner | Document flow, print CSS, tables and page rules |
| Best fit | A programmatic layout with precise placement | A web-oriented template that designers can edit |
| Main caution | You must manage positioning and page breaks yourself | Untrusted HTML or CSS can create security problems |
These are workflow differences documented by the projects, not performance benchmarks. ReportLab describes itself as a Python library for directly creating PDF documents; its introduction and canvas guide document the drawing model. WeasyPrint documents HTML-to-PDF rendering and invoice use cases in its common use cases and first steps guides.
Model invoice data before drawing anything
Do not calculate totals inside PDF drawing code. Store a structured invoice, calculate each line and the grand total once, then pass the resulting values to a renderer.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →#1 Best Overall
from decimal import Decimal, ROUND_HALF_UP
from dataclasses import dataclass
@dataclass
class LineItem:
description: str
quantity: Decimal
unit_price: Decimal
tax_rate: Decimal
def money(value: Decimal) -> Decimal:
return value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
def calculate_totals(items):
rows = []
subtotal = Decimal("0")
tax_total = Decimal("0")
for item in items:
net = money(item.quantity * item.unit_price)
tax = money(net * item.tax_rate / Decimal("100"))
rows.append({"description": item.description, "quantity": item.quantity,
"unit_price": money(item.unit_price), "net": net,
"tax": tax, "tax_rate": item.tax_rate})
subtotal += net
tax_total += tax
return rows, money(subtotal), money(tax_total), money(subtotal + tax_total)
invoice = {
"number": "INV-001",
"issue_date": "2026-09-29",
"currency": "USD",
"seller": {"name": "Example Studio", "address": "1 Main Street, London"},
"customer": {"name": "Acme Ltd", "address": "20 Market Road, London"},
"items": [
LineItem("Consulting", Decimal("4"), Decimal("125.00"), Decimal("20")),
LineItem("Support", Decimal("1"), Decimal("50.00"), Decimal("20")),
],
}
rows, subtotal, tax_total, total = calculate_totals(invoice["items"])
Using Decimal avoids binary floating-point surprises. Decide and document whether tax is rounded per line or only after summing; the correct convention can be jurisdiction-specific. Also decide how to represent credits, discounts, zero-rated lines, exempt lines, payment terms, bank details, and invoice numbering before styling the PDF.
Route A: generate an invoice with ReportLab
Install the library
The documented open-source installation starts with:
python -m pip install reportlab
ReportLab’s installation guidance notes that optional extras may be needed for particular graphics features. Confirm the requirements for the version and features you deploy in the installation guide.
Complete drawing example
The canvas uses points and starts at the lower-left origin by default. This example creates a letter-sized invoice, wraps long descriptions, draws a table, and starts a second page when the line-item area is full.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Clear out junk files and repair common Windows errors3Scan for outdated or missing drivers - takes under a minuteRank #2
from reportlab.lib import colors
from reportlab.lib.pagesizes import letter
from reportlab.lib.units import mm
from reportlab.pdfgen import canvas
from reportlab.pdfbase.pdfmetrics import stringWidth
def draw_wrapped(c, text, x, y, max_width, leading=12, font="Helvetica", size=9):
c.setFont(font, size)
words = text.split()
line = ""
lines = []
for word in words:
trial = (line + " " + word).strip()
if stringWidth(trial, font, size) <= max_width:
line = trial
else:
if line:
lines.append(line)
line = word
if line:
lines.append(line)
for value in lines:
c.drawString(x, y, value)
y -= leading
return y
def header(c, inv, width, height):
c.setFont("Helvetica-Bold", 20)
c.drawString(20 * mm, height - 25 * mm, "INVOICE")
c.setFont("Helvetica", 9)
c.drawRightString(width - 20 * mm, height - 22 * mm, f"Number: {inv['number']}")
c.drawRightString(width - 20 * mm, height - 29 * mm, f"Issued: {inv['issue_date']}")
y = height - 50 * mm
c.setFont("Helvetica-Bold", 10)
c.drawString(20 * mm, y, "From")
c.drawString(110 * mm, y, "Bill to")
c.setFont("Helvetica", 9)
draw_wrapped(c, f"{inv['seller']['name']}n{inv['seller']['address']}",
20 * mm, y - 14, 75 * mm)
draw_wrapped(c, f"{inv['customer']['name']}n{inv['customer']['address']}",
110 * mm, y - 14, 75 * mm)
def make_pdf(inv, rows, subtotal, tax_total, total, filename="invoice.pdf"):
c = canvas.Canvas(filename, pagesize=letter)
width, height = letter
y = height - 95 * mm
header(c, inv, width, height)
def table_header():
nonlocal y
c.setFillColor(colors.HexColor("#eeeeee"))
c.rect(20 * mm, y - 5, width - 40 * mm, 18, fill=1, stroke=0)
c.setFillColor(colors.black)
c.setFont("Helvetica-Bold", 8)
c.drawString(22 * mm, y, "Description")
c.drawRightString(130 * mm, y, "Qty")
c.drawRightString(155 * mm, y, "Unit")
c.drawRightString(190 * mm, y, "Net")
y -= 22
table_header()
for row in rows:
if y < 45 * mm:
c.showPage()
y = height - 25 * mm
c.setFont("Helvetica-Bold", 12)
c.drawString(20 * mm, y, f"Invoice {inv['number']} (continued)")
y -= 18
table_header()
c.setFont("Helvetica", 8)
y = draw_wrapped(c, row["description"], 22 * mm, y, 95 * mm, leading=10, size=8)
c.drawRightString(130 * mm, y + 10, str(row["quantity"]))
c.drawRightString(155 * mm, y + 10, f"{row['unit_price']:.2f}")
c.drawRightString(190 * mm, y + 10, f"{row['net']:.2f}")
y -= 8
y -= 8
c.line(115 * mm, y, 190 * mm, y)
c.setFont("Helvetica", 9)
for label, value in (("Subtotal", subtotal), ("Tax", tax_total), ("Total", total)):
y -= 15
c.drawString(120 * mm, y, label)
c.drawRightString(190 * mm, y, f"{inv['currency']} {value:.2f}")
c.setFont("Helvetica", 8)
c.drawString(20 * mm, 20 * mm, "Payment terms and legally required notices go here.")
c.save()
make_pdf(invoice, rows, subtotal, tax_total, total)
The short skeleton in the official canvas guide follows the same lifecycle: create a canvas, draw text, call showPage(), then call save(). Choose the page size deliberately for the destination rather than assuming the default is appropriate.
ReportLab failure modes
- Text runs off the page: measure or wrap strings, reduce font size, and reserve a footer area.
- Rows overlap: decrement the vertical cursor after every wrapped line, not just every item.
- Missing characters: register and embed a font that contains the required glyphs; test currency symbols and non-Latin customer names.
- Unreadable multipage output: repeat the table header after
showPage()and include the invoice number on continuation pages.
Route B: render an HTML invoice with WeasyPrint
Install and understand the dependency boundary
Install the Python package according to the platform instructions in WeasyPrint’s documentation. A minimal API call is:
from weasyprint import HTML
html = """
<h1>Invoice</h1>
<p>Invoice number: INV-001</p>
"""
HTML(string=html).write_pdf("invoice.pdf")
For a production invoice, generate escaped values from structured data and keep print rules in CSS. Never concatenate arbitrary customer-provided HTML into the template.
Complete HTML/CSS rendering example
from html import escape
from weasyprint import HTML
def invoice_html(inv, rows, subtotal, tax_total, total):
item_rows = "".join(
f"<tr><td>{escape(str(r['description']))}</td>"
f"<td class='num'>{r['quantity']}</td>"
f"<td class='num'>{r['unit_price']:.2f}</td>"
f"<td class='num'>{r['net']:.2f}</td></tr>"
for r in rows
)
return f"""<!doctype html>
<html><head><meta charset='utf-8'>
<style>
@page {{ size: A4; margin: 18mm 16mm 20mm; @bottom-right {{ content: 'Page ' counter(page) ' of ' counter(pages); font-size: 8pt; }} }}
body {{ font-family: sans-serif; color: #222; font-size: 10pt; }}
h1 {{ margin: 0; }}
.meta {{ float: right; text-align: right; }}
.addresses {{ display: flex; justify-content: space-between; margin: 18mm 0 12mm; }}
table {{ width: 100%; border-collapse: collapse; page-break-inside: auto; }}
thead {{ display: table-header-group; }}
tr {{ page-break-inside: avoid; }}
th, td {{ border-bottom: 1px solid #ddd; padding: 7px 4px; text-align: left; }}
.num {{ text-align: right; }}
.totals {{ margin-left: auto; width: 45%; }}
.totals td {{ border: 0; }}
</style></head><body>
<header><h1>Invoice</h1>
<div class='meta'>Number: {escape(inv['number'])}<br>Issued: {escape(inv['issue_date'])}</div></header>
<section class='addresses'><div><strong>From</strong><br>{escape(inv['seller']['name'])}<br>{escape(inv['seller']['address'])}</div>
<div><strong>Bill to</strong><br>{escape(inv['customer']['name'])}<br>{escape(inv['customer']['address'])}</div></section>
<table><thead><tr><th>Description</th><th class='num'>Qty</th><th class='num'>Unit</th><th class='num'>Net</th></tr></thead>
<tbody>{item_rows}</tbody></table>
<table class='totals'><tr><td>Subtotal</td><td class='num'>{inv['currency']} {subtotal:.2f}</td></tr>
<tr><td>Tax</td><td class='num'>{inv['currency']} {tax_total:.2f}</td></tr>
<tr><th>Total</th><th class='num'>{inv['currency']} {total:.2f}</th></tr></table>
</body></html>"""
HTML(string=invoice_html(invoice, rows, subtotal, tax_total, total), base_url=".").write_pdf("invoice.pdf")
CSS gives you familiar tools such as table headers that repeat on new pages, controlled margins, and page-break-inside: avoid. The WeasyPrint invoice use-case documentation also shows workflows involving PDF/A and a Factur-X XML attachment; adapt those file contents to your actual invoice rather than copying the example unchanged.
WeasyPrint security boundary
WeasyPrint warns that untrusted HTML or CSS can create security problems. Use a fixed, reviewed template; escape values; restrict accessible resources; and isolate rendering if users can influence invoice content. Do not accept arbitrary CSS, file URLs, scripts, or network references from an untrusted tenant.
Validate the generated PDF before sending it
- Recalculate the totals independently of the renderer and compare the displayed subtotal, tax, and total.
- Open PDFs produced with short and very long descriptions, many rows, zero quantities where allowed, discounts, non-ASCII names, and multiple tax rates.
- Check page boundaries, repeated headers, footer text, date and currency formatting, and that no address or total is clipped.
- Extract text in an automated test or inspect the PDF manually; also print or view it at normal zoom.
- Store the source data and a stable invoice number so a regenerated file can be compared with the issued record.
Legal and operational limits
ReportLab and WeasyPrint only create the document. They do not decide which fields are legally required, how tax must be calculated, whether electronic invoicing is mandatory, which language or currency rules apply, or how long records must be retained. Those requirements vary by jurisdiction, transaction type, and business status. Obtain the applicable rules from your tax authority or regulator and have a qualified adviser review your process.
Or skip the browser setup
If your invoice already exists as a web page and you need a visual check or a shareable capture, ScreenshotNeo can take the screenshot without you managing a headless browser. Its API accepts a URL and can return PNG, JPEG, WebP, or PDF.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for all parameters. The equivalent Python and Node.js calls are:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', data));
Before capture, ScreenshotNeo can accept consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status with X-Page-Verdict and X-Billed headers. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Sign up for ScreenshotNeo to start with the free allowance.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Troubleshooting checklist
“No module named reportlab” or “No module named weasyprint”
Install into the same virtual environment used to run the script: python -m pip install reportlab or the platform-specific WeasyPrint installation steps. Confirm the interpreter with python -c "import sys; print(sys.executable)".
The PDF is blank
For ReportLab, verify that drawing calls occur before save(). For WeasyPrint, inspect the generated HTML string, ensure the data is not empty, and pass a correct base_url when loading local assets.
Free tools Windows power users keep installed
One-click scans. No signup required.
Images or fonts do not appear
Use resolvable paths or URLs, set base_url for WeasyPrint, and register fonts explicitly in ReportLab. Test in the same restricted environment used in production.
Best Value
Rows split badly across pages
In ReportLab, reserve space and start a new page before drawing a row that will not fit. In WeasyPrint, use table headers, page-break-inside: avoid, and conservative margins; very large unbreakable content may still require a deliberate page break.
FAQ
Frequently Asked Questions
Can I generate an invoice entirely in memory?
Yes. Both libraries can write to a file-like object such as an io.BytesIO buffer; return those bytes from a web endpoint instead of writing a temporary file.
Should I use PDF/A for every invoice?
Not automatically. PDF/A, Factur-X, and attached XML can be appropriate in workflows that require archival or structured electronic invoices, but the applicable requirement comes from your jurisdiction and customer agreement.
How do I prevent duplicate invoice numbers after a retry?
Allocate the invoice number in a durable database transaction, enforce a uniqueness constraint, and make PDF generation idempotent for that number. Rendering should not be the component that assigns numbering.
Can a PDF prove that tax was calculated correctly?
No. It displays the values your application supplied. Keep the input data, rounding policy, tax-rate source, and calculation tests so the result can be audited.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




