Rebased Team writing about tech we use.

Languages, frameworks, libraries,
tools. Certified for 0% fluff.

Wkhtmltopdf Considered Harmful

Krzysztof Zych

There are eight reasons why wkhtmltopdf shouldn’t be used for anything serious:

8. Difficult to build

Compilation requires Qt (currently an obsolete version) and WebKit, both hefty codebases. One Docker Hub repo author stated problems with compiling it under two hours – which is Docker’s timeout for automated builds.

This makes getting at the newest version hard. The official GitHub repository does release official packages and binaries for the newest stable version – but the prerelease (0.13 at the moment), which may or may not fix issues further on in the list, is nowhere to be found – and back to the build issues we go.

7. Process invocation overhead

This is not a big issue nowadays, therefore it ranks lower in the list. But still: unless you use something really slow, it may be faster to include a native PDF-generating library in your project and use that, instead of spawning a wkhtmltopdf process for each document you need to output. All modern operating systems deal with this nicely, keeping it in cache so that frequent usage does not incur load time penalties. But once launched, you cannot tell or control how much resources (memory in particular) it consumes – it’s a black box. If you’re running on cheap instances with low RAM wkhtmltopdf may be the death of your machine.

6. One footer, one header, no footnotes

All that the documentation tells you about headers and footers is that you can put them in separate HTML files and set them with options. (Or use dumb plain-text content instead.) Then you get them on every page, no exceptions (and for page numbers… wait till the № 1 item on this list). What it doesn’t tell you is that it wants them to be complete, separate HTML documents, so you need to take extra steps to ensure they look like the rest of your document.

Also, good luck with coherent footnotes – you’re fully on your own there. As the footer is global, the only thing you can do is run some JavaScript detecting footnote references in the page and stuffing their expansions into this page’s footer.

If your documents are to be bound, and you need odd and even pages to format slightly differently – again, good luck.

5. One page size, one page layout per document

Want landscape pages? Sure, you can have them but all pages in your output will be landscape. No flipping. And no, rotating page content 90 degrees is not the answer, output may look usable on paper, but not in a viewer application. The CSS Paged Media spec has declarative syntax for all of that – but wkhtmltopdf does not support any usable subset of it.

4. Poorly documented (and generally poor) local resource access

Are you supposed to use file:// URLs for local content? Or just paths? Does the <base> tag work for setting where these assets can be found? No answers from the official docs. You have to browse GitHub Issues for an answer. On Linux, absolute paths seem to work fine, but that is barely an acceptable solution. But hey, you can always expose your resources as absolute URLs pointing at some (hopefully internal) server. Thus fetching them over the network (localhost, hopefully) instead of the filesystem. Or represent them very inefficiently as base64-encoded inline resources, which is especially bad if you are embedding large font files.

3. Dramatically bad kerning for some users

The GitHub repository has a bug open for four years now, showing atrocious kerning issues in rendered text. Some users report that increasing the output DPI helps, but the core issue is still not fixed. Show its output to someone with a knack for typography and watch them cringe in terror. Actually please don’t – they are very nice if slightly weird people. Make friends with them instead.

2. Ridiculously poor font support

While wkhtmltopdf loads web fonts fine (problems from point 4 notwithstanding), its handling of multiple font files listed under the same font family is atrocious. If you have mixed scripts in your output (say, addresses in Japanese, but invoice items in English), you’re out of luck. All you can do is use a single font containing every unicode character. If you want to use Google’s Noto fonts, separated per script – good luck.

However, there actually is a solution for that, and it’s also the reason why you get different results on your development machine vs a production server: the fontconfig file. Wkhtmltopdf (and most of your desktop environment) uses the fontconfig library to parse it and discover available fonts. Without fontconfig, wkhtmltopdf can only render some basic fonts like Helvetica and Times, plus whatever’s declared with @font-face.

Therefore, in four easy steps:

Remove @font-face declarations from your CSS.

Verify you’re using the correct font family name (one that’s displayed in a font chooser in your text editor or graphic program).

Create a fontconfig file with a single directive pointing to where you ship the font files. The path must be absolute.

Remember to adjust your deploy process to output correct paths on production. And now not only you have full control of which fonts are available, but all the mixed script issues are gone!

1. You need JavaScript to have page numbers

