HTML to PDF, five honest ways.
Whether you have a single .htmlfile on your desktop, a folder of 400 saved articles, or a build script that spits out invoices, one of these methods will fit. All five are free. We'll compare fidelity on real fixture pages at the end so you can pick without guessing.
TL;DR by use case
Skip to the row that describes you. Every recommendation here is repeated in more depth below with commands, gotchas, and screenshots of the output.
The rest of this page is: what each method is, the exact command or click sequence, and where each one breaks. If you just want the one-click desktop option, see Convert: Anything to PDF. If you want to save any webpage as PDF (not just a local HTML file), our save-HTML-as-PDF guide walks through the same territory from the “live page” angle.
Method 1: Open .html in Chrome + Save as PDF
This is the answer to how to save a HTML as PDF for 90% of people, 90% of the time. No installs, no accounts, no upload. Chrome (or any Chromium browser: Edge, Brave, Arc, Vivaldi, Opera) can open a local .html file and print it to a PDF using its own rendering engine.
Steps
- Drag the
.htmlor.htmfile onto an empty Chrome tab, or useFile → Open File. The address bar will showfile:///path/to/page.html. - Press Cmd/Ctrl + P to open the print dialog.
- Set Destination to Save as PDF.
- Open More settings. Turn off headers and footers if you don't want “file:///Users/…” along the top of every page. Turn on Background graphics if the page uses colored backgrounds you want to keep.
- Click Save.
What works, what doesn't
Works well: most static documents — resumes, receipts, tutorials, MHTML archives, exports from tools like Notion or Craft that ship as a folder of HTML + assets. The output is a real PDF with selectable text and clickable links.
Watch out for:pages that fetch fonts or images from a CDN and haven't loaded yet when you hit print, pages that require JavaScript to render (Chrome will run it, but timing-heavy scripts sometimes need a Cmd+R first), and pages that assume they are served over https://. Some CSS features that use@page or page-break-inside: avoidrender correctly in Chrome's print engine — others do not. If a page break lands in the middle of a table row, that's a CSS limitation, not a Chrome one.
Batch caveat:Chrome's Save-as-PDF is one file at a time. If you have twenty .htmfiles to convert, you're about to repeat the same six clicks twenty times. That's when Method 2 becomes worth the install.
Method 2: Convert: Anything to PDF (batch drop)
Convert: Anything to PDF is a free Chrome extension we ship. It exists because “convert this folder of HTML files to PDF” is a common request that has no clean answer inside the browser itself. The extension adds one: drag the folder onto the popup, get one merged PDF back.
When it's the right tool
- You have a batch — a dozen or a thousand — of
.html,.htm, or.mhtmlfiles sitting in a directory. - You want them merged into a single PDF, in a specific order, with a table of contents.
- You want the whole thing to happen locally: no upload, no login, no rate limit. Files never leave your machine.
- You need output that matches what Chrome's own Save-as-PDF produces — because that's the engine it uses under the hood.
How to use it
- Install Convert: Anything to PDF from the Chrome Web Store. It works in any Chromium browser.
- Click the extension icon. A dropzone appears. Drag your files or the entire folder onto it.
- Reorder if needed by dragging. Pick paper size and margins if defaults don't suit you.
- Hit convert. One PDF comes out.
It also handles the “htm to pdf” case verbatim — .htm, .html, and .mhtml are all recognized. If your files were saved from an older Windows machine as .mhtml, that path has its own writeup in Convert MHTML to PDF — same extension, same one-click flow.
Method 3: Python — WeasyPrint, pdfkit, Playwright
If you're generating HTML from Jinja/Django templates or scraping reports on a schedule, Python has three respectable options. They are not interchangeable — pick based on the fidelity you need and what's already on the server.
WeasyPrint — pure-Python, print-CSS native
WeasyPrint is a from-scratch HTML/CSS renderer written in Python. It has no Chromium dependency, which makes it the friendliest option for slim Docker images and serverless runtimes. It has excellent support for @page, page counters, and running headers — the kind of typography features you want for invoices and reports.
pip install weasyprint
weasyprint input.html output.pdfIts blind spot: JavaScript. WeasyPrint doesn't run it. If the content you want in the PDF is drawn by JS at runtime, render the page in a headless browser first, then hand the resulting HTML to WeasyPrint (or skip it and use Playwright).
pdfkit — Python wrapper around wkhtmltopdf
pdfkit is a thin binding to the wkhtmltopdf binary, which itself uses an old fork of WebKit. It's been the go-to for a decade because it's fast and simple, but note that wkhtmltopdf's underlying engine is frozen — modern CSS features (flexbox edge cases, grid, some@media print queries) may render differently than in a fresh browser.
pip install pdfkit
# and install the wkhtmltopdf binary
import pdfkit
pdfkit.from_file("input.html", "output.pdf")Playwright — full Chromium, scripted
When you need to hit an authenticated URL, wait for a chart library to finish drawing, or emulate a specific viewport size, Playwright gives you a real browser under a Python API.
pip install playwright && playwright install chromium
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
b = p.chromium.launch()
page = b.new_page()
page.goto("file:///path/to/input.html")
page.pdf(path="output.pdf", format="A4")
b.close()For a side-by-side of these three plus fringe options, see our long-form comparison: HTML to PDF: the Python library landscape.
Method 4: Node — Puppeteer, html2pdf.js, pdf-lib
Three tools, three different jobs. If you pick the wrong one for your job you'll fight it forever, so this section is mostly about matching.
Puppeteer — server-side, high fidelity
Puppeteer drives a real headless Chromium from Node. This is the default choice when the HTML you're rendering was designed for a browser: complex layouts, web fonts, charts drawn with SVG or Canvas, print-specific CSS. The output matches Chrome's Save-as-PDF because it is Chrome saving as PDF, just automated.
npm i puppeteer
import puppeteer from "puppeteer";
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto("file:///path/to/input.html", { waitUntil: "networkidle0" });
await page.pdf({ path: "output.pdf", format: "A4", printBackground: true });
await browser.close();The cost is weight: a Chromium binary in every deployment. On AWS Lambda that means the chrome-aws-lambdalayer or Playwright's slimmer builds.
html2pdf.js — client-side, no server
html2pdf.js runs in the browser. It rasterizes the DOM via html2canvas and then wraps the images in a PDF via jsPDF. It's appealing because no server is involved — the visitor's own browser does all the work — but you're trading real text for images. Selection, search, and accessibility all suffer.
Use it when the output is one-off and doesn't need to be accessible: a “save this page as PDF” button on a dashboard, a certificate download after a quiz. Don't use it for long-form documents.
pdf-lib — assemble PDFs, don't render HTML
pdf-lib is worth mentioning to prevent a common mistake: it does notconvert HTML to PDF. It creates and edits PDFs from primitives — drawing text, images, and shapes. Use it to stitch Puppeteer-generated pages together, add a cover, watermark, or merge with an existing PDF. Pair it with Puppeteer; don't replace Puppeteer with it.
Method 5: Free HTML-to-PDF APIs
If you don't want to host a headless browser yourself, a handful of services will do it for you over HTTP. All of the ones worth mentioning have a free tier; none of them are truly “free forever, unlimited.” Read the pricing before you build against one.
What to compare
- Engine.Chromium-based APIs (PDFShift, api2pdf, Browserless) match what you'd get from Puppeteer. Older wkhtmltopdf-backed APIs are cheaper but render older CSS.
- Free tier. Usually 50–500 conversions per month before card required. Check whether they watermark on the free tier — some do.
- What it can reach. Public URLs only, or can you POST raw HTML? Can it reach a URL that requires headers or basic auth? Can it wait for a specific selector before printing?
- Data policy. The service sees the HTML you send it. If that content is sensitive, prefer the local options above. (This is the exact reason the extensions on this site are local-first.)
A typical call
# generic pattern; API paths vary
curl -X POST https://api.example.com/v2/convert \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"source": "<html>...</html>", "format": "A4"}' \
--output result.pdfThe convenience is real: no browser to install, no memory to budget, and scale is somebody else's problem. The trade is per-conversion cost and the network round-trip. For an internal batch job at the office, that's usually the wrong shape. For a product feature (“email me this receipt as a PDF”), it's often the right one.
Fidelity comparison on 3 fixture pages
Every “best HTML-to-PDF tool” roundup on the web tests the same trivially easy documents. Here's how each method above handles three fixtures that actually stress the renderers.
Fixture A: A long-form article with web fonts
A 5,000-word blog post with an Inter web font, three embedded images from a CDN, and a table of contents anchored to headings. This is the “easy” case.
- Chrome Print / Convert: Anything to PDF / Puppeteer / Playwright: pixel-perfect. Fonts embed. Anchors work.
- WeasyPrint: fonts render correctly if you point it at the CSS
@font-facedeclarations. Otherwise it substitutes. - wkhtmltopdf / pdfkit: renders, but line-height drifts and any
grid-based layout collapses to block-level. - html2pdf.js: works. Output is an image-based PDF — text is not selectable, file size is 4–6× larger.
Fixture B: An invoice using @page and page counters
Two-page invoice with a fixed footer showing “Page X of Y” via CSS counters, line items that must not break mid-row, and a total block that must stay together with the last line item.
- WeasyPrint: best-in-class. Page counters, running headers, and
break-insideall work as specified. - Puppeteer / Playwright / Chrome Print: counters need to be set via the
headerTemplate/ footer template, not pure CSS. Once you do that, correct. - wkhtmltopdf: supports header/footer via CLI flags. Reliable, if verbose.
- html2pdf.js: struggles. Page breaks are approximate, counters unsupported.
Fixture C: A dashboard with a JS-drawn chart
A page that fetches JSON on load and renders a Chart.js line chart inside a canvas. The chart takes ~600ms to draw.
- Puppeteer / Playwright / Chrome Print: correct provided you wait for the network to go idle (or wait on a selector).
- Convert: Anything to PDF (for local files): correct — it uses the same Chromium engine and honors load timing.
- WeasyPrint / wkhtmltopdf: blank canvas. Neither runs JS.
- Free APIs:depends. Chromium-backed ones with a configurable “wait” parameter work. wkhtmltopdf-backed ones don't.
The pattern: if the page runs JavaScript, you need a real browser somewhere in the pipeline. If it doesn't, you have a lot more freedom — and WeasyPrint deserves a serious look.
FAQ
Is any of this actually free?
What's the difference between .html and .htm?
.htm is a leftover from the 8.3-filename DOS era. Every browser and every tool listed here treats .htm and .html identically. Same for .mhtml, which bundles a page plus its assets into one file.How do I save a webpage (not a file) as PDF?
Why is my PDF missing images?
waitUntil: "networkidle0"fixes this), the images are set as CSS backgrounds and Background graphics is off in the print dialog, or a CORS policy is blocking them when opened via file://. Serving the folder via a quick python -m http.server resolves the last one.Can I keep text selectable in the PDF?
Which method matches Chrome's output exactly?
Skip the terminal. Just drop the file.
If “one .html file, one PDF, right now” is your whole problem, Convert: Anything to PDF is the shortest distance. Free, local, no account. Same engine as everything above.