HTML to PDF in 2026: All Methods Compared with Code
Converting HTML to PDF is a solved problem — in the sense that many tools exist. Choosing the right one for your use case is not trivial, because each method makes different trade-offs on rendering fidelity, CSS support, operational complexity, and cost.
This article covers every major HTML to PDF method available in 2026, with honest pros, cons, and real code for each approach.
For a focused comparison of Puppeteer specifically against a managed API, see Puppeteer vs PDF API. For CSS tips that apply across all Chromium-based methods, see HTML to PDF CSS Tips.
The Methods at a Glance
| Method | Engine | CSS Support | Operational Complexity | Self-Hosted |
|---|---|---|---|---|
| Browser print dialog | Chromium / WebKit | Full | None (manual) | — |
| wkhtmltopdf | QtWebKit (archived) | Partial | Low | Yes |
| Puppeteer / Playwright | Chromium | Full | High | Yes |
| WeasyPrint | Python rendering | Good | Low–Medium | Yes |
| Gotenberg | Chromium + LibreOffice | Full | Medium | Yes |
| Managed PDF API | Chromium | Full | Very Low | No |
1. Browser Print Dialog
The simplest method: window.print() in JavaScript opens the browser's native print dialog, letting the user save as PDF.
// Trigger print dialog
window.print();
// Or add a print button
document.getElementById('print-btn').addEventListener('click', () => {
window.print();
});
Pair with CSS @media print rules to control layout:
@media print {
.no-print { display: none; }
body { font-size: 12pt; }
@page { margin: 20mm; size: A4; }
}
Pros:
- Zero setup, zero cost
- Uses the full browser rendering engine
- No server-side code required
Cons:
- User must manually save as PDF — not automatable
- Print dialog appearance varies by browser and OS
- Page breaks are hard to control reliably
- Headers, footers, and multi-page layout are difficult
- Cannot be triggered silently from a server
Best for: Developer tools, internal dashboards where a "save as PDF" button is sufficient and automation is not needed.
2. wkhtmltopdf
A command-line tool based on the QtWebKit rendering engine. It was the dominant solution for server-side HTML to PDF conversion for many years.
# Install (Debian/Ubuntu)
sudo apt-get install wkhtmltopdf
# Basic conversion
wkhtmltopdf input.html output.pdf
# With options
wkhtmltopdf \
--page-size A4 \
--margin-top 20mm \
--margin-bottom 20mm \
--margin-left 15mm \
--margin-right 15mm \
--encoding utf-8 \
invoice.html invoice.pdf
# From a URL
wkhtmltopdf https://example.com page.pdf
# Python wrapper
import subprocess
def html_to_pdf(html_path: str, output_path: str) -> None:
subprocess.run([
'wkhtmltopdf',
'--page-size', 'A4',
'--margin-top', '20mm',
'--margin-bottom', '20mm',
html_path,
output_path,
], check=True)
Pros:
- Lightweight and fast (lower resource usage than Chromium-based tools)
- Simple CLI interface
- Mature and widely documented
Cons:
- Archived in 2023 — no security patches or bug fixes
- QtWebKit engine is 7–8 years behind modern browsers
- Incomplete CSS Grid and Flexbox support
- Cannot render pages that require JavaScript execution
- CJK font support requires manual font installation
- Not recommended for new projects
Best for: Legacy systems already using wkhtmltopdf that are not yet migrated. For migration guidance, see the wkhtmltopdf migration guide.
3. Puppeteer (Node.js)
Puppeteer is a Node.js library that controls a headless Chromium instance. It was originally built for browser automation and testing; PDF generation is one of its features.
const puppeteer = require('puppeteer');
async function generatePdf(html) {
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
});
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
const pdf = await page.pdf({
format: 'A4',
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
printBackground: true,
});
await browser.close();
return pdf;
}
Pros:
- Full modern CSS support (Grid, Flexbox, custom properties)
- Can run JavaScript before generating the PDF
- Large community and ecosystem
- Identical rendering to Chrome
Cons:
- Requires Chromium installation and version management
- 200–500 MB memory per process
- Cold starts: 1–3 seconds (8–15 seconds on Lambda)
- Browser pool required for concurrent use
- Docker images balloon to 1 GB+
- Not purpose-built for PDF generation
Best for: Low-to-medium volume PDF generation where you are already using Puppeteer for browser testing, or where you need page.evaluate() to run JavaScript before export.
4. Playwright (Node.js, Python, .NET, Java)
Playwright is Microsoft's browser automation library. It supports Chromium, Firefox, and WebKit, and offers a cleaner API than Puppeteer. For PDF generation, it uses Chromium, so the output is identical.
// Node.js
const { chromium } = require('playwright');
async function generatePdf(html) {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
await page.setContent(html, { waitUntil: 'networkidle' });
const pdf = await page.pdf({
format: 'A4',
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
printBackground: true,
});
await browser.close();
return pdf;
}
# Python
import asyncio
from playwright.async_api import async_playwright
async def generate_pdf(html: str) -> bytes:
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page()
await page.set_content(html, wait_until='networkidle')
pdf = await page.pdf(
format='A4',
margin={'top': '20mm', 'bottom': '20mm',
'left': '15mm', 'right': '15mm'},
print_background=True,
)
await browser.close()
return pdf
Pros:
- Cleaner API than Puppeteer
- Multi-language support (Node.js, Python, .NET, Java)
- Active development and maintenance
- Full modern CSS support
Cons:
- Same core problems as Puppeteer: Chromium management, 200–500 MB memory, cold starts
- Playwright installs its own Chromium bundle (
playwright install chromium) — even larger download than Puppeteer - Not purpose-built for PDF generation
Best for: Same situations as Puppeteer, when you prefer Python or .NET, or when you need Firefox/WebKit rendering for testing. If operational problems are the pain point, migrating to a managed API is the higher-ROI move. See the Playwright to PDF API migration guide.
5. WeasyPrint
WeasyPrint is a Python library that converts HTML and CSS to PDF using its own rendering engine (not Chromium). It implements the CSS Paged Media specification directly.
# Install
pip install weasyprint
# CLI
weasyprint input.html output.pdf
from weasyprint import HTML, CSS
def generate_pdf(html: str, css: str = None) -> bytes:
html_doc = HTML(string=html)
stylesheets = [CSS(string=css)] if css else []
return html_doc.write_pdf(stylesheets=stylesheets)
WeasyPrint has excellent support for CSS Paged Media features like @page rules, named pages, and running elements:
@page {
size: A4;
margin: 20mm;
@bottom-center {
content: "Page " counter(page) " of " counter(pages);
}
}
/* Running header/footer */
.header { position: running(header); }
@page { @top-center { content: element(header); } }
Pros:
- Excellent CSS Paged Media support (headers, footers, page counters)
- Lightweight — no Chromium required
- Pure Python — easy to integrate into Django, Flask, FastAPI
- Good for documents with complex multi-page layout
Cons:
- Does not support JavaScript execution
- CSS support is not identical to browsers (some modern properties missing)
- CJK font support requires font installation
- Slower than Chromium for complex pages with many CSS rules
Best for: Python applications generating structured documents (reports, books, contracts) where CSS Paged Media features are important and JavaScript rendering is not needed.
6. Gotenberg
Gotenberg is an open-source Docker service that wraps Chromium and LibreOffice to expose a REST API for document conversion.
# Start Gotenberg with Docker
docker run --rm -p 3000:3000 gotenberg/gotenberg:8
# Convert HTML to PDF
curl \
--request POST http://localhost:3000/forms/chromium/convert/html \
--form files=@index.html \
--form marginTop=20 \
--form marginBottom=20 \
-o output.pdf
# Convert a URL
curl \
--request POST http://localhost:3000/forms/chromium/convert/url \
--form url=https://example.com \
-o output.pdf
import requests
def generate_pdf_gotenberg(html: str) -> bytes:
response = requests.post(
'http://localhost:3000/forms/chromium/convert/html',
files={'files': ('index.html', html, 'text/html')},
data={'marginTop': '20', 'marginBottom': '20'},
)
response.raise_for_status()
return response.content
Pros:
- Language-agnostic REST API
- Full Chromium rendering capabilities
- Also converts Word, Excel, and PowerPoint files (via LibreOffice)
- One-command Docker setup for local development
Cons:
- Requires Docker — difficult to run in serverless environments
- Self-managed scaling (you run and scale the container)
- Large Docker image (Chromium + LibreOffice)
- CJK font support requires additional Docker configuration
- Limited built-in error handling and retry support
Best for: Teams that want a self-hosted REST API with language-agnostic access, and who also need Office document conversion. Kubernetes deployments where you manage scaling. Not suited to serverless.
7. Managed PDF API
A managed PDF API (such as FUNBREW PDF) is a hosted service. You POST HTML and receive PDF bytes. No server infrastructure to manage.
// Node.js
async function generatePdf(html) {
const response = await fetch('https://pdf.funbrew.cloud/api/v1/pdf/generate', {
method: 'POST',
headers: {
'X-API-Key': process.env.FUNBREW_PDF_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html,
options: {
format: 'A4',
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
print_background: true,
},
}),
});
if (!response.ok) throw new Error(`API error: ${response.status}`);
return Buffer.from(await response.arrayBuffer());
}
# Python
import os, requests
def generate_pdf(html: str) -> bytes:
resp = requests.post(
'https://pdf.funbrew.cloud/api/v1/pdf/generate',
headers={
'X-API-Key': os.environ['FUNBREW_PDF_API_KEY'],
'Content-Type': 'application/json',
},
json={
'html': html,
'options': {
'format': 'A4',
'margin': {'top': '20mm', 'bottom': '20mm',
'left': '15mm', 'right': '15mm'},
'print_background': True,
},
},
timeout=30,
)
resp.raise_for_status()
return resp.content
<?php
// PHP
function generatePdf(string $html): string {
$apiKey = getenv('FUNBREW_PDF_API_KEY');
$ch = curl_init('https://pdf.funbrew.cloud/api/v1/pdf/generate');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'X-API-Key: ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'html' => $html,
'options' => ['format' => 'A4', 'print_background' => true],
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) throw new RuntimeException("PDF API error: HTTP $status");
return $body;
}
Pros:
- Zero infrastructure management — start with an API key
- Full Chromium rendering (same CSS support as Puppeteer)
- Auto-scaling for concurrent requests
- No cold starts on your side
- Built-in Japanese and CJK fonts
- Small Docker images (no Chromium on your servers)
- Webhooks, template engine, and email delivery built in
Cons:
- Per-request cost at scale
- External service dependency
- HTML is sent to a third-party server (review your data policy)
Best for: Most production applications where PDF generation is a real feature, serverless environments, and teams that do not want to maintain Chromium infrastructure. Try the Playground to verify rendering quality before committing.
Choosing the Right Method
Decision Guide
Is automation required?
No → Browser print dialog
Is JavaScript execution before export required?
Yes → Puppeteer, Playwright, or Managed API
What is your primary language?
Python (no JS needed) → WeasyPrint or Managed API
Python (JS needed) → Playwright (Python) or Managed API
Node.js → Puppeteer, Playwright, or Managed API
PHP / Ruby / Go → Managed API or Gotenberg
Do you need to convert Office files too?
Yes → Gotenberg
Are you on serverless (Lambda, Vercel, CF Workers)?
Yes → Managed API strongly recommended
Is volume < 30 PDFs/month?
Yes → Managed API free tier or Puppeteer
Do you have a strict policy against sending HTML externally?
Yes → Self-hosted Puppeteer, Playwright, or Gotenberg
Do you want zero infrastructure maintenance?
Yes → Managed API
Summary Table
| Method | Setup | Memory | Serverless | CJK | Cost |
|---|---|---|---|---|---|
| Browser print | None | — | N/A | Native | Free |
| wkhtmltopdf | Low | Low | Hard | Manual | Free (deprecated) |
| Puppeteer | High | 200–500 MB | Hard | Manual | Server cost |
| Playwright | High | 200–500 MB | Hard | Manual | Server cost |
| WeasyPrint | Medium | Low | Easy | Manual | Free |
| Gotenberg | Docker | High | Hard | Docker config | Free (self-hosted) |
| Managed API | None | < 10 MB | Easy | Built-in | Per request |
Conclusion
In 2026, the managed PDF API is the default recommendation for new production applications. It delivers the same Chromium rendering quality as Puppeteer with none of the operational overhead. The only strong reasons to self-host are data-residency requirements or a need to run application JavaScript before export that cannot be moved to server-side rendering.
For teams already running Puppeteer or wkhtmltopdf at scale, a managed API typically becomes cost-effective once you account for maintenance engineering hours. Test your templates in the Playground and check the free plan before deciding.
Related
- Puppeteer vs PDF API: Performance and Cost Compared — Focused comparison of Puppeteer vs managed API
- HTML to PDF API Comparison 2026 — Comparison with code examples for major tools
- wkhtmltopdf Migration Guide — Moving from wkhtmltopdf to a modern solution
- HTML to PDF CSS Tips — CSS techniques that work across all Chromium methods
- PDF API Quickstart by Language — API code in Node.js, Python, PHP, Ruby, Go
- Puppeteer to PDF API Migration Guide — Step-by-step Puppeteer migration
- Serverless PDF API Guide — Lambda and Cloudflare Workers deployment