Convert HTML to PDF for Free: A Developer's 2026 Guide
You're probably here because the easy part is done. You already have HTML. Maybe it's an invoice, a shipping label, a report, a contract preview, or an internal dashboard someone now wants as a PDF. The hard part is getting output that doesn't break the moment real data shows up.
That's where most “free” HTML-to-PDF advice falls apart. It's usually either too simplistic or too optimistic. A one-line example works on a toy page, then your actual document shows missing fonts, clipped tables, broken page breaks, or JavaScript content that never renders.
The good news is that you can absolutely convert HTML to PDF for free. The less pleasant truth is that every free method has a breaking point. The key is choosing the method whose failure mode you can tolerate.
Your Guide to Free HTML to PDF Conversion Methods
PDF isn't niche plumbing. It's standard business infrastructure. PDF is used by 98% of businesses, and over 290 billion new PDFs are generated annually according to PDF usage statistics from Smallpdf. If your app produces documents, sooner or later you need a reliable HTML-to-PDF path.
That need usually starts small. One export button. One scheduled report. One invoice template. Then it turns into edge cases. A long table spans pages badly. A chart loads after the capture starts. A legal footer must stay on every page. A customer sends a screenshot of a PDF where the layout is wrecked.
The trade-off that matters
Free methods usually force you to balance three things:
- Rendering fidelity. How closely the PDF matches your HTML and CSS.
- Operational simplicity. How easy it is to install, deploy, and maintain.
- Runtime cost. How much CPU, memory, and engineering time the conversion path consumes.
If your HTML is mostly static and print-focused, a lightweight server-side renderer can be enough. If your page depends on modern layout, client-side rendering, or asynchronous content, you need a real browser engine.
Practical rule: The more your page behaves like an application instead of a document, the less likely a lightweight free converter will hold up.
The free methods most teams actually use
When people want to convert HTML to PDF for free, they usually end up in one of these buckets:
- Command-line renderers for simple server-side conversion.
- Headless browsers for high-fidelity output.
- Language wrappers that expose either of those approaches in Python or.NET.
- Browser-only export hacks for lightweight client-side use cases.
If you want a quick overview before choosing an implementation path, this free HTML to PDF converter guide is a useful starting point.
Here's the opinionated version. Use the simplest method that matches your HTML today, not the method you hope your HTML will stay compatible with. Teams waste time by choosing a lightweight option for documents that already require browser-grade rendering.
Command-Line Tools like wkhtmltopdf and WeasyPrint
Command-line conversion still has a place. It's attractive for backend jobs, cron tasks, and environments where you want local execution without running a full browser per request. But this category splits into two very different experiences.

Using wkhtmltopdf for basic jobs
The classic pattern is straightforward:
wkhtmltopdf input.html output.pdf
Or with common print options:
wkhtmltopdf \
--margin-top 15mm \
--margin-right 12mm \
--margin-bottom 15mm \
--margin-left 12mm \
--page-size A4 \
--print-media-type \
input.html output.pdf
This still works for older layouts and predictable templates. If your HTML is table-heavy, mostly static, and doesn't lean on modern CSS, it can be a practical free path.
The problem is maintenance and rendering limits. wkhtmltopdf is built on an unsupported WebKit engine and struggles with modern CSS such as Flexbox and Grid, which is one reason many developers moved toward Chromium-based rendering, as noted in this discussion of wkhtmltopdf's current limitations.
For installation details and Odoo-style troubleshooting patterns, this wkhtmltopdf tutorial is worth bookmarking.
What wkhtmltopdf is still good at
- Stable print templates where the HTML barely changes
- Offline server jobs in locked-down environments
- Simple header and footer workflows driven by known markup
Where it becomes a bad choice
- Modern layouts using grid-based dashboards or flex-heavy components
- JavaScript-rendered pages that depend on client-side hydration
- Design systems built around current browser behavior
If your page looks correct in a modern browser but wrong in PDF, and the converter uses an old engine, that isn't a configuration bug. It's the wrong renderer.
Using WeasyPrint for document-style output
WeasyPrint is a different animal. It's usually better thought of as a document renderer, not a browser substitute. It can be excellent when your HTML is intentionally written for print.
Basic usage is clean:
from weasyprint import HTML
HTML("invoice.html").write_pdf("invoice.pdf")
You can also pass a string:
from weasyprint import HTML
html = """
<html>
<head>
<style>
@page { size: A4; margin: 20mm; }
h1 { color: #222; }
</style>
</head>
<body>
<h1>Invoice</h1>
<p>Hello from HTML.</p>
</body>
</html>
"""
HTML(string=html).write_pdf("invoice.pdf")
How these two approaches differ
| Approach | Best fit | Main weakness |
|---|---|---|
| wkhtmltopdf | Older static templates | Falls behind modern browser rendering |
| WeasyPrint | Print-oriented documents with controlled CSS | Doesn't execute browser-side JavaScript |
WeasyPrint tends to produce cleaner results for true document layouts, especially when you invest in print CSS. But if your page needs script execution to become complete, it won't save you.
That's the dividing line. Command-line tools are fine when your HTML already behaves like a finished document before rendering starts. If not, you need a browser engine.
Using Headless Browsers for High-Fidelity Rendering
When people ask for a free way to generate PDFs that match the web page, this is usually the answer. A headless browser loads the page the same way a user's browser does, runs the JavaScript, applies modern CSS, waits for assets, and then prints to PDF.
That's why this method has become the default for serious free implementations. Headless browser rendering achieves over 95% visual replication for JavaScript-heavy pages, compared with about 60% to 70% for alternatives that don't use a full browser engine, based on this review of HTML-to-PDF conversion methods.

