Inkspan › Guides › wkhtmltopdf
Every replacement for wkhtmltopdf makes a trade you should understand before you start rewriting templates. This guide measures four of them on the same document and tells you which fits — including the cases where the right answer is free software rather than a paid product.
If your HTML needs JavaScript, use a Chromium-based renderer — Playwright or Puppeteer as a library, or Gotenberg as a self-hosted HTTP service. If your HTML is server-rendered and static — which most invoices, statements and reports are — WeasyPrint is faster, lighter and free. wkhtmltopdf executed JavaScript, so moving to a non-browser engine can be a feature regression. Check that first; it decides everything else.
| Fact | Detail |
|---|---|
| Last release | 0.12.6, 10 June 2020 |
| Repository | Archived and read-only; the packaging repo was archived 28 August 2023 |
| Engine | A patched Qt WebKit fork, years behind modern browsers on CSS |
| Debian | Removed for Trixie — bug #1091821, "RoQA; upstream dead; depends on qtwebkit". Still in bullseye and bookworm. |
| Open CVE | CVE-2022-35583 — SSRF, CVSS v3.1 9.8, no upstream fix |
Most comparisons lead with "critical 9.8 vulnerability" and stop. The fuller picture: Ubuntu's security team deferred the fix and attributes the issue to applications passing unsanitised user-supplied HTML to the renderer, not to a defect in wkhtmltopdf itself.
That matters twice. If you only ever render your own templates, your exposure is far lower than the score implies. And if you do render untrusted HTML, this class of SSRF follows you to whichever engine you choose — any renderer that fetches remote subresources can be pointed at your internal network. Migrating does not fix it; restricting the renderer's network egress does.
The strongest honest reason to move is simpler: an unmaintained engine with no security patches, dropped by your distro, that will eventually stop building.
wkhtmltopdf ran JavaScript. Qt WebKit was a real browser engine, so --javascript-delay and --window-status existed and people relied on them: charts drawn client-side, tables populated after load, templates waiting on a fetch. Half the replacement options cannot do this at all.
Run one test before choosing anything. Render this and read the output:
<p id="out">JS_DID_NOT_RUN</p>
<script>document.getElementById('out').textContent = 'JS_DID_RUN';</script>
You need a browser engine:
Skip the WeasyPrint sections below; they will not work for you.
Your templates are server-rendered — the common case for invoices, statements, contracts and reports. Every option is open to you, and the lightest ones are the best:
No replacement paginates identically to Qt WebKit. Teams report the same document breaking across a different number of pages after switching. Budget time to adjust @page rules and page-break CSS on your busiest templates, and diff output page-by-page before shipping.
| Option | Engine | JavaScript | Runs as | Best fit |
|---|---|---|---|---|
| WeasyPrint | Own CSS layout engine (Python) | No | Library | Server-rendered documents; strong @page / paged-media support |
| Playwright / Puppeteer | Chromium | Yes | Library + browser binary | JS-dependent pages; closest functional match to wkhtmltopdf |
| Gotenberg | Chromium | Yes | Docker HTTP service | Several apps sharing one renderer; keeps Chromium out of your app image |
| Typst | Purpose-built typesetter | No | Binary / library | High volume or typography-critical output; not an HTML drop-in |
| Commercial CSS engines | Proprietary (e.g. Prince) | Varies | Licensed binary or API | Print-grade pagination, PDF/A, complex page rules |
| Hosted APIs | Varies — ask | Varies — ask | HTTP call | No renderer to install, patch or scale |
Comparisons of these tools are usually prose. Here are numbers. One realistic A4 invoice — eight line items, CSS grid, flexbox, striped table, @page footer with page counters — rendered five times per path, median reported.
| Path | Median | Output | Notes |
|---|---|---|---|
| Chromium, warm browser | 71 ms | 42.4 KB | Fastest by a wide margin — if you keep a browser alive |
| WeasyPrint, in-process | 182 ms | 17.3 KB | Free; 651 ms one-time import cost |
| Hosted API (Inkspan) | 871 ms | 17.3 KB | Includes network round-trip from the test machine |
| Chromium, cold process | 1,499 ms | 42.4 KB | Fresh process per run — proxy for a serverless cold start |
pip install weasyprint works on your platform and static HTML is enough, that is the answer. Anyone selling you an API should be willing to say so.| wkhtmltopdf flag | WeasyPrint | Chromium (Playwright / Puppeteer) |
|---|---|---|
--page-size A4 | @page { size: A4 } | format: 'A4' |
--orientation Landscape | @page { size: A4 landscape } | landscape: true |
-T -B -L -R margins | @page { margin: ... } | margin: { top, bottom, left, right } |
--header-html / --footer-html | Margin boxes: @top-center, @bottom-center | headerTemplate / footerTemplate + displayHeaderFooter |
[page] / [topage] | counter(page) / counter(pages) | <span class="pageNumber"> / totalPages |
--javascript-delay, --window-status | No equivalent — no JS execution | waitUntil, waitForSelector, waitForFunction |
--enable-local-file-access | base_url argument | Serve assets over HTTP, or file:// with care |
--print-media-type | Print media is the default | emulateMedia({ media: 'print' }) |
--zoom | CSS sizing / zoom | scale |
--cookie, --custom-header | Fetch the HTML yourself, pass the string | setExtraHTTPHeaders, context.addCookies |
--toc | Rebuild with target-counter in CSS | Generate in your template |
--grayscale, --lowquality | Post-process the PDF | Post-process the PDF |
from weasyprint import HTML
pdf = HTML(string=html, base_url="./assets/").write_pdf()
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(args=["--no-sandbox"])
page = browser.new_page()
page.set_content(html, wait_until="load")
pdf = page.pdf(format="A4", print_background=True)
browser.close() # reuse the browser across requests in production
wkhtmltopdf shipped one binary. Every Chromium-based replacement ships an operations problem. This is not an argument against self-hosting — a warm Chromium was the fastest option we measured — but the costs are real and rarely listed:
fonts.conf.WeasyPrint avoids nearly all of this — no browser, no binary matrix — at the cost of no JavaScript. Its own friction is the Pango and cairo system libraries: unremarkable on Linux, genuinely annoying on Windows and in slim containers.
A hosted API is the right call when rendering isn't your product, your traffic is bursty enough that cold starts dominate, or your platform makes the dependencies painful. Options worth comparing, and the two questions to ask each: what engine, and does it execute JavaScript?
| Service | Notes |
|---|---|
| Gotenberg (self-host or hosted) | Chromium; open source, so you can start hosted and move in-house without changing engine |
| DocRaptor | Commercial CSS engine; aimed at print-grade pagination |
| PDFShift, Api2Pdf, Browserless | Browser-based; confirm current engine and JS support before committing |
| PDFSpark | Chromium via Playwright; free no-signup tier with per-IP rate limits |
| Inkspan | WeasyPrint 65.1; no JavaScript execution; free tier of 100 documents/month |
Disclosure, and the part most vendor guides leave out. This guide is published by Inkspan. Our /v1/generate/html endpoint is hosted WeasyPrint 65.1 — you can verify that yourself in the /Producer field of any PDF we return. So we do not execute JavaScript, and we are not a replacement for Puppeteer or for the JS-dependent parts of a wkhtmltopdf setup. It also means that if you can install WeasyPrint locally, you get the same rendering for free and faster — our own benchmark above says so. What you buy from us is not the renderer: it is not owning the dependencies, and the pipeline below.
Migration guides treat this as an HTML-to-PDF problem. Real wkhtmltopdf deployments rarely were. If yours also shells out to pdftk, qpdf, Ghostscript or wkhtmltoimage, those are separate migrations you haven't scoped — and pdftk has its own abandonment story.
Audit for these before you estimate the work:
You can assemble all of it from open-source parts — pypdf and qpdf cover most cases well. The reason to consider a single API is that each is a separate dependency, with separate system libraries and a separate migration the next time one gets archived. Inkspan exposes them as one authenticated POST each (/pdf/merge, /pdf/split, /pdf/compress, /pdf/watermark, /pdf/to-images, /pdf/fill-form, /extract/text, /extract/tables) — a convenience argument rather than a capability one. Both are legitimate reasons to buy; they are just different reasons.
One workload where the no-JavaScript trade-off costs nothing: documents generated by LLMs and agents. An agent producing an invoice, report or summary emits Markdown or server-side HTML. It is never rendering a single-page app, so a browser engine buys it nothing but latency and footprint.
For that shape of work the useful primitives are Markdown or template-plus-JSON to PDF, and extraction and translation on the way back in — not waitForSelector. If you are building document generation into an agent pipeline, evaluate on those, and don't pay for a browser you will never use.
If the migration is real work and you need a quarter to plan it properly, there is a middle option the other guides don't mention. newinnovations/wkhtml-packaging continues to publish wkhtmltopdf 0.12.6.1 builds for current distributions, including Debian 13 and Ubuntu 26.04, with releases as recent as May 2026.
That is a packaging fork, not a maintained engine: it does not modernise Qt WebKit and it does not fix the SSRF class. Use it to unblock a build, on trusted templates only, with a migration date on the calendar. It is a bridge, not a destination.
pip install weasyprint works on your platform and your HTML is static, use the library. Consider hosted when the dependencies are painful, cold starts dominate your traffic, or you want the operations to be someone else's problem.What was measured. One A4 invoice with eight line items, CSS grid, flexbox, a striped table and an @page footer using page counters. Five iterations per path, median reported, identical input HTML throughout. Chromium 150 driven by Playwright; WeasyPrint 65.1; hosted call to POST /v1/generate/html.
What these numbers do not prove. They come from a single Linux VM, not a controlled benchmark rig, and one document is not a representative corpus — heavy documents with many images or webfonts will shift the ordering. The hosted figure includes network round-trip from the test machine, so your latency depends on your distance from the API. The "cold process" row launches a fresh OS process per iteration as a stand-in for a serverless cold start; it is not a measurement of AWS Lambda. Treat all of it as an order-of-magnitude guide and re-run on your own documents before making a decision that matters.
Related: generate a PDF from HTML in Python · template to PDF · invoice PDF API · merge PDF files