PDF: Snappy, wkhtmltopdf & Template Setup

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

How can we make the email we're sending from the console command cooler? By adding an attachment! Wait, hmm. That's probably too easy - Mailer makes attachments simple. Ok, then... how about this: in addition to having the table inside the email that summarizes what the author wrote during the past week, let's generate a PDF with a similar table and attach it to the email.

So that's the first challenge: generating a styled PDF... and hopefully enjoying the process!

Installing Snappy & wkhtmltopdf

My favorite tool for creating PDFs is called Snappy. Fly over to your terminal and install it with:

composer require knplabs/knp-snappy-bundle

Snappy is a wrapper around a command-line utility called wkhtmltopdf. It has some quirks, but is a super powerful tool: you create some HTML that's styled with CSS, give it to wkhtmltopdf, it renders it like a browser would, and gives you back a PDF version. Snappy makes working with wkhtmltopdf pretty easy, but you'll need to make sure it's installed on your system. I installed it on my Mac via brew.

wkhtmltopdf --version

Also, check where it's installed with which or whereis:

which wkhtmltopdf

Mine is installed at /usr/local/bin/wkhtmltopdf. If your binary live somewhere else, you'll need to tweak some config. When we installed the bundle, the bundle's recipe added a new section to the bottom of our .env file with two new environment variables.

46 lines .env
... lines 1 - 41
###> knplabs/knp-snappy-bundle ###
WKHTMLTOPDF_PATH=/usr/local/bin/wkhtmltopdf
WKHTMLTOIMAGE_PATH=/usr/local/bin/wkhtmltoimage
###

These are both used inside a new knp_snappy.yaml file that was also added by the bundle.

knp_snappy:
pdf:
enabled: true
binary: '%env(WKHTMLTOPDF_PATH)%'
options: []
image:
enabled: true
binary: '%env(WKHTMLTOIMAGE_PATH)%'
options: []

The WKHTMLTOPDF_PATH variable already equals what I have on my machine. So if your path is different, copy this, paste it to your .env.local file, and customize it. Oh, and don't worry about wkhtmltoimage: we won't use that utility.

Creating the PDF Templates

Ultimately, to create the PDF, we're going to render a template with Twig and pass the HTML from that to Snappy so it can do its work. Open up templates/email/author-weekly-report.html.twig.

{% extends 'email/emailBase.html.twig' %}
{% block content %}
<hr>
<spacer size="20"></spacer>
<row>
<columns>
<p>
What a week {{ email.toName }}! Here's a quick review of what you've been up to on the Space Bar this week
</p>
</columns>
</row>
<row>
<columns>
<table>
<tr>
<th>#</th>
<th>Title</th>
<th>Comments</th>
</tr>
{% for article in articles %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ article.title }}</td>
<td>{{ article.comments|length }}</td>
</tr>
{% endfor %}
</table>
</columns>
</row>
<row>
<columns>
<center>
<spacer size="20"></spacer>
<button href="{{ url('app_homepage') }}">Check on the Space Bar</button>
<spacer size="20"></spacer>
</center>
</columns>
</row>
{% endblock %}

Hmm. In theory, we could just render this template and use its HTML. But... that won't work because it relies on the special email variable. And more importantly, we probably don't want the PDF to look exactly like the email - we don't want the logo on top, for example.

No problem: let's do some organizing! Copy the table code. Then, in the templates/email directory, I'll create a new file called _report-table.html.twig and paste!

<table>
<tr>
<th>#</th>
<th>Title</th>
<th>Comments</th>
</tr>
{% for article in articles %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ article.title }}</td>
<td>{{ article.comments|length }}</td>
</tr>
{% endfor %}
</table>

Let's make this fancier by adding class="table table-striped". Oo, fancy!

<table class="table table-striped">
... lines 2 - 13
</table>

Those CSS classes come from Bootstrap CSS, which our site uses, but our emails do not. So when we render this table in the email, these won't do anything. But my hope is that when we generate the PDF, we will include Bootstrap CSS and our table will look pretty.

