October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsWindows FixRecommendedWindows errors stealing your time? Find the fix fastScan stability, cleanup and performance issues.Fix NowOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
Django

Creating PDFs with Django and wkhtmltopdf (Setup, Code, Security, and Troubleshooting)

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

  1. Build a normal Django view and template.
  2. Render the template to a string, with absolute asset URLs or assets available to the renderer.
  3. Pass the string and conversion options to wkhtmltopdf.
  4. Return the bytes with Content-Disposition: inline or attachment.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
/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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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 print and 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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-access limits 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Read next

Recommended PC Tool
Recommended PC Tool
Windows Errors? Fix Them Before They SpreadFree repair scan
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.