How to create a PDF from HTML using SnappyBundle (wkhtmltopdf) in Symfony 3


Have you ever tried to create PDFs with complicated and fancy charts using PHP libraries like DomPdf or TCPDF, such a **** task isn't ?. There are not a lot of available server-side free libraries to generate charts, and those one that are available, do not create so nice charts for being embedded as an image in our PDF.

An alternative to server-side generated charts, are the generation of charts with Javascript (client-side). There are a lot of Javascript libraries that allow to create fancy charts and you can retrieve its SVG (or an base64 image) and return it to the server to include them as an image in your PDF. Sounds good and functional, however there's a disadvantage with this solution. The SVG will change and will cause problems with the dimensions later in your PDF according to the resolution of the monitor of your user or the dimensions of the browser window.

Some web apps, as a solution, will generate an html view as response into the browser (which will render your fancy charts) and the user will need to use (and know how) the Browser to save (or print) the content as PDF. However, as an user, you may just want to download a PDF with a single button, choose where you want to save it and that's all. Not an easy task to do with libraries like TCPDF don't you think?.

Problems everywhere, but that's where wkhtmltopdf comes in handy, as you'll be able to write css,html and javascript in the same way you do in a website (with some and minimal limitations obviously) and your PDF will look awesome.

In this article you'll learn how to implement SnappyBundle in your symfony 3 project and how to generate PDFs of different ways.

Requirements

You'll need wkhtmltopdf available in your system and accesible in the command prompt. wkhtmltopdf is a command line tool to render HTML into PDF and various image formats using the Qt WebKit rendering engine. These run entirely "headless" and do not require a display or display service.

  • Windows: you can download an installer for each architecture (x86 and x64) in the installation area. Although you can change the path of the wkhtmltopdf executable later in the config.yml file, is recommendable to have wkhtmltopdf accesible as an environment variable on your system (if you don't want to create an environment variable for wkhtmltopdf, then you can provide the entire path to the executable later). You can read how to create environment variables in windows in this article.
  • Debian/Ubuntu: You can install the distribution from wkhtmltopdf directly in the console using the following command :
$ sudo apt-get install wkhtmltopdf

Visit the homepage of wkhtmltopdf for more information here.

1) Installing and configuring SnappyBundle

Snappy by itself is a PHP (5.3+) wrapper for the wkhtmltopdf conversion utility. It allows you to generate either pdf or image files from your html documents, using the webkit engine. The KnpSnappyBundle provides a simple integration for your Symfony project.

To install SnappyBundle in your project, execute the following composer command :

composer require knplabs/knp-snappy-bundle

Or add manually adding the package name into your composer.json file and then execute composer install:

{
    "require": {
        "knplabs/knp-snappy-bundle": "~1.4"
    }
}

When the download is complete, enable the bundle adding the following line to your kernel :

$bundles = [
    //..//
    new Knp\Bundle\SnappyBundle\KnpSnappyBundle(),
];      

Finally you just need to add the basic configuration in your config.yml file providing and enabling the binary path of wkhtmltopdf.

Note that as mentioned previously, SnappyBundle requires wkhtmltopdf to work, therefore we need to provide in the binary option of the config.yml the fullpath to the executable of wkhtmltopdf before using it, otherwise you'll face one of the most known errors :

Binary path in Windows

With the default installer (and default installation settings) of wkhtmltopdf, there should be a folder wkhtmltopdf/bin in the Program Files of your main partition with the executable of wkhtmltopdf inside, so you only need to provide the path of the following example.

However, if you used a custom installation, just change the path with the new one that contains the wkhtmltopdf executable in the binary property.

# Windows configuration
knp_snappy:
    pdf:
        enabled:    true
        # If you have wkhtmltopdf as an environment variable you don't need to provide the
        # full path to the executable, use it in the same way as you use in the console
        #binary:  "wkhtmltopdf"
        binary:     "\"C:\\Program Files\\wkhtmltopdf\\bin\\wkhtmltopdf.exe\""
        options:    []
    image:
        enabled:    true
        binary:     "\"C:\\Program Files\\wkhtmltopdf\\bin\\wkhtmltoimage.exe\""
        options:    []