Back in author-weekly-report.html.twig, take out that table and just say {{ include('email/_report-table.html.twig') }}

... lines 1 - 2
{% block content %}
... lines 4 - 12
<row>
<columns>
{{ include('email/_report-table.html.twig') }}
</columns>
</row>
... lines 18 - 26
{% endblock %}

Now we can create a template that we will render to get the HTML for the PDF. Well, we could just render this _report-table.html.twig template... but because it doesn't have an HTML body or CSS, it would look... simply awful.

Instead, in templates/email/, create a new file: author-weekly-report-pdf.html.twig. To add some basic HTML, I'll use a PhpStorm shortcut that I just learned! Add an exclamation point then hit "tab". Boom! Thanks Victor!

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
</body>
</html>

Because we're going to add Bootstrap CSS to this template, let's add a little Bootstrap structure: <div class="container">, <div class="row"> and <div class="col-sm-12">.

... lines 1 - 11
<body>
<div class="container">
<div class="row">
<div class="col-sm-12">
... lines 16 - 18
</div>
</div>
</div>
</body>
... lines 23 - 24

Inside, how about an <h1> with "Weekly Report" and today's date, which we can get with {{ 'now'|date('Y-m-d') }}.

... lines 1 - 11
<body>
<div class="container">
<div class="row">
<div class="col-sm-12">
<h1>Weekly report {{ 'now'|date('Y-m-d') }}</h1>
... lines 17 - 18
</div>
</div>
</div>
</body>
... lines 23 - 24

Bring in the table with {{ include('email/_report-table.html.twig') }}.

... lines 1 - 11
<body>
<div class="container">
<div class="row">
<div class="col-sm-12">
<h1>Weekly report {{ 'now'|date('Y-m-d') }}</h1>
{{ include('email/_report-table.html.twig') }}
</div>
</div>
</div>
</body>
... lines 23 - 24

Adding CSS to the Template

If we just rendered this and passed the HTML to Snappy, it would work, but would contain no CSS styling... so it would look like it was designed in the 90's. If you look in base.html.twig, this project uses Webpack Encore. The encore_entry_link_tags() function basically adds the base CSS, which includes Bootstrap.

Copy this line, close that template, and add this to the PDF template.

... lines 1 - 2
<head>
... lines 4 - 9
{{ encore_entry_link_tags('app') }}
</head>
... lines 12 - 24

Even if you're not using Encore, the point is that an easy way to style your PDF is by bringing in the same CSS that your site uses. Oh, and because our site has a gray background... but I want my PDF to not share that specific styling, I'll hack in a background-color: #fff.

By the way, if our app needed to generate multiple PDF files, I would absolutely create a PDF "base template" - like pdfBase.html.twig - so that every PDF could share the same look and feel. Also, I'm not bringing in any JavaScript tags, but you could if your JavaScript is responsible for helping render how your page looks.

Ok, we're ready! Next, let's use Snappy to create the PDF, attach it to the email and high-five ourselves. Because celebrating victories is important!

