Convert HTML to PDF: six methods, and how each one breaks
Browser print dialog, wkhtmltopdf, WeasyPrint, Puppeteer/Playwright, LibreOffice, and a hosted API. What each produces, what it costs to run, and the failure you will hit first.
There are more ways to turn HTML into a PDF than there are good reasons to. Six of them are worth knowing, each fails differently, and the right choice depends less on features than on who is going to keep the thing running six months from now. Here is the honest map.
The short version
| Method | Fidelity | Setup | JavaScript | Best for |
|---|---|---|---|---|
| Browser print dialog | Perfect | None | Yes | One document, by hand |
| wkhtmltopdf | Poor on modern CSS | Binary + libs | Partial, old engine | Legacy systems only |
| WeasyPrint | Excellent for documents | pip install | No | Invoices, contracts, statements |
| Puppeteer / Playwright | Perfect | Browser + ops | Yes | Full control, you own the fleet |
| LibreOffice headless | Poor | Heavy install | No | Office formats, not HTML |
| Hosted API | Perfect | One HTTP call | Yes | Everything else |
1. The browser's print dialog
Open the page, press Ctrl+P, choose *Save as PDF*. Free, perfect fidelity, and zero automation.
It is genuinely the right answer when someone needs one document once. The moment a second person needs it, or a cron job does, it stops being an answer at all. There is no API, no way to pass data, and no way to guarantee two people produce the same file — margins and the "background graphics" checkbox differ per machine.
Breaks when: you need it done more than once, or by anyone but a human.
2. wkhtmltopdf
A command-line tool wrapping a fork of WebKit. For a decade it was the default answer in every Stack Overflow thread, and it still appears in Dockerfiles everywhere.
The project is archived. The engine underneath predates the CSS most teams write today: flexbox behaves oddly, grid is absent, and several properties are ignored rather than approximated. The symptom is always the same — a layout that is correct in the browser and collapses in the PDF.
There is also an operational cost people forget: it is a binary on every machine that renders, with system libraries it expects to find, and an unpatched rendering engine sitting inside your infrastructure.
Breaks when: your CSS is younger than about 2016, or web fonts need to load reliably. See what to use instead and the migration checklist.
3. WeasyPrint
A Python library that implements the paged-media parts of CSS and writes PDF directly — no browser involved. For documents *authored as documents*, the output is often better than a browser's: running headers, page counters, footnotes and string-set all behave the way the specification says.
It runs no JavaScript at all. A page that mounts its content with a framework arrives empty.
In our own measurements WeasyPrint renders a one-page invoice in about 0.19 s against 0.64 s for a warm Chromium — three times faster, because there is no browser in the path. On a fourteen-page report with charts the order flips. The full numbers are in WeasyPrint vs Playwright.
Breaks when: the page needs a script to render anything.
4. Puppeteer or Playwright
Drive real headless Chromium and call page.pdf(). Output matches the browser exactly, because it *is* the browser.
The cost is operational, and it arrives later than you expect:
- Chromium and its system libraries on every rendering machine — roughly 400 MB in your image
- A warm browser between renders; a cold start costs over a second
- A pool, because one browser per request will bury a small server
- Recycling, because a long-lived browser that has rendered thousands of pages holds more memory
than one that has rendered five
- Timeouts and crash recovery, because one page that never fires
loadshould not stall a queue
None of it is difficult. All of it is yours to run.
Breaks when: you deploy to a 512 MB container and discover Chromium wants most of it before it renders anything.
5. LibreOffice headless
soffice --headless --convert-to pdf file.html. Surprisingly serviceable for plain HTML, poor for anything styled, and a heavy install. It exists in this list because people try it — usually because LibreOffice is already present for DOCX conversion.
Breaks when: CSS matters at all.
6. A hosted API
You POST HTML, you receive a PDF. Someone else runs the browser, patches it, pools it, and pays for its memory.
curl -X POST https://pdfgeny.com/api/v1/render \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"html":"<h1>Invoice 001</h1><p>Total: €149</p>","format":"A4"}' \
--output invoice.pdf
That is the entire integration. The trade is a network hop and a per-document price; what you get back is not having a browser in your deployment.
Breaks when: you have no outbound network, or your volume is high enough that a dedicated box is cheaper than per-document pricing.
Choosing, honestly
- One document, once → print dialog. Do not build anything.
- Documents you author yourself, no JavaScript, hundreds a day → WeasyPrint. It is fast, cheap
and the typography is excellent.
- Real web pages, or output that must match the browser, and you have people to run infrastructure
→ Puppeteer or Playwright.
- Real web pages, and you would rather not run a browser fleet → a hosted API.
- You are on wkhtmltopdf today → move. Not urgently, but deliberately; the engine will not improve.
What actually breaks, in order of frequency
1. Fonts fall back. The PDF shows Times where the page showed your brand font. Causes are timing, CORS, and formats — all covered in web fonts in generated PDFs. 2. Page breaks land badly. Table rows split, headings orphan at the bottom. Four CSS properties fix most of it: see page breaks in HTML to PDF. 3. Backgrounds disappear. Almost always print_background (or the browser dialog's checkbox) left off. 4. The page is empty. Rendering started before JavaScript finished. Wait for network idle, or for a selector that only exists once content is drawn. 5. Memory grows. A long-lived browser leaks slowly. Recycle it every few hundred documents.
Try it without installing anything
Paste markup into the HTML to PDF converter and download the result, or point the URL to PDF converter at a page. Both use the same Chromium the API does, so what you see there is what the API returns.