A working Node example
Install the package first:
npm install puppeteer
Then generate a PDF from a live page:
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({
headless: true,
args: ['--force-device-scale-factor=2']
});
const page = await browser.newPage();
await page.goto('http://localhost:3000/invoice/123', {
waitUntil: 'networkidle0'
});
await page.emulateMediaType('print');
await page.pdf({
path: 'invoice.pdf',
format: 'A4',
printBackground: true,
margin: {
top: '20mm',
right: '12mm',
bottom: '20mm',
left: '12mm'
}
});
await browser.close();
})();
For raw HTML instead of a URL:
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({
headless: true,
args: ['--force-device-scale-factor=2']
});
const page = await browser.newPage();
await page.setContent(`
<!doctype html>
<html>
<head>
<style>
@page { size: A4; margin: 16mm; }
body { font-family: Arial, sans-serif; }
.card { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
</style>
</head>
<body>
<h1>Report</h1>
<div class="card">
<div>Left block</div>
<div>Right block</div>
</div>
</body>
</html>
`, {
waitUntil: 'networkidle0'
});
await page.emulateMediaType('print');
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true
});
await browser.close();
})();
For a deeper implementation walkthrough, this Puppeteer HTML-to-PDF guide covers the practical knobs that matter in production.
The settings that usually decide success
Three options do most of the heavy lifting.
Wait properly
If data arrives asynchronously, generate the PDF only after the page is fully done. networkidle0 is a decent baseline, but it isn't magic. If your page keeps long-lived requests open, use an explicit selector wait or a custom readiness flag instead.
Print the background
Without printBackground: true, many documents lose brand colors, highlighted table cells, or shaded labels. Teams often mistake that for CSS failure when it's just the print config.
Emulate print media
If you've written @media print rules, call page.emulateMediaType('print'). Otherwise the browser can render the screen layout instead of the print layout, which gives you ugly spacing and stray UI elements.
Don't treat HTML-to-PDF as screenshotting. Treat it as controlled browser printing.
Common gotchas in headless rendering
The fidelity is excellent, but headless browsers come with operational baggage.
- Cold starts hurt. Launching a browser for every request adds latency.
- Memory use adds up. Parallel jobs can stress a small server quickly.
- Fonts and assets still fail if your environment can't access them consistently.
One issue is especially common. Blurry text affects 15% of unoptimized implementations, and the usual fix is setting a higher device scale factor, according to the same headless conversion analysis cited above. That's why I included --force-device-scale-factor=2 in the examples.
If your renderer needs authenticated access to internal pages or region-specific content during capture, a clean browser networking setup matters. In those cases, this guide to secure Playwright proxy setup is useful even if your broader automation stack already handles most routing concerns.
When headless browsers are the right free option
Use this approach when your page includes:
- Client-rendered data that appears after initial HTML load
- Modern CSS layout such as grid, flexbox, sticky sections, or layered components
- Shared UI code where the PDF view reuses the web view with print overrides
If your output must look like the browser, use a browser. That sounds obvious, but it saves a lot of wasted time.
Practical Examples in Python and C#
The language changes. The trade-offs don't. In Python and C#, the main question is still whether you need lightweight document rendering or full browser rendering.
Python with a lightweight wrapper
If your stack already has a command-line renderer installed, a wrapper can get you moving quickly.
import pdfkit
pdfkit.from_file('input.html', 'output.pdf')
You can also pass a string:
import pdfkit
html = """
<html>
<body>
<h1>Invoice</h1>
<p>Rendered from Python.</p>
</body>
</html>
"""
pdfkit.from_string(html, 'output.pdf')
This is fine for controlled templates. It's not what I'd pick for pages that depend on script execution or current browser layout behavior.
Python with browser rendering
When fidelity matters, the browser-driven approach maps cleanly to Python too.
import asyncio
from pyppeteer import launch
async def generate_pdf():
browser = await launch(headless=True, args=['--force-device-scale-factor=2'])
page = await browser.newPage()
await page.setContent("""
<html>
<head>
<style>
@page { size: A4; margin: 20mm; }
body { font-family: Arial, sans-serif; }
</style>
</head>
<body>
<h1>Monthly Report</h1>
<p>This page is rendered in a real browser engine.</p>
</body>
</html>
""")
await page.emulateMedia('print')
await page.pdf({
'path': 'report.pdf',
'format': 'A4',
'printBackground': True
})
await browser.close()
asyncio.get_event_loop().run_until_complete(generate_pdf())
If you're packaging Python-based conversion or scraping workflows into a scheduled backend task, this guide to create your Apify Python Actor is a practical resource for turning a local script into a repeatable job.
C# with browser-grade output
In.NET, browser-backed conversion is usually the cleaner long-term path for modern HTML.
Install the package:
dotnet add package PuppeteerSharp
Then use it like this:
using System.Threading.Tasks;
using PuppeteerSharp;
class Program
{
public static async Task Main()
{
await new BrowserFetcher().DownloadAsync();
await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = true,
Args = new[] { "--force-device-scale-factor=2" }
});
await using var page = await browser.NewPageAsync();
await page.SetContentAsync(@"
<html>
<head>
<style>
@page { size: A4; margin: 18mm; }
body { font-family: Arial, sans-serif; }
</style>
</head>
<body>
<h1>C# PDF Example</h1>
<p>Rendered with a headless browser.</p>
</body>
</html>
");
await page.EmulateMediaTypeAsync(MediaType.Print);
await page.PdfAsync("output.pdf", new PdfOptions
{
Format = PaperFormat.A4,
PrintBackground = true
});
}
}
For more.NET-specific patterns, this C# HTML-to-PDF guide is a solid reference.
In Python and C#, wrappers feel fast at first. Browser rendering usually feels slower at first. Over time, the browser path often costs less because you spend less time debugging layout mismatches.
Fixing Common Pitfalls and Configuration Headaches
Getting a file named output.pdf is easy. Getting a PDF you can ship to customers is where the work starts.

