Generating a PDF from HTML sounds like a solved problem until you actually have to pick a tool for it. There are a dozen libraries and a handful of hosted services, and the right choice depends less on which one is trending and more on what you're optimizing for: rendering fidelity, how much infrastructure you're willing to run, or a document that was built for print in the first place. This article groups the options into three approaches, explains the tradeoffs of each, and includes a working code example for every option so you can see exactly what integrating each one looks like.
How HTML-to-PDF generation works
At a high level, every tool in this space solves the same problem: take HTML and CSS and turn it into a fixed, paginated document. How they do that splits into three broad strategies.
The first strategy is to reuse an actual web browser. Chromium already knows how to render modern CSS, execute JavaScript, and lay out a page exactly the way a user would see it — so tools like Puppeteer and Playwright drive a real (headless) browser and ask it to print to PDF.
The second strategy is to skip the browser entirely and use a purpose-built rendering engine that understands HTML and CSS well enough to typeset a document, without needing to run JavaScript or replicate every quirk of a browser's rendering pipeline. WeasyPrint and Prince fall into this category.
The third strategy is to outsource the whole problem: send HTML or a URL to a hosted API and get a PDF back, without installing or operating any rendering engine yourself. That's where a managed service like PDFGate fits in.
Each approach has a real, defensible use case. Let's go through them.
Option 1: Puppeteer
Puppeteer is a Node.js library that controls headless Chromium, and it exposes PDF generation through a simple page.pdf() call. Because it's driving an actual browser, whatever renders correctly in Chrome will render correctly in the PDF — including modern CSS, web fonts, and content produced by JavaScript.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle0' });
await page.pdf({
path: 'output.pdf',
format: 'A4',
printBackground: true,
});
await browser.close();
})();
Puppeteer is a strong choice when:
- The HTML already looks correct in Chrome.
- JavaScript must execute before the PDF is generated (client-side rendering, charts, dynamically loaded content).
- The page relies on modern CSS or a frontend framework.
- You want a free, self-hosted option and are comfortable running Chromium yourself.
The honest downside is everything that comes after "self-hosted." Chromium has to be installed and kept up to date. Browser processes are memory-hungry, and running many of them concurrently means you're now managing a browser pool, not just calling a function. You have to handle timeouts, failed page loads, missing fonts and assets, security sandboxing, request queues, and the browser's own lifecycle (crashes, zombie processes, restarts). Print-specific details — headers, footers, page breaks, page numbers — are possible but usually take extra CSS and configuration to get right.
None of this makes Puppeteer a bad choice. It makes it a choice that comes with infrastructure responsibilities, which matters a lot once you're generating PDFs in production rather than in a script on your laptop.
Option 2: Playwright
Playwright is Microsoft's browser automation library, and it offers equivalent PDF generation through its own page API. Functionally, it solves the same problem as Puppeteer in much the same way — headless Chromium under the hood, browser-accurate rendering, JavaScript execution before printing.
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.pdf({
path: 'output.pdf',
format: 'A4',
printBackground: true,
});
await browser.close();
})();
Playwright tends to make the most sense for teams that are already using it for end-to-end testing or scraping. If Playwright is already part of your stack, adding PDF generation is a small incremental step rather than a new dependency to evaluate and maintain.
If PDF generation is the only thing you need, though, Playwright brings more tooling than the job requires — multi-browser support, test runners, and automation features you won't touch just to print a document. And it carries the same operational costs as Puppeteer: Chromium installation and updates, memory usage, concurrency and scaling, timeouts, fonts, assets, and browser lifecycle management.
This is the natural point where it's worth asking: do you actually want to own a browser fleet, or do you just want a PDF? That question is what motivates the third approach later in this article — but first, there's a whole other category of tool worth understanding.
Option 3: WeasyPrint
WeasyPrint is a Python-based rendering engine that converts HTML and CSS directly into a PDF. It does not run a full browser. Instead, it implements its own rendering of HTML/CSS, with a particular focus on print-oriented layout rather than browser-perfect rendering of modern web applications.
That distinction matters. A browser engine tries to print a webpage. A dedicated typesetting engine tries to produce a document.
from weasyprint import HTML
# From a URL
HTML('https://example.com').write_pdf('output.pdf')
# From an HTML string
HTML(string='<h1>Invoice #1042</h1><p>Total due: $500</p>').write_pdf('output.pdf')
WeasyPrint is a good fit when you're already working in a Python codebase and want to generate PDFs — invoices, reports, certificates — without pulling in a browser dependency. It's lightweight compared to running Chromium, and it's well suited to documents that were designed for print from the start, using CSS features like @page rules, page breaks, and print-specific styling.
The tradeoff is that WeasyPrint's rendering isn't identical to a browser's. If your HTML relies on JavaScript execution, complex modern CSS layout, or pixel-perfect parity with what you see in Chrome, you may run into gaps. For content that was actually authored with print output in mind, though, this usually isn't a practical limitation — it's the intended use case.
It's also worth a quick note here: some readers will be thinking of xhtml2pdf instead, another Python HTML-to-PDF library. Based on the kind of print-focused, CSS-driven use case described above, WeasyPrint is the more actively relevant comparison for this article.
Option 4: Prince
Prince is a commercial HTML-to-PDF engine built for professional publishing. It's less "convert this webpage" and more "typeset this document" — full support for CSS Paged Media, running headers and footers, page numbering, table of contents generation, and the kind of structured layout you'd expect from books, reports, and other long-form print documents.
Prince is used primarily as a command-line tool (with bindings available for several languages):
prince invoice.html -o invoice.pdf
Prince tends to show up in workflows where the output quality bar is high and the documents are structurally complex: technical manuals, generated books, formal reports. If you need fine-grained control over pagination and print typography, Prince is built specifically for that.
The obvious tradeoff is licensing cost — Prince is a commercial product, not a free library. That's worth keeping in mind when you're comparing it to the "free" options above; it's not competing on price, it's competing on typesetting capability.
Why wkhtmltopdf is no longer the default choice
Any article about HTML-to-PDF tools eventually has to address wkhtmltopdf, because for years it was one of the most widely used options in this space. It renders using Qt WebKit rather than a modern Chromium-based engine, and it shipped as a simple command-line tool that many frameworks and libraries wrapped directly.
The important, current fact is this: wkhtmltopdf's main GitHub repository was archived by its owner on January 2, 2023, and is now read-only. That means no further updates, bug fixes, or security patches are being made to it through that repository.
It's worth being balanced about what that means in practice. wkhtmltopdf still exists in many older systems, and migrating an established implementation off of it may not always be an immediate priority — if it's working, stable, and not exposed to untrusted input, ripping it out purely on principle isn't necessarily the best use of your time. That said, it would generally not be the first choice for a new project in 2026. Its underlying rendering engine (WebKit, not a current Chromium build) is outdated relative to modern CSS support, and the project itself is no longer actively maintained. For new work, Puppeteer, Playwright, or WeasyPrint will generally give you better rendering fidelity and a healthier long-term maintenance story.
Option 5: Using a managed API such as PDFGate
The three approaches above share a common theme: browser-based tools give you accuracy but hand you an infrastructure problem, and dedicated engines trade some rendering fidelity for simplicity — but you're still the one running the software. A managed HTML-to-PDF API is the answer to a slightly different question: what if you didn't have to operate any of this yourself?
PDFGate is one example of this category. Under the hood, it also uses modern browser-based rendering — so you get the same accuracy benefits as Puppeteer or Playwright — but instead of installing and operating Chromium infrastructure, you interact with it through an API. The workflow is simple: send HTML or a URL, receive a generated PDF back. You're not managing browser pools, containers, job queues, storage, or scaling — that's the part being outsourced.
Using the official Node.js SDK, generating a PDF looks like this:
import PdfGate from 'pdfgate';
const client = new PdfGate(process.env.PDFGATE_API_KEY);
const doc = await client.generatePdf({
html: '<div><h1>Invoice #1042</h1><p>Total due: $500</p></div>',
});
const pdf = await client.getFile({
documentId: doc.id,
});
Or generate directly from a live URL:
const doc = await client.generatePdf({
url: 'https://example.com/',
scale: 1.3,
preSignedUrlExpiresIn: 3600,
});
The SDK picks the right environment automatically based on your API key (test_ for sandbox, live_ for production), and generatePdf returns a document object and when preSignedUrlExpiresIn is set, it returns a temporary URL to the file.
That tradeoff is worth being upfront about. It's an external, paid service, which means a dependency on a third party and an ongoing cost rather than a one-time infrastructure investment. It's more accurate to describe this as a managed option than simply a "paid" one, though — Prince is also a paid product, so "free versus paid" isn't really the axis that separates these tools. The more useful distinction is who operates the rendering infrastructure: you, or the vendor.
For teams whose PDF needs grow beyond "generate a document," this category typically also offers additional capabilities beyond generation itself — fillable form fields, watermarking, encryption, compression, and digital signing — which can save you from bolting on a separate PDF-processing library later.
Comparison
| Option | Rendering approach | Best for | Main drawback |
|---|---|---|---|
| Puppeteer | Headless Chromium | Node.js applications and modern webpages | You manage Chromium and scaling |
| Playwright | Browser automation with Chromium PDF output | Teams already using Playwright | More tooling than needed for PDF generation alone |
| WeasyPrint | Python HTML/CSS renderer | Python applications and print-oriented documents | Not identical to modern browser rendering |
| Prince | Commercial publishing engine | Books, reports, publishing, and advanced paged media | Commercial licensing |
| wkhtmltopdf | Legacy Qt WebKit renderer | Maintaining older implementations | Archived and based on an outdated engine |
| PDFGate | Managed HTML-to-PDF API | Production applications that don't want to run PDF infrastructure | External paid service |
Which option should you choose?
There isn't one correct answer here — it depends on what you're optimizing for.
Choose Puppeteer when you want browser-accurate rendering and you're comfortable owning the infrastructure that comes with it. Choose Playwright when it's already part of your stack for testing or automation, so PDF generation is a small addition rather than a new dependency. Choose WeasyPrint when you're working in Python and generating documents that were designed for print from the start. Choose Prince when your requirements involve advanced publishing features — pagination, running headers, generated tables of contents — and licensing cost isn't the deciding factor. And choose a managed API like PDFGate when you want browser-based accuracy without taking on the job of operating rendering infrastructure yourself.
Conclusion
HTML-to-PDF generation in 2026 isn't a solved-and-forgotten problem — it's a decision with real tradeoffs on both sides: rendering fidelity, operational burden, licensing, and how much infrastructure you actually want to own. Browser-based tools like Puppeteer and Playwright give you the most accurate rendering at the cost of managing Chromium yourself. Dedicated engines like WeasyPrint and Prince trade some of that browser fidelity for simplicity or advanced typesetting. Legacy tools like wkhtmltopdf are still out there, but their unmaintained status makes them a maintenance choice rather than a new-project choice. And managed APIs like PDFGate exist for teams that want the accuracy of browser rendering without the job of running that infrastructure themselves.
Understanding which category a tool falls into — and why — is more useful than memorizing a list of library names. Once you know what you're optimizing for, the right choice tends to be obvious.
Top comments (0)