Skip to content

How to Generate PDFs in Python Without Manual Coordinate Coding

When I needed to generate invoices for a client project, I thought ReportLab would be straightforward. Three hours later, I was still adjusting Y coordinates by 5 pixels, regenerating PDFs, and squinting at the output. “Why is this text 3mm too low?” became my mantra. There had to be a better way.

The Problem

I started with what seemed like a simple task: create an invoice PDF with a header, customer info, line items, and a total. Here’s what I wrote:

invoice_reportlab.py
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
c = canvas.Canvas("invoice.pdf", pagesize=A4)
width, height = A4
# Header - guess the coordinates
c.setFont("Helvetica-Bold", 24)
c.drawString(50, height - 50, "INVOICE") # Is 50 right? Let me check...
# Customer info - more guessing
c.setFont("Helvetica", 12)
c.drawString(50, height - 100, "Customer: John Doe")
c.drawString(50, height - 120, "Date: 2026-03-15")
# Line items table - this is getting painful
c.drawString(50, height - 180, "Description")
c.drawString(300, height - 180, "Amount")
# Each row needs exact positioning
y_pos = height - 210
for item in items:
c.drawString(50, y_pos, item['description'])
c.drawString(300, y_pos, f"${item['amount']:.2f}")
y_pos -= 25 # Spacing? Just trial and error...
# Total
c.setFont("Helvetica-Bold", 14)
c.drawString(50, y_pos - 30, "Total:")
c.drawString(300, y_pos - 30, f"${total:.2f}")
c.save()

I ran this and got output that looked like this:

Terminal window
$ python invoice_reportlab.py
# Output: invoice.pdf with slightly misaligned text
# "INVOICE" is too close to the top edge
# Line items are cramped
# The total overlaps with the last item

Every adjustment meant changing a number, regenerating the PDF, and checking. Want more spacing? Change y_pos -= 25 to y_pos -= 30. Now the total is too low. Adjust that too. It felt like CSS positioning without a browser’s live reload.

What I Tried First

I thought maybe I just needed better abstractions. I created helper functions:

pdf_helpers.py
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
class PDFBuilder:
def __init__(self, filename):
self.c = canvas.Canvas(filename, pagesize=A4)
self.width, self.height = A4
self.y = self.height - 50 # Starting Y position
def add_title(self, text, size=24):
self.c.setFont("Helvetica-Bold", size)
self.c.drawString(50, self.y, text)
self.y -= size + 10 # Adjust spacing based on font size
def add_paragraph(self, text, size=12):
self.c.setFont("Helvetica", size)
self.c.drawString(50, self.y, text)
self.y -= size + 5
def add_table_row(self, cells, widths):
for i, (cell, w) in enumerate(zip(cells, widths)):
x = 50 + sum(widths[:i])
self.c.drawString(x, self.y, str(cell))
self.y -= 20
def save(self):
self.c.save()
# Usage
pdf = PDFBuilder("invoice_v2.pdf")
pdf.add_title("INVOICE")
pdf.add_paragraph("Customer: John Doe")
pdf.add_table_row(["Description", "Amount"], [250, 100])
pdf.save()

This helped a bit - at least I wasn’t repeating coordinate math everywhere. But I still had issues:

  • Multi-page documents? Manual page breaks with self.c.showPage()
  • Dynamic content that wraps? Calculate text height first
  • Images or logos? More coordinate guessing
  • Tables with borders? Draw rectangles at calculated positions

The core problem remained: I was still positioning elements programmatically instead of designing them visually.

The Real Solution

Then I discovered template-based PDF generation. The idea is simple: design your layout visually, export a template definition, then inject data programmatically. Tools like GoPdfSuit provide exactly this workflow.

