Generating PDF files with Symfony

This article explains how to generate PDF files in Symfony with KnpSnappyBundle and the underlying wkhtmltopdf library.

This article explains how to generate PDF files in Symfony with KnpSnappyBundle and the underlying wkhtmltopdf library and will tackle the following subjects:

  • Install KnpSnappyBundle and wkhtmltopdf with Composer
  • Create a specific Twig layout template for the generated PDF files
  • Use of absolute URLs for all assets
  • Add the app host to /etc/hosts for your dev environment
  • Viewport size adjustment for CSS media queries (e.g. Bootstrap grid system)

Install the KnpSnappyBundle library

With Composer:

composer require knplabs/knp-snappy-bundle

Enable the library in your kernel:

// app/AppKernel.php
public function registerBundles()
{
    $bundles = [
        //...
        new Knp\Bundle\SnappyBundle\KnpSnappyBundle(),
        //...
    ];
}

Install wkhtmltopdf

KnpSnappyBundle relies on the wkhtmltopdf library that uses the Qt library which embeds WebKit to render PDF files from HTML code.

It is a much more reliable way to render HTML to PDF instead of using pure PHP libraries like domPDF that literally try to develop a web browser with support of CSS / SVG / etc. in PHP. WebKit can be used outside a web browser UI, and supports all web standards out of the box, including JavaScript (very useful if the page you export a web page that embeds some graphs generated with HighCharts or any other charting library).

There are two ways to install wkhtmltopdf:

  • Download the library and install it in /usr/local/bin
  • Use Composer that will install the library locally in your project

I recommend the second solution. It allows you to control which version to use for each project and it avoids any surprise when deploying the project on a new server that wouldn’t have wkhtmltopdf installed on it:

composer require h4cc/wkhtmltopdf-amd64

Note that two libraries are needed for Wkhtmltopdf, you can install them on Debian based distributions with this command:

sudo apt-get install libxrender1 libfontconfig

Now configure the path to the wkhtmltopdf binary in config.yml:

# app/config/config.yml
knp_snappy:
    pdf:
        enabled:    true
        binary:     %kernel.root_dir%/../vendor/h4cc/wkhtmltopdf-amd64/bin/wkhtmltopdf-amd64

Render the PDF in a controller

On this part, nothing very special, we just make of the knp_snappy.pdf service to render the PDF file from the HTML code generated in a template:

<?php
// src/AppBundle/Controller/DemoController.php

class DemoController extends Controller
{
    /**
     * Export to PDF
     *
     * @Route("/pdf", name="acme_demo_pdf")
     */
    public function pdfAction()
    {
        $html = $this->renderView('AppBundle:Demo:pdf.html.twig');

        $filename = sprintf('test-%s.pdf', date('Y-m-d'));

        return new Response(
            $this->get('knp_snappy.pdf')->getOutputFromHtml($html),
            200,
            [
                'Content-Type'        => 'application/pdf',
                'Content-Disposition' => sprintf('attachment; filename="%s"', $filename),
            ]
        );
    }
}

The Twig template

If you intend to generate several PDF files in your Symfony application, I advice you to create a specific Twig layout file for your PDF files. This will allow you to start from an empty layout (without your website header, menu, and so on) and only add the JS / CSS assets needed for PDF generation.

The most important thing to remember is that all assets must be used with their absolute URL.

A simple layout for PDF files could look like this:

{# src/AppBundle/Resources/views/layout-pdf.html.twig #}
{% extends 'base.html.twig' %}

{% block body %}
    {# Layout for PDF files #}
    <div>
        {% block content %}{% endblock %}
    </div>
{% endblock %}

{% block stylesheets %}
    <link href="{{ absolute_url(asset('css/pdf.min.css')) }}" rel="stylesheet" type="text/css">
{% endblock %}

Note the use of the absolute_url function for the absolute asset URLs.

This layout can now be used in your template:

{# src/AppBundle/Resources/views/Demo/pdf.html.twig #}
{% extends 'AppBundle::layout-pdf.html.twig' %}

{% block content %}
    <p>This is the PDF content.</p>

    <figure>
        <img src="{{ absolute_url(asset('images/chimay.jpg')) }}" alt="">
        <figcaption>A picture with an absolute URL</figcaption>
    </figure>
{% endblock %}

Make assets accessible from within a Virtual Machine for dev environments

If you are using a Virtual machine for your development environment (Vagrant with VirtualBox for instance), you may wonder why this doesn’t work at first and wkhtmltopdf never gives very clear error messages.

All you need to do is to add your app host to your host file in the VM: the VM needs to have access to the absolute URLs when the wkhtml2pdf binary is executed on it. If your web app is configured on myapp.localhost.com, you will need to edit the /etc/hosts file in the VM and add this line:

# /etc/hosts
127.0.0.1 myapp.localhost.com

The PDF generation should now be fully functional.

Adjust the viewport size

If you are using CSS media queries for your templates (using Bootstrap grid system for instance), you may realize that your columns get all stacked in the rendered PDF.

Suppose you have included Bootstrap in your PDF layout file, and that you are rendering the following template as a PDF:

{% extends 'AppBundle::layout-pdf.html.twig' %}

{% block content %}
    <div class="container">
        <div class="row">
            <div class="col-md-6">
                <p>First column.</p>
            </div>

            <div class="col-md-6">
                <p>Second column</p>
            </div>
        </div>
    </div>
{% endblock %}

You will realize that the columns get stacked in the PDF file with the col-md- columns which is probably not the expected behavior.

The solution is to set the Viewport size for wkhtmltopdf:

# app/config/config.yml
knp_snappy:
    pdf:
        # ...
        options:
            - { name: 'viewport-size', value: ‘1024x768’ }
            - { name: 'page-size', value: 'A4' }

With such a view-port size, the page will be like a medium screen and the columns won’t get stacked.