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

To convert Django HTML to PDF, render the template to a string, pass that string to a PDF engine such as xhtml2pdf or WeasyPrint, resolve static and media files explicitly, and return the resulting bytes from a Django view with content_type="application/pdf". The renderer—not Django itself—performs the HTML-to-PDF conversion.

The conversion pipeline

  1. Load and render a Django template with its context.
  2. Give the HTML to a PDF renderer.
  3. Resolve stylesheets, images, and fonts through a known filesystem path or URL callback.
  4. Write the generated bytes to an HttpResponse.
  5. Test pagination, fonts, links, images, and long tables in your project.

Keep the template used for PDF output deliberately print-oriented. Browser-only assumptions—responsive breakpoints, JavaScript layout changes, cross-origin assets, or authenticated URLs—may not work in a server-side renderer.

Option 1: xhtml2pdf in a Django view

xhtml2pdf is a pure-Python converter built with ReportLab, html5lib, and pypdf. It supports HTML5, CSS 2.1, and part of CSS 3, making it suitable for invoices, receipts, letters, and other controlled layouts. Its documented entry point is pisa.CreatePDF(src, dest=...).

Install the dependencies

python -m pip install Django xhtml2pdf

Create the PDF view

from io import BytesIO

from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.template.loader import get_template
from xhtml2pdf import pisa

from .models import Invoice


def invoice_pdf(request, invoice_id):
    invoice = get_object_or_404(Invoice, pk=invoice_id)
    html = get_template("billing/invoice.html").render({"invoice": invoice})

    output = BytesIO()
    status = pisa.CreatePDF(
        src=html,
        dest=output,
        path="/srv/app/templates/",
    )
    if status.err:
        return HttpResponse("PDF generation failed", status=500)

    response = HttpResponse(output.getvalue(), content_type="application/pdf")
    response["Content-Disposition"] = (
        f'attachment; filename="invoice-{invoice_id}.pdf"'
    )
    return response

The path argument supplies a base directory for relative resources. Adapt it to your deployment; it is not a universal path. For inline browser viewing, use inline instead of attachment in the disposition value.

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

Add the URL

from django.urls import path
from .views import invoice_pdf

urlpatterns = [
    path("invoices/<int:invoice_id>/pdf/", invoice_pdf, name="invoice-pdf"),
]

