Recommended Free Tools
Use Django to render a controlled HTML template, then invoke the separate wkhtmltopdf executable to convert that HTML into a PDF. The reliable integration has four parts: install and verify a compatible wkhtmltopdf build, render print-oriented HTML, run the converter with bounded resources, and return or store the resulting bytes without deleting them too early.
This guide shows a complete Django pattern, explains static assets and deployment traps, and helps you decide when wkhtmltopdf is the wrong renderer.
How the Django-to-PDF pipeline works
wkhtmltopdf is not a Django library. Django creates HTML; the command-line program reads one or more page objects and writes a PDF file. Global options apply to the whole document, while page options apply to individual input pages. The command also supports cover and table-of-contents objects.
- Load and authorize the record being printed.
- Render a dedicated Django template containing print CSS and accessible assets.
- Write the HTML to a temporary file (or provide an input form supported by your wrapper).
- Run
wkhtmltopdfwith an output path, timeout and checked exit status. - Return the PDF as an attachment or save it in durable storage.
Install and verify the executable
The official downloads page identifies 0.12.6 as the current stable series, released June 11, 2020, and lists binaries by operating system and architecture. That does not guarantee support for every newer distribution. Install the package appropriate for the same OS and architecture used in production, then verify the actual binary:
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 minute#1 Best Overall
wkhtmltopdf --version
which wkhtmltopdf
Some features require the project’s patched Qt build; distribution packages can omit them. Compare the command output and a representative PDF in your deployment image rather than assuming that a package named wkhtmltopdf has identical capabilities.
The project also records that Qt 4 has been unsupported since 2015 and its WebKit had not been updated since 2012. Those statements describe the project’s foundation, not every downstream package. They are nevertheless important maintenance and compatibility warnings.
Create a print-specific Django template
Do not print your interactive page blindly. Create a template such as templates/invoices/invoice.html with deterministic content, print CSS and URLs that the converter can actually reach.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Invoice {{ invoice.number }}</title>
<style>
@page { size: A4; margin: 18mm 14mm; }
body { font: 12px/1.45 sans-serif; color: #222; }
.page-break { page-break-after: always; }
thead { display: table-header-group; }
tr { page-break-inside: avoid; }
</style>
</head>
<body>
<h1>Invoice {{ invoice.number }}</h1>
<p>{{ invoice.customer_name }}</p>
<table>...</table>
</body>
</html>
Use validated model values and Django’s escaping. If you use images, stylesheets or fonts, give the renderer reachable absolute URLs or local paths and test those paths in the production environment. A template that looks correct in a browser can still produce missing assets when the converter runs in a container without DNS, credentials or filesystem access.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Rank #2
A safe illustrative Django view
The following is a teaching outline. It deliberately makes the temporary-directory lifetime visible: returning a path from inside the context manager would delete the PDF before Django could read it. For production, either read the bytes before cleanup, stream from a file whose lifetime extends through the response, or place the file in durable storage.
from django.http import HttpResponse
from django.template.loader import render_to_string
from django.shortcuts import get_object_or_404
from django.conf import settings
import os
import subprocess
import tempfile
def invoice_pdf(request, invoice_id):
invoice = get_object_or_404(Invoice, pk=invoice_id)
# Enforce the same authorization rules as the HTML invoice view.
html = render_to_string("invoices/invoice.html", {"invoice": invoice})
executable = getattr(settings, "WKHTMLTOPDF_PATH", "wkhtmltopdf")
with tempfile.TemporaryDirectory() as directory:
html_path = os.path.join(directory, "invoice.html")
pdf_path = os.path.join(directory, "invoice.pdf")
with open(html_path, "w", encoding="utf-8") as html_file:
html_file.write(html)
try:
completed = subprocess.run(
[executable, "--quiet", html_path, pdf_path],
check=True,
capture_output=True,
text=True,
timeout=30,
)
except (OSError, subprocess.CalledProcessError, subprocess.TimeoutExpired):
# Log stderr and return a controlled 5xx response in real code.
raise
with open(pdf_path, "rb") as pdf_file:
data = pdf_file.read()
response = HttpResponse(data, content_type="application/pdf")
response["Content-Disposition"] = (
f'attachment; filename="invoice-{invoice.number}.pdf"'
)
return response
Configure an absolute executable path when service managers have a different PATH from your shell. Log stderr, elapsed time and the exit code, but avoid logging invoice contents or secrets.
Returning bytes with HttpResponse or a file with FileResponse
Use HttpResponse for bytes
If the PDF is already in memory, HttpResponse(data, content_type="application/pdf") is straightforward. Set Content-Disposition to inline for browser viewing or attachment for a download and provide a safe filename.
Use FileResponse for a binary file
Django’s FileResponse is optimized for binary file-like objects and supports as_attachment=True and a filename. Django closes the supplied file automatically. Do not pass a file that has already been closed by an enclosing with block. For BytesIO, call seek(0) before constructing the response.
Free tools Windows power users keep installed
One-click scans. No signup required.
from django.http import FileResponse
pdf_file.seek(0)
return FileResponse(
pdf_file,
as_attachment=True,
filename="invoice-123.pdf",
content_type="application/pdf",
)
Options that matter in real documents
- Page geometry: set paper size, orientation and margins explicitly so deployment defaults cannot change pagination.
- Long pages: use print CSS, repeating table headers and deliberate page breaks; verify rows containing images or long text.
- JavaScript: only rely on scripts after testing the actual build. A converter based on old WebKit is not equivalent to a current browser.
- Remote resources: ensure DNS, TLS, authentication and local file permissions work from the worker process. Prefer bundled, versioned assets for invoices and reports.
- Headers, footers and covers: pass the relevant command options in a list, never through a shell string. The command supports separate cover and table-of-contents objects when your document needs them.
- Concurrency: each conversion is a child process. Limit simultaneous jobs, use a queue for expensive reports, and cap request timeouts.
Security requirements
The official downloads page 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!
Treat this as a hard boundary.
- Render controlled templates with validated data rather than accepting arbitrary markup.
- Do not interpolate user text into command arguments. Pass an argument array to
subprocess.runwithshell=False(the default). - Run the converter as a low-privilege account, restrict outbound network access where possible, and isolate temporary directories.
- Consider operating-system controls such as AppArmor or SELinux, which the project status page recommends.
- Authorize the invoice or report before rendering it; a PDF endpoint must not become an object-ID enumeration leak.
When wkhtmltopdf is a poor fit
Choose based on the page’s JavaScript needs, HTML/CSS fidelity, security and maintenance posture, deployment packaging, and licensing cost.
| Requirement | Practical direction |
|---|---|
| Controlled reports with modest CSS and little JavaScript | wkhtmltopdf can be workable after build and output validation. |
| Modern, JavaScript-heavy application pages | The project status recommends a browser automation tool such as Puppeteer; evaluate isolation and operating cost separately. |
| Application-controlled reports where a maintained HTML renderer is preferred | The project names WeasyPrint or commercial Prince as alternatives; compare fidelity, packaging and licensing before switching. |
| Untrusted HTML | Do not feed it directly to wkhtmltopdf. Sanitize and isolate, or redesign the workflow. |
These are project recommendations, not guarantees of drop-in compatibility. Rebuild representative documents and compare pagination, fonts, links, forms and charts before changing engines.
Or skip the browser setup
If your actual need is a clean image or PDF of a URL rather than server-side Django template rendering, ScreenshotNeo makes one HTTP request. It accepts consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP server lets Claude, Cursor and other MCP clients call take_screenshot, get_page_info and capture_pdf.
See the ScreenshotNeo API documentation for options such as full-page lazy-image loading, CSS-selector element capture, device presets, custom CSS/JavaScript, cookies and headers, waiting conditions, PDF paper settings, signed webhooks and bulk capture.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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}`);
There is a free allowance of 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account when a hosted capture endpoint fits your workflow.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Troubleshooting checklist
FileNotFoundError or “command not found”
The executable is absent or not on the service user’s PATH. Install a supported binary, set WKHTMLTOPDF_PATH to its absolute path, and verify it as the same user that runs Django.
Exit status is non-zero
Capture stderr and reproduce the exact argument list in the deployment container. Check unreadable input files, unwritable output directories, malformed HTML, missing patched-Qt features and inaccessible remote assets.
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 →Blank pages or missing images
Inspect the generated HTML, test every asset URL from the worker environment, and replace browser-only relative paths with reachable absolute URLs or local files. Confirm that authentication headers and certificates are available to the converter.
Best Value
Conversion hangs
Set a subprocess timeout, terminate the child process on expiry, and move large jobs to a queue. Look for JavaScript waiting forever, unreachable hosts and resource-heavy pages.
The response downloads a zero-byte or missing PDF
Ensure the process completed successfully, the output exists and has non-zero size, and the file or bytes remain alive until Django has consumed them. With BytesIO, seek to position zero.
Layout differs between laptop and production
Compare executable versions, patched-Qt status, fonts, locale, timezone, available resources and network access. Build and test the same container or image used in production.
Operational checklist before launch
- Pin and record the tested wkhtmltopdf build and OS image.
- Render representative short, long, multilingual and image-heavy documents.
- Measure conversion time and memory under expected concurrency.
- Set process, request and queue timeouts with cleanup on failure.
- Protect endpoints with authorization and rate limits.
- Keep templates controlled and treat every user-supplied HTML or script as hostile.
- Monitor exit codes, stderr, PDF size and page-count anomalies without exposing sensitive data.
Frequently Asked Questions
Can I generate a PDF without writing an HTML file first?
wkhtmltopdf consumes HTML input and writes a PDF output; a wrapper may support other input forms, but the dependable Django pattern is to render and persist temporary HTML before invoking the executable.
Should I use a Python wkhtmltopdf wrapper?
A wrapper can simplify argument construction, but it does not remove the need to install and verify the executable, enforce security boundaries, handle timeouts and test the build used in deployment.
Is wkhtmltopdf a current browser engine?
No. The project documents its Qt 4 and old WebKit foundations, so modern JavaScript and CSS may require a different renderer.
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.