If you want to have nice presentation, that is. If you squint at the docs hard enough, you’ll find that there is a --default-header option, which shows that you can use a couple of magic strings that get replaced with page numbers. Then, towards the end, they are explained in more detail. But only in plain-text headers/footers. For nice HTML ones, there follows a snippet of JavaScript, ran with onload event on each actual page, and its job is to fetch that page number from document.location.search. Instead of supporting what’s in the Paged Media Spec, wkhtmltopdf’s solution requires a JavaScript engine to implement. And it is, like other things on this list, poorly documented. People aren’t able to find it in the docs, as evidenced by many SO threads.

Any alternatives?

PDF is an ancient format, as old as the Web. It was introduced in 1993, though at the time it was proprietary to Adobe and not really open or popular. It became an open standard in 2008 when Adobe relinquished control, but it took till 2017 to release a standard that does not reference any patented or proprietary technology. So given the age, you’d think wkhtmltopdf would be easy to avoid.

Not Ruby?

If your code runs on JVM, you probably are in the best position here. There are plenty of Java PDF generating libraries out there, with various capabilities, both free and non-free. Popular free ones are Apache PDFBox, LibrePDF and PDFjet. Plus about a million commercial ones.

Many languages also have a binding to PDFlib. It’s a very low-level library, nothing like converting a well-understood thing like HTML+CSS to a PDF file. Most of these tools are akin to graphics-drawing APIs, requiring you to position text and graphics yourself.

Fine with spawning a process?

Yes, and I have tons of investor money

Evaluate PDFReactor, AntennaHouse Formatter and PrinceXML. All have stellar Paged Media support. Between all of them they support extra features like JavaScript, output to formats other than PDF (like XPS), native handling of MathML and compliance with CSS3 (instead of 2.1 only). Choose which one works best for you – PDFreactor is a Java-based solution, the other two ship as native libraries. But the prices start at about $2800 per server.

I’m on a budget here

Check out weasyprint. It’s a BSD-licensed Python-based solution that is almost1 as good as the commercial ones listed above. This should be your replacement for wkhtmltopdf, period. Unless you need JavaScript for things other than page numbers.

Also, depending on your academic experience, you may also be fine with running TeX/LaTeX, probably via pdfTeX which outputs PDF files without additional steps. While not HTML+CSS, the environment here is huge, rich and the powerful language allows you to realize any level of typographic perfection you want.

Again, about a million of these, all varying degrees of commercial. One popular alternative is DocRaptor (which, by the way, runs PrinceXML as its engine), because it’s nicely integrated into Heroku. But before jumping to one, consider: how are you going to serve your fonts and other assets to the remote service? Is your legal department okay with sending sensitive data out of your cloud? Can you handle outages and design for resilience? Who does the archiving?
Once past all these hurdles, some have nice wrappers for many languages, and all can be handled using your favorite HTTP client library.

Ruby?

There used to be a single go-to library for Rubyists: the venerable Prawn. It still works, can be faster than spawning a process, and it’s perfectly usable on modern Ruby versions. Maintainers are currently splitting it into smaller pieces and updating a bit. It has very restricted styling capabilities, compared to CSS, but handles fonts better.

Nowadays, a modern competitor is HexaPDF. Still a thing in progress, it promises to handle more things than Prawn, and at a finer level of control than just text boxes. Although at the moment it lacks many things – table layout is a gaping hole.

Everything else is just some kind of interface to wkhtmltopdf, the most popular one being wicked_pdf, which does a decent job of wrapping around the binary’s command-line options. And not much else.

Final words

For a client that produces thousands of invoices each month, and four very different kinds of them, we moved from Prawn to wkhtmltopdf. Prawn code isn’t as readable, easily presentable like plain HTML+CSS, and it can’t be handed over to frontend folks and UX designers to tinker with.

As for the list: Points 8 and 7 we can live with. Items 6 and 5 can be worked around with additional tools, basically by gluing together multiple PDF files output by wkhtmltopdf. Number 4 is infuriating but manageable. Point 2 has a good solution described above. And items 3 and 1, with no fix on the horizon, we consider platform limitations.
We’ll be keeping an eye on weasyprint.

Footnotes

Notably, it can’t do footnotes, leaders (patterns to fill horizontal whitespace), page groups other than even and odd, cross-references, custom counters. Another big omission is running elements – you only get named strings (textual contents of specified elements), static content or counters in page margin boxes. See the full list of supported features in the docs. ↩