cult3

Generating PDFs from HTML and PhantomJS

Oct 26, 2015

Table of contents:

  1. Why generating PDFs can be difficult
  2. Adding PhantomJS to your project
  3. Creating the config.js file
  4. The PDF Writer
  5. Conclusion

Generating PDFs is one of those things that can be a pain in the arse to build, but is quite a common requirement of web applications.

For example, a lot of web applications require reports, or invoices, or any number of other things that need to be printed. PDFs are much better than a simple print stylesheet as they can also be saved or emailed to a colleague.

There are many ways to generate PDFs dynamically, from using some king of object builder, to using XML and an XSLT template.

But by far the easiest is using plain old HTML, CSS and PhantomJS.

In today’s tutorial I’m going to walk you through how I generate PDFs in web applications.

Why generating PDFs can be difficult

Fundamentally, the hard thing about creating PDFs dynamically is trying to get the thing to look how you want.

Using an object builder is really difficult because it’s hard to translate a design into method calls.

Using XML and XSLT is difficult because you are limited to the style and positioning attributes of XSLT.

Using HTML and CSS, for all that they can be annoying, is actually by far the easiest and best way of designing a page dynamically using code.

By sticking to HTML and CSS, we can very easily create the style and layout of the PDF without the hassle of the other methods.

This is perfect because HTML and CSS is also much easier to learn, and we can leverage existing CSS frameworks or brand specific styles.

Adding PhantomJS to your project

The first thing you need to do is to add PhantomJS to your project. If you know your application will only ever run on one type of operating system, you only need that single version.

If you are making a generic package, you will need to do some system checking to make sure you use the right version.

In either case you can download PhantomJS here.

Add the downloaded file to a directory in your project. A good place would be the bin directory as this is usually where executables live.

Creating the config.js file

Next we need to create a config.js file that will hold the configuration details for the PDF.

var fs = require("fs"),
  args = require("system").args,
  page = require("webpage").create();

page.content = fs.read(args[1]);
page.viewportSize = { width: 600, height: 600 };
page.paperSize = {
  format: "A4",
  orientation: "portrait",
  margin: "1cm",
  footer: {
    height: "1cm",
    contents: phantom.callback(function (pageNum, numPages) {
      return (
        '<div style="text-align: right; font-size: 12px;">' +
        pageNum +
        " / " +
        numPages +
        "</div>"
      );
    }),
  },
};

window.setTimeout(function () {
  page.render(args[1]);
  phantom.exit();
}, 250);

I’m going to be keeping this fairly simple. Here I’m basically just accepting the content, setting up the template and then capturing the output. I’m also displaying the page number in the footer of each page.

The PDF Writer

Next we can create the PDFWriter class that will be responsible for creating the PDFs.

First I will inject an object that implements Laravel’s Filesystem contract:

use Illuminate\Contracts\Filesystem\Filesystem;

class PDFWriter
{
    /**
     * @var Filesystem
     */
    private $storage;

    /**
     * @param Filesystem $storage
     */
    public function __construct(Filesystem $storage)
    {
        $this->storage = $storage;
    }
}

We need to interact with the filesystem, and that’s the easiest way of doing it.

Next I will define a write() method that will accept a template and an array of data to be displayed in the PDF:

/**
 * Write the PDF
 *
 * @param string $template
 * @param array $data
 * @return string
 */
public function write($template, array $data)
{

}

I’m just going to generate a filename for the PDF because in this example it doesn’t really matter what the file is called.

$filename = sprintf("export-%s.pdf", time());

Next we can use the handy Laravel view() function to generate the HTML by passing the template and the array of data. We can then use the injected Filesystem object to save the output:

$this->storage->put($filename, view($template, $data));

Next we need to generate the PDF using PhantomJS. First we grab the path to the file we just created:

$path = storage_path(sprintf("app/%s", $filename));

Next we grab the paths to the PhantomJS executable and the config.js file from earlier:

$phantom = base_path("bin/phantom/phantomjs");
$config = base_path("bin/phantom/config.js");

Next we generate the command to generate the PDF:

$command = sprintf("%s —ssl-protocol=any %s %s", $phantom, $config, $path);

Next we can run the process:

$process = (new Process($command, __DIR__))->setTimeout(10)->mustRun();

And finally we can return the $path of the newly created PDF from the write() method:

return $path;

Here is the PDFWriter class in full:

use Illuminate\Contracts\Filesystem\Filesystem;

class PDFWriter
{
    /**
     * @var Filesystem
     */
    private $storage;

    /**
     * @param Filesystem $storage
     */
    public function __construct(Filesystem $storage)
    {
        $this->storage = $storage;
    }

    /**
     * Write the PDF
     *
     * @param string $template
     * @param array $data
     * @return string
     */
    public function write($template, array $data)
    {
        $filename = sprintf("export-%s.pdf", time());

        $this->storage->put($filename, view($template, $data));

        $path = storage_path(sprintf("app/%s", $filename));

        $phantom = base_path("bin/phantom/phantomjs");
        $config = base_path("bin/phantom/config.js");
        $command = sprintf(
            "%s —ssl-protocol=any %s %s",
            $phantom,
            $config,
            $path
        );
        $process = (new Process($command, __DIR__))->setTimeout(10)->mustRun();

        return $path;
    }
}

Conclusion

In today’s tutorial we saw how we can use HTML and CSS to generate PDFs.

Using HTML and CSS is by far the best way to generate PDFs because of how easy it is to design them. You can also leverage CSS Frameworks such as Bootstrap, so you don’t have to reinvent the wheel.

With the class we have created today, you can very easily generate all types of pdfs for your application, from invoices to reports, and they will be incredibly easy to build because it’s just another view of your application.

I was first introduced to this technique by Laravel Cashier. You should definitely checkout that project if you are looking to add this sort of a functionality to a generic package.

In next week’s tutorial we will be looking at how we can take this same idea a big step further.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.