Page breaks that destroy the layout
The default page-breaking behavior is rarely acceptable for invoices, reports, or statements. Headings get stranded at the bottom of a page. Table rows split awkwardly. Signature blocks drift.
Use print CSS deliberately:
h1, h2, h3 {
break-after: avoid;
}
table, img,.card {
break-inside: avoid;
}
.section-start {
break-before: page;
}
Older renderers may still prefer legacy properties:
.avoid-split {
page-break-inside: avoid;
}
Missing fonts and inconsistent typography
If the converter can't load your font files at render time, it falls back by default. That changes widths, line breaks, and pagination.
A few habits help:
- Use absolute asset paths so the renderer doesn't guess wrong.
- Preload critical fonts when your browser-based renderer supports it.
- Test with production infrastructure because local assets often hide deployment failures.
Asynchronous content that never makes it into the PDF
Charts, totals, and customer data often arrive after the initial page load. If conversion starts too early, the PDF captures a partial page.
The fix depends on your app. Sometimes networkidle0 is enough. Sometimes you need to wait for a selector:
await page.waitForSelector('.report-ready');
Or for a custom signal:
await page.waitForFunction(() => window.__PDF_READY__ === true);
Performance pain you feel later
Spawning a fresh browser for every request works at small scale, then becomes the bottleneck. Reuse browser instances when you can. Open new pages instead of launching a new process every time, and queue jobs when demand spikes.
A good free pipeline isn't just about rendering quality. It's about predictable behavior under load, with asset access, fonts, and page timing all under control.
When to Upgrade to a Paid HTML to PDF API
There's a point where “free” stops being cheap. It usually arrives before teams expect it.

The real breaking point
Free tooling starts to hurt when you need any mix of these:
- High daily document volume
- Strict uptime expectations
- Sensitive customer documents
- A team that doesn't want to maintain browser infrastructure
- Consistent output across environments without ongoing tuning
Open-source methods are still useful. I'd keep using them for internal exports, lightweight admin tools, prototypes, and stable document templates. But once PDF generation becomes customer-facing infrastructure, maintenance time becomes part of the cost.
Security is where free options often fall short
This is the part many tutorials skip. Analysis shows that 78% of enterprises reject free HTML-to-PDF tools because of data privacy risks, especially when those tools process documents on unsecured servers or lack encryption and digital-signature capabilities required for compliance, according to this security-focused review of HTML-to-PDF options.
That doesn't mean every free self-hosted approach is unsafe. It means you need to own the security model yourself. You're responsible for access control, storage handling, logging, audit expectations, and document lifecycle behavior.
The question isn't “Can I convert HTML to PDF for free?” The better question is “What am I now responsible for because I chose free?”
A practical upgrade rule
Move to a paid API when your team spends more time operating PDF generation than benefiting from owning it. That includes:
- Troubleshooting rendering drift
- Managing browser binaries and server resources
- Handling retries, queueing, and failures
- Reviewing compliance and privacy risk
- Supporting internal teams who depend on document output
If you're evaluating what “better than free” should look like, this guide to the best PDF conversion software is a good next step.
For straightforward internal work, free is often enough. For production workflows with customers, compliance, or scale, managed conversion usually becomes the smarter decision long before anyone wants to admit it.
If you want a managed option after testing the free paths, Transformy.io is built for teams that need reliable HTML-to-PDF generation without running the rendering stack themselves.