Leave a comment!

  • 2020-03-19 Tomasz Gąsior

    pdflayer seems to use wkthmltopdf under the hood. :) It generates files with creator property set to qt. qt is toolkit library used for GUI application aaaand used by wkhtmltopdf to generate pdfs. :)

  • 2020-03-19 weaverryan

    Hmm, that's a good one. And maybe https://pdflayer.com/ if you want something as a paid-service? I haven't used it yet - but having this HTML->PDF generation as a service... would be nice for lazy people like me ;)

  • 2020-03-15 Tomasz Gąsior

    There is also https://weasyprint.org/ . It does not support forms and JavaScript but looks promising.

  • 2020-01-02 Victor Bocharsky

    Hey AbelardoLG,

    Hm, what version of "symfony/framework-bundle" do you have installed? You can check it with:

    $ composer info symfony/framework-bundle

    Or you don't have this framework bundle at all? Try to upgrade this first, because from the output I see knplabs/knp-snappy-bundle v1.6.0 requires symfony/framework-bundle ~2.7|~3.0|^4.0 and your "4.4.*" you have in extra should fit the "^4.0" pattern.

    Cheers!

  • 2019-12-30 AbelardoLG

    Hi there!

    I am using

    "extra": {
    "symfony": {
    "allow-contrib": false,
    "require": "4.4.*"
    }
    }

    Best regards.

  • 2019-12-30 Victor Bocharsky

    Hey

    Are you trying to install KnpSnappyBundle on Symfony 5? Or what version of Symfony do you have? Because looks like it is not ready for Symfony 5 yet as you can see from composer.json: https://github.com/KnpLabs/... though there's a PR for this: https://github.com/KnpLabs/...

    Cheers!

  • 2019-12-28 AbelardoLG

    Hi there!

    composer require knplabs/knp-snappy-bundle

    outputs this message:

    Problem 1
    - Installation request for knplabs/knp-snappy-bundle ^1.6 -> satisfiable by knplabs/knp-snappy-bundle[v1.6.0].
    - knplabs/knp-snappy-bundle v1.6.0 requires symfony/framework-bundle ~2.7|~3.0|^4.0 -> no matching package found.

    :)

    Brs.

  • 2019-11-12 Kevin Smith

    Hi!

    MPDF does support CSS, but you'll find what your design for a modern browser, the results from MPDF aren't the same and can be wildly different. It's possible to resolve these by fiddling with the CSS. Just depends on your project and having to keep a set of additional CSS styles just for PDF generation.

    With node, you can install Puppeteer (which comes with Chromium) and you use a simple Node JS script to call a URL and then convert it to PDF.

    The reason I'm quite passionate about this subject is, that I was solely working on a project (6 years old now) that did the following;

    * Used wkthmltopdf to render SPA app and hit many issues with rendering, was never perfect and was prone to issues on attempting to wait for the JS to finish rendering
    * Used Firefox in headless mode, which requires the use of X Server to do it and that can be a world of pain and it's not always reliable. Maintenance hell and really a security risk having this on a webserver.

    About 2 months ago, I rewrote the whole PDF processor as a microservice, using Symfony 4 and Puppeteer and the results produced were perfect, as well as being a lot faster and not needing to run X Server. Also, the set-up is much more secure, as the service can only be called from within our internal network - a much better set-up.

    You do need to install gtk3 and libXScrnSaver from your OS packager manager (yum for example) and that's it - much easier to manage packages than having to manage installing a binary.

    I can put together a simple working Symfony project to get you up and running, and perhaps, with your pizzazz, you can turn it into an "awesome" tutorial for the SymfonyCasts community ? :)

  • 2019-11-12 weaverryan

    Yo Kevin Smith!

    Yea, it's SO true that their releases are a mess these days and we've had compatibility issues getting it installed in certain systems :/. We haven't had many problems with the rendering - though we have had a few minor issues.

    > For a less complicated option, MPDF would also be a good choice, though you have to be careful with the CSS - but perfectly easy to manage this

    What do you mean by "have to be careful with the CSS"?

    > I'd be happy to share my experiences with Puppeteer and Chromium and a solid way of managing these dependencies for a project

    Sure! I'd love to hear about this - it looks like it's a Node library and you can write a pretty small script that would do, basically something similar to wkhtmltopdf?

    Cheers!

  • 2019-11-12 Kevin Smith

    Though using wkhtmltopdf, was for me, a useful tool at the time, it certainly isn't perfect and can cause issue with complicated HTML/CSS. I do not use wkhtmltopdf anymore and it's very rare for a release to occur. This was one of the main reasons I dropped use of it, as there are some fixes in the alpha version that would have resolve some issues, but has been in alpha for over 2-3 years. Plus, it's trying re-invent the wheel with a browser.

    An even more useful tutorial would be to use Puppeteer and Chromium to render the page "exactly" as it would look in a browser.

    For a less complicated option, MPDF would also be a good choice, though you have to be careful with the CSS - but perfectly easy to manage this.

    I'd be happy to share my experiences with Puppeteer and Chromium and a solid way of managing these dependencies for a project.