Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Fix the driver behind crashes, sound loss and screen glitches3Repair Windows errors before they cause bigger problemsUse Flask and Jinja to produce print-focused HTML, then pass that HTML through WeasyPrint and return the resulting bytes with Flask. Flask supplies templates and application URLs; WeasyPrint is the separate HTML/CSS-to-PDF engine. The Flask-WeasyPrint integration keeps rendering inside Flask’s request context, so URLs for your own routes and static files can be resolved without an extra network request.
This guide builds an endpoint that renders a template, applies print CSS, and sends an inline or downloadable PDF. It also covers in-memory HTML, asset paths, page layout, JavaScript limitations, security, troubleshooting, and an API alternative when you do not want to manage a browser-like rendering setup.
What you need
- A Flask application with a normal
templates/directory and, if needed, astatic/directory. - Flask-WeasyPrint, which provides Flask-aware wrappers around WeasyPrint. Its first-steps documentation uses
pip install flask_weasyprint; check the current installation guidance for the operating-system image and Python version you deploy. See the Flask-WeasyPrint project documentation. - A print-oriented template. Do not assume that a screen layout will paginate correctly.
WeasyPrint itself accepts an absolute URL, a filename, a readable file object, or an in-memory HTML string. Calling write_pdf() without a destination returns PDF bytes; passing a path writes a file instead. The API examples and supported inputs are documented by the official WeasyPrint API reference.
Build a Flask PDF endpoint
1. Install the Python packages
python -m venv .venv
# Activate the environment using the command for your shell
python -m pip install flask flask_weasyprint
Native libraries and compatible versions vary by operating system and deployment image. Follow the current WeasyPrint installation instructions for your target rather than copying an old system-package list.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstall#1 Best Overall
2. Create a print template
Put this file at templates/invoice.html. The url_for calls generate application-root URLs that Flask-WeasyPrint can resolve through Flask’s WSGI layer.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Invoice {{ invoice.number }}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='print.css') }}">
</head>
<body>
<header class="document-header">
<img src="{{ url_for('static', filename='logo.png') }}" alt="Company logo">
<h1>Invoice {{ invoice.number }}</h1>
</header>
<p>Issued: {{ invoice.issued_at }}</p>
<table>
<thead><tr><th>Description</th><th>Qty</th><th>Amount</th></tr></thead>
<tbody>
{% for item in invoice.items %}
<tr><td>{{ item.description }}</td><td>{{ item.quantity }}</td><td>{{ item.amount }}</td></tr>
{% endfor %}
</tbody>
</table>
<p class="total">Total: {{ invoice.total }}</p>
</body>
</html>
Keep user-provided values escaped by Jinja’s default behavior. Only mark content safe after you have deliberately sanitized it.
3. Add print CSS
Save this as static/print.css. The @page rule controls PDF paper and margins, while the break rules reduce awkward splits. Test with your longest realistic records because pagination depends on actual content.
@page {
size: A4;
margin: 18mm 16mm 20mm;
}
* { box-sizing: border-box; }
body {
color: #222;
font: 10.5pt/1.45 sans-serif;
}
.document-header {
display: flex;
align-items: center;
gap: 12px;
border-bottom: 1px solid #bbb;
padding-bottom: 8px;
margin-bottom: 18px;
}
.document-header img { max-width: 140px; max-height: 40px; }
table { width: 100%; border-collapse: collapse; }
th, td { border-bottom: 1px solid #ddd; padding: 6px; text-align: left; }
thead { display: table-header-group; }
tr { break-inside: avoid; }
.total { text-align: right; font-weight: 700; margin-top: 16px; }
@media print { a { color: inherit; text-decoration: none; } }
4. Render and return PDF bytes
Import HTML from flask_weasyprint, not directly from a browser automation library. The following complete application renders a record and returns a PDF response. The attachment filename is optional; change inline to attachment to make most browsers download it instead of displaying it.
Recommended Free Tools
from flask import Flask, make_response, render_template
from flask_weasyprint import HTML
app = Flask(__name__)
@app.get("/invoice/<int:invoice_id>.pdf")
def invoice_pdf(invoice_id):
invoice = load_invoice(invoice_id) # Replace with your database lookup
if invoice is None:
return {"error": "Invoice not found"}, 404
html = render_template("invoice.html", invoice=invoice)
pdf_bytes = HTML(string=html, base_url=request_base_url()).write_pdf()
response = make_response(pdf_bytes)
response.headers["Content-Type"] = "application/pdf"
response.headers["Content-Disposition"] = (
f'inline; filename="invoice-{invoice_id}.pdf"'
)
return response
def request_base_url():
# Use the current request URL as the base for relative assets in this example.
from flask import request
return request.url_root
def load_invoice(invoice_id):
# Demonstration data; replace with validated application data.
return {
"number": str(invoice_id),
"issued_at": "2026-09-29",
"items": [{"description": "Consulting", "quantity": 1, "amount": "$100.00"}],
"total": "$100.00",
}
if __name__ == "__main__":
app.run(debug=True)
The integration is intended for an active request context. In a normal Flask view that context already exists. If a background task or test renders outside a view, Flask-WeasyPrint’s documentation demonstrates creating an application test request context and supplying a base URL so application-root resources can still be resolved. See its request-context guidance.
Rank #2
Depending on the Flask version and your response conventions, you can also use send_file with an in-memory buffer. The essential requirements are unchanged: return the bytes, set Content-Type: application/pdf, and choose a suitable Content-Disposition.
Convert a URL or an HTML string directly
Render an absolute URL
from weasyprint import HTML
HTML("https://weasyprint.org/").write_pdf("/tmp/weasyprint-website.pdf")
This is the official project’s basic URL example. In a Flask application, prefer the Flask-WeasyPrint HTML wrapper when the page contains local routes or static assets.
Render an in-memory string
from weasyprint import HTML
html = "<h1>Report</h1><p>Generated by Flask</p>"
pdf_bytes = HTML(string=html, base_url="https://example.com/").write_pdf()
Set base_url when your HTML uses relative CSS, image, font, or link URLs. Without a usable base, those resources may not load.
What WeasyPrint does—and does not—render
WeasyPrint implements a substantial HTML/CSS print pipeline, including page rules and common layout features, but it is not a full interactive browser. Browser-perfect equivalence is not guaranteed. JavaScript-driven content, canvas output, advanced browser-only CSS, and client-side data fetching may be missing or different.
- If the document is server-rendered by Jinja, WeasyPrint is usually a straightforward in-process choice.
- If the page must execute JavaScript before it becomes complete, evaluate a browser-based or wkhtmltopdf-based integration. The available Flask guidance documents wkhtmltopdf as an option for JavaScript-dependent templates, not as a universal replacement.
- If PDF work is CPU- or memory-intensive, move it to a worker queue, cap concurrency, and return a job status rather than tying up every web request.
Measure with representative documents. No general latency, throughput, or memory number applies to every template and deployment.
Assets, fonts, and page layout
Use stable URLs
Generate static URLs with url_for('static', filename=...). For files outside Flask’s static directory, provide an absolute URL or a readable file path that the renderer is allowed to access. Confirm that the production process can read every image, font, and stylesheet.
Control pagination deliberately
- Use
@pagefor paper size, margins, and (where supported) page decorations. - Keep table headers in a repeating table-header group and avoid splitting rows that must remain together.
- Test long paragraphs, large images, empty sections, and records that span several pages.
- Embed or explicitly expose fonts required by your brand; a missing font can change line wrapping and page count.
Security and reliability checklist
WeasyPrint warns that untrusted HTML or CSS can create security problems. Do not feed arbitrary user markup to the renderer without a threat model and sanitization policy.
- Allow only the URL schemes and hosts your document actually needs; review external resource fetching and redirects.
- Do not let a user choose arbitrary local filenames, file URLs, headers, or cookies.
- Sanitize HTML and CSS if users can edit document content, and isolate rendering credentials from the public request.
- Set request and job time limits, monitor memory and CPU, and reject unexpectedly large documents or images.
- Log renderer failures without logging secrets embedded in URLs or headers.
Flask-WeasyPrint’s WSGI URL fetching can avoid a network round trip for your own application resources, but that convenience does not make external resources trustworthy or guaranteed to be available in production.
Troubleshooting common failures
“No module named flask_weasyprint”
The package was installed into a different interpreter or virtual environment. Activate the environment, run python -m pip show flask_weasyprint, and start Flask with that same Python.
Missing images or CSS
Relative URLs lack a base URL, or the production process cannot access the target. Use Flask’s url_for, pass base_url, and verify the generated URL from the deployment environment.
PDF is blank or content is incomplete
Inspect the rendered HTML separately. Server-side data may be absent, or the page may depend on JavaScript that WeasyPrint does not execute. Render the final HTML with all data and replace client-only work with server-side output when possible.
Unexpected page breaks
Check @page margins, font availability, image dimensions, and break rules. Re-test with realistic long content; a short fixture can hide pagination problems.
Works locally but fails in production
Compare Python, WeasyPrint, Flask-WeasyPrint, native-library, font, and OS-image versions. Confirm filesystem permissions and outbound-resource policy. Avoid copying an unverified OS dependency list between distributions.
Requests become slow or time out
Large images, external assets, and complex layouts consume resources. Reduce asset size, restrict remote fetching, cache stable inputs, and queue expensive jobs. Establish limits from measurements in your own environment rather than a generic benchmark.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server when you need a hosted capture rather than maintaining rendering dependencies. It accepts one GET request and can return PNG, JPEG, WebP, or PDF. Before capture it accepts cookie/consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
Free tools Windows power users keep installed
One-click scans. No signup required.
For a Flask page that is already reachable at a URL, use the API as documented at ScreenshotNeo’s API documentation:
Best Value
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)
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}`);
You can request PDF output and configure options such as full-page capture, CSS-selector element capture, device and viewport settings, retina scale, custom CSS or JavaScript, click and wait actions, blocked resources, headers, cookies, user agent, timezone, geolocation, transparent backgrounds, resizing, a chosen cache TTL, signed links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, and usage reporting. Every feature is on every plan: 1,000 shots per month free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Frequently Asked Questions
Can I generate a PDF without saving an intermediate HTML file?
Yes. Render a Jinja template to a string and pass it to HTML(string=...); write_pdf() returns bytes when no destination is supplied.
Should the endpoint display or download the PDF?
Set Content-Disposition to inline for browser viewing or attachment for a download, while keeping Content-Type as application/pdf.
Is Flask-WeasyPrint suitable for arbitrary customer HTML?
Not without controls. Treat HTML, CSS, and resource URLs as untrusted input, sanitize them, restrict fetching, and isolate rendering according to your threat model.
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.