Use a PDF-friendly template

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    @page { size: A4; margin: 18mm; }
    body { font-family: DejaVu Sans, sans-serif; font-size: 10pt; }
    table { width: 100%; border-collapse: collapse; }
    th, td { border: 1px solid #bbb; padding: 5px; }
    thead { display: table-header-group; }
    tr { page-break-inside: avoid; }
  </style>
</head>
<body>
  <h1>Invoice {{ invoice.number }}</h1>
  <p>Issued: {{ invoice.issued_at|date:"Y-m-d" }}</p>
  <table>
    <thead><tr><th>Description</th><th>Amount</th></tr></thead>
    <tbody>
      {% for line in invoice.lines.all %}
      <tr><td>{{ line.description }}</td><td>{{ line.amount }}</td></tr>
      {% endfor %}
    </tbody>
  </table>
</body>
</html>

Django auto-escapes template variables in normal HTML contexts. Do not apply safe, mark_safe, or disabled autoescaping to user-authored text unless it has been sanitized for the output format.

Static files, media, fonts, and URLs

A PDF process does not share the browser’s document context. Relative URLs must resolve through a deterministic base path or callback. For xhtml2pdf, link_callback can rewrite a URI; the resulting path is still checked by the renderer’s resource policy.

Map Django assets to approved files

from pathlib import Path
from django.conf import settings
from django.contrib.staticfiles import finders


def link_callback(uri, rel):
    if uri.startswith(settings.STATIC_URL):
        path = finders.find(uri[len(settings.STATIC_URL):])
    elif uri.startswith(settings.MEDIA_URL):
        path = Path(settings.MEDIA_ROOT) / uri[len(settings.MEDIA_URL):]
    else:
        path = None

    if not path or not Path(path).exists():
        raise FileNotFoundError(f"PDF asset not found: {uri}")
    return str(path)

Pass link_callback=link_callback to CreatePDF. In production, ensure collectstatic has populated the directory and that uploaded media paths cannot escape MEDIA_ROOT. Remote assets should use an explicit allowlist rather than arbitrary user-provided URLs.

Absolute URLs when required

If a renderer must fetch an HTTP asset, use a configured, canonical host and credentials that are safe for the PDF worker. A renderer may not have the user’s session cookie, and private URLs can fail or accidentally expose data. Prefer local files for stylesheets, images, and fonts whenever possible.

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

Choosing a renderer

Renderer Best fit Important trade-offs
xhtml2pdf Python-native invoices and documents with a constrained layout CSS support is HTML5, CSS 2.1, and some CSS 3; responsive media-query conditions are ignored, although all, print, and pdf media types are honored. Asset paths and policies require explicit configuration.
WeasyPrint CSS paged-media rules, hyperlinks, bookmarks, and attachments Verify the installed release’s feature set and operating-system libraries before deployment. Compare its Python and system dependencies with your container image.
wkhtmltopdf via django-wkhtmltopdf Systems already standardized on wkhtmltopdf The Django wrapper provides PDFTemplateView, but the engine, JavaScript behavior, maintenance, and packaging should be evaluated for a new project.

Choose by supported CSS and paged-media rules, JavaScript or browser fidelity, asset and font resolution, SSRF controls, Python and operating-system dependencies, container complexity, concurrent-request behavior, and maintenance. Do not select from a benchmark that was not run against your templates; measure your own workload.

WeasyPrint example

WeasyPrint’s API documentation describes broad W3C CSS support and PDF output with hyperlinks, bookmarks, and attachments. A minimal view is:

from io import BytesIO
from django.http import HttpResponse
from django.template.loader import render_to_string
from weasyprint import HTML


def report_pdf(request):
    html = render_to_string("reports/report.html", {"title": "Report"})
    pdf_bytes = HTML(string=html, base_url=request.build_absolute_uri("/")).write_pdf()
    response = HttpResponse(pdf_bytes, content_type="application/pdf")
    response["Content-Disposition"] = 'attachment; filename="report.pdf"'
    return response

Use a carefully controlled base_url. It can make relative resources convenient, but allowing arbitrary remote content turns PDF generation into a server-side request surface.

Security controls you should not skip

xhtml2pdf’s security documentation states that a document can determine which files the converter opens and which hosts it contacts. Its default policy refuses destinations resolving to internal addresses and local reads outside the document directory, while public HTTP(S) remains available.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Keep a restrictive resource_policy; grant only required asset roots and hosts.
  • Block loopback, link-local, private-network, metadata-service, and other internal destinations.
  • Set network timeouts, maximum HTML and output sizes, and request or job limits.
  • Validate uploaded templates and rich-text fields. Treat stored HTML as untrusted.
  • Do not use safe or mark_safe to silence escaping errors.
  • Run conversion in an isolated worker or container when documents contain user-controlled content.

Django’s security guidance covers auto-escaping, uploaded files, and the risks of disabled escaping; apply those controls before handing HTML to any renderer.

Testing and troubleshooting

Blank or missing images and CSS

Cause: a relative URL has no usable base, collectstatic was not run, or the callback returned a nonexistent path. Log every resolved URI, verify the file inside the worker/container, and pass path, base_url, or link_callback explicitly.

Fonts are substituted or text wraps differently

Install the required fonts in the image, reference them through approved local paths, and test on the same operating-system image used in production. Font availability differs between a laptop and a container.

CSS looks correct in Chrome but not in the PDF

PDF engines are not identical browsers. Reduce layout to the engine’s supported CSS, add print-specific rules, and avoid relying on ignored responsive media-query conditions in xhtml2pdf. If paged-media features are central, evaluate WeasyPrint.

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.

Remote images time out or leak data

Use local copies or an allowlist, set timeouts, and reject private or internal addresses. Do not make the resource policy permissive just to hide an asset-resolution bug.

Long tables split badly

Use a real thead, print-friendly table styles, and row rules such as page-break-inside: avoid. Add regression fixtures with enough rows to cross several pages.

The response is slow under load

Conversion is CPU- and I/O-intensive. Keep it out of latency-sensitive request paths when documents are large, queue jobs, cap concurrency, cache immutable PDFs, and measure memory, duration, failure rate, and output size using your own templates.

Regression checklist

  • Page breaks occur at intentional boundaries.
  • Fonts, logos, uploaded images, and CSS load in the production image.
  • Links point to the intended destinations.
  • Long tables repeat headers and do not overlap footers.
  • Dates, currency, time zones, and localization are correct.
  • Unauthorized users cannot request another user’s document.
  • Malformed HTML and failed resources return a controlled error.
  • Generated files stay within configured size and time limits.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your requirement is simply a reliable screenshot or PDF of a URL rather than rendering a Django template inside your application, ScreenshotNeo provides a website screenshot API. Its cleanup step accepts cookie-consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result. It also offers an MCP server for AI agents, including Claude and Cursor.

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

For a PDF or image generated from a deployed page, see the ScreenshotNeo API documentation and call:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

There is a free allowance of 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account to try it.

FAQ

Can Django itself create a PDF?

No. Django renders the HTML and serves the response; a separate renderer creates the PDF bytes.

Which engine should I start with?

Start with xhtml2pdf for a Python-native, controlled document. Evaluate WeasyPrint for paged-media navigation features or an existing wkhtmltopdf standard.

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

Why does a PDF need a resource policy?

The renderer may read files and contact hosts while resolving assets. A policy limits that access and reduces accidental data exposure and SSRF risk.

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.