Use Django to render an HTML template, pass that HTML to the separately installed wkhtmltopdf executable through a Python wrapper, and return the resulting bytes as an application/pdf response. The binary is not installed by pip. You must install and pin it, configure its path, make your templates print-friendly, and isolate the renderer because untrusted HTML or JavaScript can compromise the host.
How the Django-to-PDF pipeline works
Django is responsible for authentication, data access, and rendering a template. wkhtmltopdf is a separate command-line program that converts the rendered HTML (using its Qt/WebKit engine) to PDF. A wrapper such as django-pdfkit or django-wkhtmltopdf starts that executable and gives Django the output bytes.
- Build a normal Django view and template.
- Render the template to a string, with absolute asset URLs or assets available to the renderer.
- Pass the string and conversion options to wkhtmltopdf.
- Return the bytes with
Content-Disposition: inlineorattachment.
The official downloads page identifies the 0.12.6 series as the current stable series and dates that release June 11, 2020. That age makes reproducible pinning and compatibility testing especially important.
Install and pin the required components
Install the executable
Download a build for your deployment operating system from the official wkhtmltopdf downloads page. Builds are listed for Windows, macOS, and Debian. Some capabilities depend on a build with patched Qt.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →#1 Best Overall
Distribution repositories on Debian or Ubuntu can contain packages with reduced functionality. Treat the downloaded binary as an application dependency, not as something the Python package will provide. Record its version in your deployment documentation and verify the checksum according to your normal supply-chain process.
Install a Django wrapper
python -m pip install django-pdfkit pdfkit
django-wkhtmltopdf is another wrapper that supplies Django views around the binary. Install one wrapper, pin its version in your requirements file, and test it with the exact wkhtmltopdf build used in production.
Configure the binary path
If wkhtmltopdf is on the service account’s PATH, the wrapper can usually find it. An explicit path is safer in containers and systemd services:
# settings.py
WKHTMLTOPDF_BIN = "/usr/local/bin/wkhtmltopdf"
For django-pdfkit, use WKHTMLTOPDF_BIN. For django-wkhtmltopdf, use WKHTMLTOPDF_CMD; that package also supports WKHTMLTOPDF_CMD_OPTIONS. Confirm the path as the same Unix user that runs Django:
PC 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 & 11Outdated 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 match/usr/local/bin/wkhtmltopdf --version
Do not assume a binary that works in an interactive shell is visible to a web worker with a different environment.
Rank #2
A complete Django view using pdfkit
This function-based example makes the conversion steps explicit and lets you control the response disposition. It renders an invoice, converts it in memory, and never writes a temporary PDF to a public directory.
# invoices/views.py
from pathlib import Path
import pdfkit
from django.conf import settings
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.template.loader import render_to_string
from .models import Invoice
def invoice_pdf(request, invoice_id):
invoice = get_object_or_404(Invoice, pk=invoice_id)
html = render_to_string(
"invoices/invoice.html",
{"invoice": invoice},
request=request,
)
binary = getattr(settings, "WKHTMLTOPDF_BIN", "wkhtmltopdf")
configuration = pdfkit.configuration(wkhtmltopdf=binary)
options = {
"encoding": "UTF-8",
"page-size": "A4",
"margin-top": "15mm",
"margin-right": "15mm",
"margin-bottom": "15mm",
"margin-left": "15mm",
"print-media-type": None,
"quiet": None,
}
pdf_bytes = pdfkit.from_string(
html,
False,
configuration=configuration,
options=options,
)
response = HttpResponse(pdf_bytes, content_type="application/pdf")
response["Content-Disposition"] = (
f'inline; filename="invoice-{invoice.pk}.pdf"'
)
return response
Use attachment instead of inline when the browser should download the file:
response["Content-Disposition"] = 'attachment; filename="invoice-123.pdf"'
Use a filename derived from trusted identifiers only. Never place a user-supplied filename directly in this header.
Wire the URL
# invoices/urls.py
from django.urls import path
from .views import invoice_pdf
urlpatterns = [
path("<int:invoice_id>/pdf/", invoice_pdf, name="invoice-pdf"),
]
Protect the URL with the same authentication and object-level authorization as the invoice HTML page. A PDF endpoint must not turn an otherwise private record into a public download.
Template and print CSS
{# templates/invoices/invoice.html #}
{% load static %}
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Invoice {{ invoice.number }}</title>
<link rel="stylesheet" href="https://app.example.com{% static 'invoices/print.css' %}">
</head>
<body>
<h1>Invoice {{ invoice.number }}</h1>
<p>{{ invoice.customer_name }}</p>
<table>
{% for item in invoice.items.all %}
<tr><td>{{ item.description }}</td><td>{{ item.total }}</td></tr>
{% endfor %}
</table>
</body>
</html>
Use Django’s normal escaping. If you deliberately render rich text with |safe, sanitize it before it reaches the template. wkhtmltopdf may need to fetch CSS, fonts, and images over HTTPS; a browser-relative URL that works for a user may fail for a headless process with no session cookie.
Using django-pdfkit or django-wkhtmltopdf views
django-pdfkit documents PDFView as a drop-in replacement for TemplateView. That is useful when your existing page already has a class-based view:
from pdfkit.views import PDFView
class InvoicePDFView(PDFView):
template_name = "invoices/invoice.html"
Keep your existing get_context_data() authorization and context logic, then map the class in urls.py. The package supports inline, download, html, and debug query parameters; download is the default, while inline asks the browser to display the PDF when supported. Check the installed wrapper’s version for the exact option names before relying on query-string behavior.
Recommended Free Tools
django-wkhtmltopdf provides similar Django views and reads WKHTMLTOPDF_CMD and, when needed, WKHTMLTOPDF_CMD_OPTIONS. Do not configure both wrappers in the same endpoint unless you have a specific reason; having one conversion path makes failures and upgrades easier to diagnose.
CSS, images, fonts, JavaScript, and page breaks
Make resources reachable
- Prefer absolute HTTPS URLs for stylesheets, images, and web fonts that the renderer can reach without an interactive login.
- If you must use local files, understand the security implications of enabling local-file access and restrict the process at the operating-system level.
- Use print-specific rules in
@media printand set explicit widths, colors, and font stacks. - Test the fonts installed in the deployment image; a missing font changes line wrapping and pagination.
Control pagination
@media print {
.page-break { page-break-before: always; }
tr { page-break-inside: avoid; }
.screen-only { display: none; }
}
Long tables, images, and nested flex layouts are common sources of unexpected breaks. Keep a fixed-width printable layout for invoices and reports, then test representative short, long, and multi-page records.
Know the JavaScript boundary
wkhtmltopdf can execute some JavaScript, but it is not a current browser. Charts or components that depend on modern APIs, delayed network calls, or complex client-side rendering can remain blank. If JavaScript is essential, set a deliberate delay or wait strategy only after measuring it; an arbitrary long delay slows every request and still does not guarantee readiness.
Security: treat the renderer as a privileged boundary
The wkhtmltopdf project warns: “Do not use wkhtmltopdf with any untrusted HTML – be sure to sanitize any user-supplied HTML/JS, otherwise it can lead to complete takeover of the server it is running on!” Django’s security guidance likewise requires sanitizing user input before use and identifies unsanitized data as an XSS risk.
Consider template context, uploaded HTML, remote URLs, CSS, and JavaScript attacker-controlled unless your application constrains them. Recommended controls include:
- Escape normal Django variables and sanitize any intentionally allowed rich HTML with a narrowly defined allowlist.
- Do not let users submit arbitrary URLs for the renderer to fetch. Permit only known hosts and schemes.
- Run conversion in a separate worker or container with no unnecessary credentials, filesystem access, or network reachability.
- Use AppArmor or SELinux. The project’s AppArmor guidance explains that
--disable-local-file-accesslimits local-file access but cannot replace operating-system confinement if the binary has a vulnerability. - Apply request authentication, authorization, rate limits, and audit logging to PDF endpoints.
Never place secrets in the HTML, command line, or environment visible to the renderer. If a document needs private images, issue short-lived, narrowly scoped URLs or provide a controlled authenticated fetch mechanism rather than exposing an entire storage bucket.
Performance, reliability, and deployment
Keep web requests predictable
Conversion consumes CPU and memory and can block a web worker while remote assets load. For user-triggered, small documents, a synchronous response may be acceptable. For large reports, batches, or documents containing many images, queue a background job, store the result in private object storage, and let the user download it after completion. Set an application timeout that is shorter than your proxy timeout and log the wkhtmltopdf exit status and stderr.
Build a reproducible renderer image
- Pin Django, the wrapper, and the wkhtmltopdf binary version together.
- Install the same fonts and locale in development, CI, and production.
- Run smoke tests that compare page count, key text, images, and page breaks on every image rebuild.
- Rebuild when security updates require it; Django’s December 4, 2024 security release notice listed fixes for Django 5.1.4, 5.0.10, and 4.2.17.
The stable wkhtmltopdf release being from 2020 means you should not treat an unpinned operating-system package as a maintenance strategy. Record the exact command output from wkhtmltopdf --version with each deployment.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Best Value
Common failures and precise fixes
| Symptom | Likely cause | Fix |
|---|---|---|
No wkhtmltopdf executable found |
Binary is absent or not on the service account’s PATH. | Install it and set WKHTMLTOPDF_BIN or WKHTMLTOPDF_CMD to the absolute path; verify it as the web-worker user. |
| PDF is blank or missing CSS | Asset URL is relative, inaccessible, or blocked by authentication. | Use reachable absolute URLs, check worker egress and TLS, and inspect the generated HTML with the wrapper’s html/debug facilities. |
| Images or fonts do not appear | Missing files, unsupported format, certificate failure, or local-file restrictions. | Test each asset URL from the renderer environment, install required fonts, and choose a controlled HTTPS asset strategy. |
| Modern chart is empty | JavaScript relies on browser APIs or finishes after conversion. | Render the chart server-side, simplify the script, or use a browser engine such as Puppeteer for dynamic pages. |
| Page breaks differ between machines | Different binary, patched-Qt build, fonts, locale, or CSS. | Pin one build, reproduce the font set, set page dimensions explicitly, and run visual regression fixtures. |
| Conversion hangs | Remote resource timeout, infinite script, or a blocked network call. | Remove unneeded external calls, impose worker and process timeouts, restrict egress, and capture stderr for the failing URL. |
| Server security alert | Untrusted HTML/JS reached the renderer. | Stop processing that input, sanitize with an allowlist, isolate the worker with AppArmor/SELinux, and review exposed credentials and filesystem paths. |
When another rendering engine is a better fit
Choose based on the templates you actually ship rather than on a generic feature list:
| Need | Candidate | Reason to consider it |
|---|---|---|
| Controlled, mostly static reports | WeasyPrint | The wkhtmltopdf project specifically suggests it for report generation when you control the HTML. |
| Commercial support or specialized pagination | Prince | The project identifies Prince as a commercial report-generation option. |
| Modern, JavaScript-heavy pages | Puppeteer | The project recommends a browser-based engine for dynamic JavaScript sites. |
Compare CSS and pagination fidelity, JavaScript timing, licensing and operating cost, binary age and patching, isolation requirements, fonts, and deployment reproducibility on a sample of your real documents.
Or skip the browser setup
If your requirement is simply “give me a clean screenshot or PDF of a URL,” ScreenshotNeo provides a website screenshot API and MCP server rather than a local wkhtmltopdf installation. A single GET request returns PNG, JPEG, WebP, or PDF. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing result in X-Page-Verdict and X-Billed headers.
See the ScreenshotNeo API documentation for all options. This cURL request captures a URL as WebP:
Free tools Windows power users keep installed
One-click scans. No signup required.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://your-domain.example.com/invoices/123 -o shot.webp
Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://your-domain.example.com/invoices/123"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://your-domain.example.com/invoices/123' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`ScreenshotNeo returned ${res.status}`);
const buffer = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', buffer));
ScreenshotNeo also supports full-page capture with lazy images loaded, CSS-selector element capture, dark mode, 12 device presets and custom viewports, retina scale, PDF paper size/margins/landscape/page ranges, HTML/CSS-to-image, custom CSS and JavaScript, click-before-capture, hidden selectors, selector/delay/network-idle waits, request and resource blocking, custom headers/cookies/user agents/Authorization, timezone and geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed public-image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, OpenAPI, and compatible parameter names used by other screenshot APIs. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.
The Free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free, and every feature is available on every plan. Create a free ScreenshotNeo account.
Frequently Asked Questions
Should I generate a PDF during the HTTP request or in a job queue?
Use a synchronous response only for small, predictable documents. Queue large, image-heavy, or batch reports so a slow remote asset or renderer process cannot occupy a web worker indefinitely.
How can I prove a wkhtmltopdf upgrade did not change invoices?
Keep fixed fixtures covering short and multi-page invoices, missing data, long descriptions, images, and page breaks; compare extracted text and rendered pages in CI using the exact production fonts and binary.
The Bottom Line
wkhtmltopdf remains practical for controlled Django templates when you pin the old 0.12.6-era toolchain, make assets reachable, test pagination, and isolate every conversion process. Do not feed it unsanitized user HTML or JavaScript.
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.