Binary path in Linux/Unix like

If you installed wkhtmltopdf using the apt-get method, the paths probably are :

# app/config/config.yml
knp_snappy:
    pdf:
        enabled:    true
        binary:     /usr/local/bin/wkhtmltopdf
        options:    []
    image:
        enabled:    true
        binary:     /usr/local/bin/wkhtmltoimage
        options:    []

How to change PDF generation settings

You can dinamically change settings by default in the config.yml file :

# config.yml
knp_snappy:
    pdf:
        enabled:    true
        binary:     "wkhtmltopdf"
        options:    
            no-outline: true
            page-size: LETTER
            # Recommended to set UTF-8 as default encoding :)
            encoding: UTF-8

Or dinamically in your controller (or service etc) using PHP with the setOption method of snappy :

$snappy = $this->get('knp_snappy.pdf');
$snappy->setOption('no-outline', true);
$snappy->setOption('page-size','LETTER');
$snappy->setOption('encoding', 'UTF-8');

You can see a complete list of all the options available for wkhtmltopdf in this document.

Change cache path of the generation of PDFs

Snappy uses the sys_get_temp_dir() method by default to get the temporary files folder of the system to save the PDFs, however you can change the path that you want changing with the temporary_folder property :

# app/config/config.yml
knp_snappy:
    temporary_folder: %kernel.cache_dir%/snappy

The previous example would target a folder in the cache folder of your SF3 project (var/cache/snappy).

2) Examples

Render PDF from a web url

The following example will render Our Code World homepage in a PDF :

<?php

namespace sandboxBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends Controller
{
    public function indexAction()
    {
        $snappy = $this->get('knp_snappy.pdf');
        $filename = 'myFirstSnappyPDF';
        $url = 'http://ourcodeworld.com';
        

        return new Response(
            $snappy->getOutput($url),
            200,
            array(
                'Content-Type'          => 'application/pdf',
                'Content-Disposition'   => 'inline; filename="'.$filename.'.pdf"'
            )
        );
    }
}

Render PDF from a project url (symfony routing)

With 2 routes in this example :

sandbox_homepage:
    path:     /
    defaults: { _controller: sandboxBundle:Default:index }
    
sandbox_pdfexample:
    path:     /pdf-example
    defaults: { _controller: sandboxBundle:Default:pdf }

And the controller :

<?php

namespace sandboxBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class DefaultController extends Controller
{
    public function indexAction()
    {
        return $this->render('sandboxBundle:Default:index.html.twig',[
            
        ]);
    }
    
    /**
     *  Render in a PDF the sandbox_homepage URL
     * @return Response
     */
    public function pdfAction()
    {
        $snappy = $this->get('knp_snappy.pdf');
        $filename = 'myFirstSnappyPDF';
        
        // use absolute path !
        $pageUrl = $this->generateUrl('sandbox_homepage', array(), UrlGeneratorInterface::ABSOLUTE_URL);
        
        return new Response(
            $snappy->getOutput($pageUrl),
            200,
            array(
                'Content-Type'          => 'application/pdf',
                'Content-Disposition'   => 'inline; filename="'.$filename.'.pdf"'
            )
        );
    }
}

Note: the host that's used when generating an absolute URL is automatically detected using the current Request object. When generating absolute URLs from outside the web context (for instance in a console command) this doesn't work. See How to Generate URLs from the Console to learn how to solve this problem.

Render PDF from the HTML of a twig view

<?php

namespace sandboxBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends Controller
{
    public function indexAction()
    {
        $snappy = $this->get('knp_snappy.pdf');
        
        $html = $this->renderView('sandboxBundle:Default:template.html.twig', array(
            //..Send some data to your view if you need to //
        ));
        
        $filename = 'myFirstSnappyPDF';

        return new Response(
            $snappy->getOutputFromHtml($html),
            200,
            array(
                'Content-Type'          => 'application/pdf',
                'Content-Disposition'   => 'inline; filename="'.$filename.'.pdf"'
            )
        );
    }
}