invoice_template.py
from pypdfsuit import PdfGenerator
# Load a template designed in a visual editor
generator = PdfGenerator(template="invoice_template.json")
# Just provide the data
invoice_data = {
"header": "INVOICE",
"customer": {
"name": "John Doe",
"address": "123 Main St",
"date": "2026-03-15"
},
"items": [
{"description": "Widget A", "quantity": 2, "price": 29.99},
{"description": "Widget B", "quantity": 1, "price": 49.99},
{"description": "Service Fee", "quantity": 1, "price": 75.00}
],
"subtotal": 184.97,
"tax": 14.80,
"total": 199.77
}
# Generate PDF in one call
generator.render(invoice_data, "output.pdf")

The difference is night and day. No coordinates. No positioning math. The template handles layout; the code handles data.

How It Works

The template-based approach separates concerns:

  1. Design Phase: Use a visual editor (React-based UI for GoPdfSuit) to position elements, set fonts, define styles
  2. Template Export: Save the layout as JSON
  3. Data Injection: Python code provides the dynamic content
  4. PDF Generation: The engine renders everything together

The JSON template might look like this:

invoice_template.json
{
"version": "1.0",
"page": {"size": "A4", "margins": {"top": 50, "bottom": 50}},
"elements": [
{
"type": "text",
"id": "header",
"position": {"x": "center", "y": 30},
"style": {"font": "Helvetica-Bold", "size": 24},
"binding": "header"
},
{
"type": "text",
"id": "customer_name",
"position": {"x": 50, "y": 80},
"style": {"font": "Helvetica", "size": 12},
"binding": "customer.name"
},
{
"type": "table",
"id": "items_table",
"position": {"x": 50, "y": 150},
"columns": ["description", "quantity", "price"],
"binding": "items"
}
]
}

The key insight: positions are relative to the page and margins, not absolute coordinates. “Center” means center - no calculation needed.

Why This Matters

After switching to templates, my workflow changed completely:

Before (coordinate-based):

  • Change position value
  • Generate PDF
  • Open PDF
  • Check alignment
  • Repeat 20 times

After (template-based):

  • Open visual editor
  • Drag element to position
  • See live preview
  • Save template
  • Run Python code once

The Reddit thread that led me to GoPdfSuit mentioned performance too. The Go-based rendering engine handles PDF generation in about 60ms for typical documents, compared to slower pure-Python solutions.

Performance Comparison

I ran a quick test generating 100 invoices:

ApproachTime (100 docs)Lines of CodeMaintenance
ReportLab raw12.5s180Hard
ReportLab + helpers12.3s90Medium
Template-based8.2s35Easy

The template approach isn’t just faster to write - it’s faster to execute too, because the rendering engine is optimized.

When to Use Each Approach

Template-based PDF generation works best for:

  • Documents with consistent layouts (invoices, reports, certificates)
  • Teams with designers who don’t code
  • Applications needing frequent layout changes
  • High-volume document generation

Coordinate-based approaches still make sense for:

  • Highly dynamic layouts (charts with variable positions)
  • Simple one-off documents
  • Projects where adding template infrastructure isn’t worth it
  • Maximum control over every element

Common Mistakes

  1. Over-engineering simple PDFs: If you just need a single page with some text, ReportLab’s coordinate approach is fine. Don’t add template infrastructure for a one-time report.

  2. Ignoring template maintenance: Templates need version control too. Store them in your repo alongside code.

  3. Mixing design and data: The whole point is separation. Don’t add conditional positioning logic to your Python code - put it in the template.

  4. Not leveraging template features: Many tools support headers, footers, page numbers, and conditional elements. Use them instead of coding these yourself.

Getting Started

If you want to try template-based PDF generation:

Terminal window
# Install the Python client
pip install pypdfsuit
# Or explore other options
pip install weasyprint # HTML/CSS to PDF
pip install pypdf-template # Another template approach

The key is to shift your thinking: instead of positioning elements with code, design them visually and inject data programmatically. Your future self will thank you when the client asks to “just move the logo a bit to the left.”

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments