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

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Render the Django template to an HTML string, pass that string to a PDF engine, resolve every stylesheet/image/font through an approved path, and return the generated bytes from an HttpResponse. The example below uses xhtml2pdf because it has a Python API that fits directly into a Django view; WeasyPrint and wkhtmltopdf are covered when their rendering model is a better match.

The conversion pipeline

Django does not create PDF files itself. Your view supplies data to a template and produces HTML; a separate renderer interprets that HTML and writes PDF bytes.

  1. Load and render the template with the view context.
  2. Give the resulting HTML to a renderer.
  3. Make relative CSS, image, and font URLs deterministic with a base path or callback.
  4. Check the renderer status and return the bytes with content_type="application/pdf".
  5. Test page breaks, fonts, images, links, and long tables in your project’s test suite.

A complete xhtml2pdf Django view

Install the renderer in the same environment as Django:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
python -m pip install django xhtml2pdf

This view renders billing/invoice.html, writes the PDF to memory, and sends it as a download. Replace the invoice lookup and template path with your application’s code.

from io import BytesIO

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


def invoice_pdf(request, invoice_id):
    invoice = ...  # Fetch and authorize this invoice.
    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

pisa.CreatePDF accepts a file-like dest. The path argument supplies a base directory for relative resources. In production, use an application-specific directory rather than assuming the process working directory.

Template example

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    @page { size: A4; margin: 18mm 15mm; }
    body { font-family: DejaVu Sans, sans-serif; font-size: 10pt; }
    h1 { color: #18324b; }
    .items { width: 100%; border-collapse: collapse; }
    .items th, .items td { border-bottom: 0.5pt solid #bbb; padding: 5pt; }
    .items tr { page-break-inside: avoid; }
  </style>
</head>
<body>
  <h1>Invoice {{ invoice.number }}</h1>
  <p>Customer: {{ invoice.customer_name }}</p>
  <table class="items">
    {% for line in invoice.lines %}
      <tr><td>{{ line.description }}</td>
          <td>{{ line.amount }}</td></tr>
    {% endfor %}
  </table>
</body>
</html>

Django auto-escapes ordinary template variables. Keep that protection enabled; do not mark user text as safe unless it has been sanitized for the exact HTML you permit.

Static files, media, fonts, and links

A PDF process is not a browser tab. It does not automatically know your STATIC_URL, logged-in session, or current site origin. A relative URL such as ../static/invoice.css must resolve to an approved filesystem location or host.

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

Use a callback for controlled URL mapping

xhtml2pdf’s link_callback can rewrite each URI before it is opened. Map Django’s static and media prefixes to known directories, and reject everything else. Keep the callback small and deterministic; an accidental “download any URL” implementation creates both reliability and SSRF risk.

from pathlib import Path
from django.conf import settings


def link_callback(uri, rel):
    if uri.startswith(settings.STATIC_URL):
        relative = uri[len(settings.STATIC_URL):].lstrip("/")
        candidate = (Path(settings.STATIC_ROOT) / relative).resolve()
        root = Path(settings.STATIC_ROOT).resolve()
    elif uri.startswith(settings.MEDIA_URL):
        relative = uri[len(settings.MEDIA_URL):].lstrip("/")
        candidate = (Path(settings.MEDIA_ROOT) / relative).resolve()
        root = Path(settings.MEDIA_ROOT).resolve()
    else:
        raise ValueError(f"Unapproved PDF resource: {uri}")

    if root not in candidate.parents and candidate != root:
        raise ValueError("Resource escapes the configured asset directory")
    return str(candidate)

Pass link_callback=link_callback to CreatePDF. If assets are hosted remotely, allow only the specific hosts you need and configure timeouts and size limits. Fonts must be installed or mapped to files the renderer can read; a browser-only webfont URL is not a guarantee that the PDF engine can load it.

Choose print CSS deliberately

xhtml2pdf honors @media types all, print, and pdf, but does not evaluate responsive media-query conditions. Design a print stylesheet instead of relying on mobile breakpoints. Keep tables simple, avoid layout that depends on unsupported CSS, and use explicit page-break rules for invoices, letters, and repeated sections.

Choosing a renderer

Renderer Best fit Important trade-off
xhtml2pdf Python-native invoices, receipts, letters, and layouts within its supported CSS subset. Implements HTML5, CSS 2.1, and some CSS 3; responsive media-query conditions are ignored. Asset paths and policy need explicit configuration.
WeasyPrint Documents where CSS paged-media rules and PDF navigation are central. Its API documents broad W3C CSS support plus hyperlinks, bookmarks, and attachments. Verify the installed release and operating-system libraries before deployment.
wkhtmltopdf Existing systems standardized on that engine or requiring its established browser-oriented behavior. django-wkhtmltopdf supplies a PDFTemplateView; compare JavaScript behavior, engine maintenance, and container dependencies before choosing it for a new build.

Compare candidates on paged-media CSS, JavaScript fidelity, static/media/font resolution, SSRF controls, Python and system dependencies, container packaging, concurrent-request performance, and maintenance. Do not infer speed or CSS coverage from another deployment; measure representative documents in your own environment.

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

Security and isolation

Rendering is an input-processing boundary. xhtml2pdf’s security model lets a document determine which files are opened and which hosts are contacted; its default policy refuses destinations resolving to internal addresses and local reads outside the document directory, while public HTTP(S) can remain available. Preserve restrictive behavior and add explicit allowlists rather than making the policy permissive to “fix” an asset.

  • Treat user-authored HTML, rich-text fields, uploaded templates, and uploaded images as untrusted.
  • Use Django’s escaping and validation rules; review every use of safe, mark_safe, disabled autoescaping, and stored HTML.
  • Allow only approved filesystem roots and hosts; block internal network ranges.
  • Set request timeouts, maximum document and image sizes, and a maximum output size.
  • Authorize the object before rendering it, and avoid exposing arbitrary template names or URLs through a view.

Returning PDFs reliably

Inline display versus download

Use attachment in Content-Disposition to force a download. Use inline when you want a capable browser to display the PDF. Generate a filename from trusted identifiers and quote it as shown in the example.

Large documents and background jobs

For a short invoice, an in-memory BytesIO response is straightforward. Long reports consume worker memory and request time. Move generation to a task queue, write to controlled temporary or object storage, then return a download URL after authorization. Add cleanup for abandoned temporary files and enforce output-size limits.

Regression tests

Assert the response status, content type, disposition, and the PDF signature (%PDF-). Keep fixture documents that exercise page breaks, long tables, missing assets, Unicode fonts, hyperlinks, and images. Visual comparison or text extraction catches layout regressions that a status-code test cannot.

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

Troubleshooting

Images or CSS are missing

The renderer cannot resolve the URL, the file is outside the permitted root, or the process lacks read permission. Log the URI received by link_callback, map it to an absolute approved path, and verify permissions inside the deployment container.

Fonts show as boxes or fall back

The font is unavailable to the renderer or does not contain the required glyphs. Install or explicitly map a font file, use a family with the needed Unicode coverage, and test the actual production image.

Content is cut off or overlaps

Unsupported CSS, fixed heights, or an unbreakable table row is usually responsible. Remove browser-only layout rules, allow rows to break where acceptable, add explicit page margins, and test with the longest realistic data.

Remote assets cause hangs or security errors

Network access may be blocked by policy, or the host is slow/untrusted. Prefer local collected assets; otherwise allowlist the host, set a timeout, and never permit arbitrary user-supplied URLs.

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

status.err is true

Capture renderer logs and inspect the generated HTML. Common causes include malformed markup, unavailable resources, unsupported CSS, and permission errors. Return a generic failure to the client while logging the document identifier and sanitized diagnostic details.

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 the Django page is reachable over HTTP and you need a PDF or a clean visual capture rather than an in-process renderer, ScreenshotNeo can do it with one request. It accepts cookie/consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup 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.

For PDF output, request the PDF format and pass the URL of your Django route (protect that route with authentication or a signed, short-lived URL):

curl -G "https://api.screenshotneo.com/v1/shot" 
  -d access_key=YOUR_API_KEY 
  --data-urlencode url=https://example.com/invoices/123/ 
  -d format=pdf 
  -o invoice-123.pdf

See the ScreenshotNeo API documentation for authentication and capture options. The same endpoint can set viewport and device presets, wait for a selector, delay or network idle, click before capture, hide selectors, block requests or resource types, supply headers/cookies/user-agent, choose timezone or geolocation, resize images, cache with a chosen TTL, and submit asynchronous jobs with signed webhooks. An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. Bulk capture supports 100 URLs per call, and an OpenAPI specification and usage API are available.

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

Python:

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={
        "access_key": "YOUR_API_KEY",
        "url": "https://example.com/invoices/123/",
        "format": "pdf",
    },
    timeout=90,
)
r.raise_for_status()
open("invoice-123.pdf", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com/invoices/123/',
  format: 'pdf'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('invoice-123.pdf', Buffer.from(await res.arrayBuffer()));

There is a free allowance of 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Sign up for ScreenshotNeo when this URL-based workflow fits your deployment.

Frequently asked questions

Can I convert a Django template without serving a URL?

Yes. Render it with Django’s template loader and pass the resulting string directly to xhtml2pdf or another Python renderer, as in the view above.

Should I use a headless browser instead?

Use a browser-based approach when JavaScript execution and browser-specific layout are requirements. For mostly static, print-oriented documents, a Python renderer usually has fewer moving parts; evaluate the exact CSS and asset behavior of your chosen engine.

How do I handle private Django pages with ScreenshotNeo?

Do not expose an unrestricted page. Create a narrowly scoped, short-lived signed URL or an authenticated capture flow, and ensure sensitive data cannot be fetched by an arbitrary caller.

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

Frequently Asked Questions

Can I convert a Django template without serving a URL?

Yes. Render it with Django’s template loader and pass the resulting HTML string directly to xhtml2pdf or another Python renderer.

Should I use a headless browser instead?

Choose a browser-based approach when JavaScript execution or browser-specific layout is required; use a Python renderer for static, print-oriented documents when its CSS support matches your template.

How do I handle private Django pages with ScreenshotNeo?

Expose only a narrowly scoped, short-lived signed URL or authenticated capture flow, and prevent arbitrary callers from fetching sensitive data.

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.

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.