HTML Template to PDF in Django
By Justin

Generating PDFs is a common task for web applications and various projects. In the tutorial below, we'll use django and xhtml2pdf to create a PDF that is styled with CSS.
The goal is to:
- Render a PDF as an HTTP response
- Work much like the render shortcut function in Django
- Be reusable across projects
Let's create our render_to_pdf project and functionality now.
Create Blank Django Project
shell
$ mkdir -p ~/Dev/render_to_pdf/src
$ cd ~/Dev/render_to_pdf
$ python3 -m venv venv
$ source venv/bin/activate
$ echo "django" >> requirements.txt
$ echo "xhtml2pdf" >> requirements.txt
(venv) $ python -m pip install -r requirements.txt
(venv) $ cd src
(venv) $ django-admin startproject render_to_pdf
The resulting project will match this repo. The last tested Django version is 4.2 but has worked since 1.11. Python 3.8+ is required.
Install xhtml2pdf
Using Python 3
shell
venv/bin/python -m pip install xhtml2pdf pip --upgrade
This step was should have been completed before, but if issues arise, try again or review the docs.
Create renderers.py next to settings.py
In my case, I have cfehome/ as my project config root where settings.py, urls.py and wsgi.py are available by default. Create the file renderers.py with the following contents:
python
from io import BytesIO
from django.http import HttpResponse
from django.template.loader import get_template
from xhtml2pdf import pisa
def render_to_pdf(template_src, context_dict={}):
template = get_template(template_src)
html = template.render(context_dict)
result = BytesIO()
pdf = pisa.pisaDocument(BytesIO(html.encode("ISO-8859-1")), result)
if pdf.err:
return HttpResponse("Invalid PDF", status_code=400, content_type='text/plain')
return HttpResponse(result.getvalue(), content_type='application/pdf')
Create PDF Template
Update settings.py's TEMPLATES setting to include:
python
BASE_DIR = Path(__file__).resolve().parent.parent
...
TEMPLATES = [{
...
"DIRS": [BASE_DIR / "templates"],
...
}]
Create a directory templates/pdf. I recommend using inline styles when designing PDFs. In templates/pdfs add the file invoice.html with the following contents:
html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>{% if pdf_title %}{{ pdf_title }}{% else %}PDF{% endif %}</title>
<style type="text/css">
body {
font-weight: 200;
font-size: 14px;
}
.header {
font-size: 20px;
font-weight: 100;
text-align: center;
color: #007cae;
}
.title {
font-size: 22px;
font-weight: 100;
/* text-align: right;*/
padding: 10px 20px 0px 20px;
}
.title span {
color: #007cae;
}
.details {
padding: 10px 20px 0px 20px;
text-align: left !important;
/*margin-left: 40%;*/
}
.hrItem {
border: none;
height: 1px;
/* Set the hr color */
color: #333; /* old IE */
background-color: #fff; /* Modern Browsers */
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='header'>
<p class='title'>Invoice #{{ invoice_number}}</p>
</div>
<div>
<div class='details'>
Bill to: {{ customer_name }}<br/>
Amount: {{ amount }} <br/>
Date: {{ date }}
<hr class='hrItem' />
</div>
</div>
</body>
</html>
render_to_pdf In Views
Our render_to_pdf view returns an HTTPResponse by default. This means we can use it in any view that returns an HTTPResponse and thus update a based on a request if needed.
Basic Usage
This response will return a PDF based on our template and the context we pass in the view. The browser will likely just render this PDF within the browser.
python
from django.http import HTTP404
from cfehome import renderers
def pdf_view(self, request, *args, **kwargs):
data = {
'today': datetime.date.today(),
'amount': 39.99,
'customer_name': 'Cooper Mann',
'invoice_number': 1233434,
}
return renderers.render_to_pdf('pdfs/invoice.html', data)
Advanced Usage
In this example
python
import locale
locale.setlocale(locale.LC_ALL, "")
def advanced_pdf_view(request):
invoice_number = "007cae"
context = {
"bill_to": "Ethan Hunt",
"invoice_number": f"{invoice_number}",
"amount": locale.currency(100_000, grouping=True),
"date": "2021-07-04",
"pdf_title": f"Invoice #{invoice_number}",
}
response = renderers.render_to_pdf("pdfs/invoice.html", context)
if response.status_code == 404:
raise HTTP404("Invoice not found")
filename = f"Invoice_{invoice_number}.pdf"
"""
Tell browser to view inline (default)
"""
content = f"inline; filename={filename}"
download = request.GET.get("download")
if download:
"""
Tells browser to initiate download
"""
content = f"attachment; filename={filename}"
response["Content-Disposition"] = content
return response
Now we have a simple and effective way to create PDFs in Django as HTTP responses. The goal here is to make it easy to generate PDF data based on a user's request.