Return PDF response for being viewed in the browser

<?php

namespace sandboxBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends Controller
{
    public function indexAction()
    {
        $snappy = $this->get('knp_snappy.pdf');
        
        $html = '<h1>Hello</h1>';
        
        $filename = 'myFirstSnappyPDF';

        return new Response(
            $snappy->getOutputFromHtml($html),
            200,
            array(
                'Content-Type'          => 'application/pdf',
                'Content-Disposition'   => 'inline; filename="'.$filename.'.pdf"'
            )
        );
    }
}

Return PDF response as an attachment (download response)

<?php

namespace sandboxBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends Controller
{
    public function indexAction()
    {
        $snappy = $this->get('knp_snappy.pdf');
        
        $html = '<h1>Hello</h1>';
        
        $filename = 'myFirstSnappyPDF';

        return new Response(
            $snappy->getOutputFromHtml($html),
            200,
            array(
                'Content-Type'          => 'application/pdf',
                'Content-Disposition'   => 'attachment; filename="'.$filename.'.pdf"'
            )
        );
    }
}

Basic example of the main features (and advantages) of wkhtmltopdf

Just work in the way you do in the browser, wkhtmltopdf will be at charge of converting that to pdf. In this example, we are going to generate a PDF with some basic features as usage of javascript, svg etc.

<h1>{{title}}</h1>
<p>This is my awesome first PDF generated using Snappy in my Symfony 3 project.</p>
<p>UTF-8 Test : κ?σμε</p>
<p>Image : </p><br>
<img height="200" src="http://ourcodeworld.com/resources/img/ocw-empty.png"/>
<p>SVG Example : </p><br>
<div>
    <!-- Do not forget to give width and height to your svg 
        use PLAIN SVG directly (<svg>content</svg>)
        Tiger SVG EXAMPLE : https://upload.wikimedia.org/wikipedia/commons/f/fd/Ghostscript_Tiger.svg
    -->
</div>
<p>Canvas Example with Javascript: </p><br>
<div>
    <canvas id="myCanvas" width="300" height="200"/>
</div>
<hr>
<span id="dinamic-content"></span>
<script>
    document.getElementById("dinamic-content").innerHTML = 'This string has been appended using javascript';
    
    // Draw circle on canvas
    var c = document.getElementById("myCanvas");
    var ctx = c.getContext("2d");
    ctx.beginPath();
    ctx.arc(100,75,50,0,2*Math.PI);
    ctx.stroke();
</script>

We are going to return the generated PDF to the browser with a simple controller:

<?php

namespace sandboxBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends Controller
{
    public function indexAction()
    {
        $snappy = $this->get('knp_snappy.pdf');
        
        $html = $this->renderView('sandboxBundle:Default:template.html.twig', array(
            'title' => 'Hello World !'
        ));
        
        $filename = 'myFirstSnappyPDF';

        return new Response(
            $snappy->getOutputFromHtml($html),
            200,
            array(
                'Content-Type'          => 'application/pdf',
                'Content-Disposition'   => 'inline; filename="'.$filename.'.pdf"'
            )
        );
    }
}

And the PDF output should look like :

SnappyBundle PDF from a twig view - javascript

Conclusions

  • You need wkhtmltopdf installed on your machine. Snappy is just a wrapper which makes easy for you the generation of PDFs easily with a couple of lines of PHP. If you're not a friend of installation setups, you can use a "portable" version in your project using wkhtmltopdf as a composer dependency.
  • wkhtmltopdf allow you to create images (screenshots) from web urls (or plain html) and SnappyBundle has an included wrapper for it, read more about this feature in the documentation.
  • Thanks to wkhtmltopdf, you're able to use Javascript to modify your HTML content to generate a PDF, which comes in handy to show charts or graphics in a PDF with client-side code (libraries like Highcharts, D3.js etc).

Have fun !

Become a more social person