PhantomJs: Generate Pixel Perfect PDF Reports

I've been working lately on a financial project that needs to implement a WYSIWYG reporting module. Which means the module offers to clients the possibility of building their own reports (charts, tables, text ...) and the generated pdf should be exactly the same, a pixel perfect clone of what they've built.

The project was java based, and traditional solutions will just not work. So we had 2 solutions: find out a quick alternative, or get ready to quit our job and become farmers.

HTML/CSS can have many downsides, but the reality is that they are one of the easiest ways to programatically generate a document with any design we could think of.

So We thought that if We wanted our reports to follow the branding of our web application, we should be rendering HTML/CSS to a PDF.

This will even allow you to generate the PDF reports using your existing code, be it Angular/React… etc.

PhantomJs

PhantomJS is a “headless” browser, that can render a web page just like Chrome, from the command line. And it can output the website as an image or pdf.

You can actually run this from Java, NodeJS, Python, or whichever language that allows you to call an executable. In fact from NodeJS it would be even easier since PhantomJS runs in Node.

Since We did this for a reporting system at work, that was built in Java, I’ll focus on that.

You’ll need three things to generate a report:

1- PhantomJS executable
2- JavaScript config file for PhantomJS
3- HTML file to convert into PDF

Install PhantomJS

Nothing much to say here, one command to run them all:

npm install phantomjs

JS Config file

The following file is a basic config file, that will configure PhantomJS to generate a standard A4 PDF, in portrait mode.

It will take the HTML content and the PDF output file from the CLI arguments.

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

    page.paperSize = {
        format: 'A4',
        orientation: 'portrait',
        margin: {
            top: "1.5cm",
            bottom: "1cm"
        },
        footer: {
            height: "1cm",
            contents: phantom.callback(function (pageNum, numPages) {
                return '' +
                    '<div style="margin: 0 1cm 0 1cm; font-size: 0.65em">' +
                    '   <div style="color: #888; padding:20px 20px 0 10px; border-top: 1px solid #ccc;">' +
                    '       <span>REPORT FOOTER</span> ' +
                    '       <span style="float:right">' + pageNum + ' / ' + numPages + '</span>' +
                    '   </div>' +
                    '</div>';
            })
        }
    };

    // This will fix some things that I'll talk about in a second
    page.settings.dpi = "96";

    page.content = fs.read(system.args[1]);

    var output = system.args[2];

    window.setTimeout(function () {
        page.render(output, {format: 'pdf'});
        phantom.exit(0);
    }, 2000);

HTML File

Any file, with any contents will work just fine. To load local resources you can just use the file:// protocol.

    <!DOCTYPE html>

    <html lang="en">
    <head>
        <title>The title is irrelevant</title>
        <meta http-equiv="content-type" content="text/html; charset=UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">

        <style type="text/css" media="all">
            html {
                margin:0;
                zoom: 1; 
            }
        </style>
    </head>

    <body>
      <h1>Sample report</h1>
      <p>Oh Look! It's Working</p>
    </div>
    </body>
    </html>

Executing

If running this from the command line, just make sure you have NodeJS and PhantomJS installed (duh… ), then run:

 phantomjs configFile.js htmlFile.html output.pdf

If you wanted to run this programatically from Java for example, you could use:

  // Get HTML Report
  URL htmlFileUrl = this.getClass().getResource("htmlFile.html");
  File htmlFile = Paths.get(htmlFileUrl.toURI()).toFile();

  // Get JS config file
  URL configFileUrl = this.getClass().getResource("configFile.js");
  File configFile = Paths.get(configFileUrl.toURI()).toFile();

  // tmp pdf file for output
  File pdfReport = File.createTempFile("report", ".pdf");

  ProcessBuilder renderProcess = new ProcessBuilder(
          "/path/to/phantomjs",
          configFile.getAbsolutePath(),
          htmlFile.getAbsolutePath(),
          pdfReport.getAbsolutePath()
  );

  Process phantom = renderProcess.start();

  // you need to read phantom.getInputStream() and phantom.getErrorStream()
  // otherwise if they output something the process won't end

  int exitCode = phantom.waitFor();

  if(exitCode != 0){
      // report generation failed
  }

  // success!

The coolest thing? If you can render it in HTML, you can render it in your report!

This means that now you can almost directly copy/paste from your application, you can even use AngularJS/React to generate the content… The possibilities are endless.

Well, let's end this post by this image that highlight a report we were able to generate using phantomJS ;)