Work
with us

Tell us about your idea
and we will find a way
to make it happen.

Get estimate

Join our awesome team

Check offers
M bro

This is a common task in our job. Almost every project I have been in, needed to generate a pdf.

It could be a report for an admin or a pretty personalized attachment that would be sent to all users via email. It's not a problem when it comes to generate a simple table or just some texts. But belive me, things got complicated when you need the pdf to be pixel perfect or you need to use dynamic javascript to for example generate charts or maps on it. 

Basically there are two most used gems in rails community: wicked_pdf and prawn. Both of them are pretty good and might be enough for your requirements. For example wicked_pdf uses wkhtmltopdf that 'screenshoots' the page. Phantom also makes a screenshot. But there's one crucial difference. PhantomJS is using WebKit and wkhtmltopdf uses some weird Qt Web browser. If you want to run this browser on your own go ahead and visit http://www.qtweb.net/

 

Simple use of phantom.js

It's very simple. Phantom.js is technically a binary that will help you generate pdf's from urls or files. The .js in Phantom.js means that you will need to write a javascript file ;) First ensure that you have phantom.js installed. On OS you can just:

brew install phantomjs

Then let's create a file boom.js and in the file write something like this:

var page = require('webpage').create();
page.open('https://google.com/', function() {
  page.render('google.pdf');
  phantom.exit();
});

And when you type:

phantomjs boom.js

You should have pdf with the picture of google site.

Using phantom.js in Ruby on Rails - inline way

The main problem is that in most cases you would need to pass some variables into your view that has to be generated as a pdf. You can acomplish that this way:

First of all let's create a phantom.js.erb file that will be our matrix for our further operations:

var page = require('webpage').create();
page.content = "<%= raw(j page_content) %>"


page.onLoadFinished = function(status) {
  page.render("our_page.pdf");
  phantom.exit();
}

We have page.onLoadFinished callback which is executed when page loads (status contains either success or false string but we won't check it rigt know and trust that always everything goes smooth). Also notice that we have something like "page_content". We will basically put all the content we need for that page from other place. 

Somewehere in service define this private method which will just render to string our html view with our variables:

def rendered_html
  ActionController::Base.new.render_to_string(
    template: 'path/to_template.html.haml',
    layout: 'layouts/your_layout.html.haml',
    locals: { foo: @foo, bar: @bar, baz: @baz }
  ).to_str
end

Now we need to pass this string into our phantom.js.erb file. The disadvantage of phantom.js is that it's working on files, because it's executed from the console. So we need to create a tempfile:

def phantomjs_file
  phantomjs_file = ActionController::Base.new.render_to_string 'folder_in_views/phantom.js.erb', locals: { page_content: rendered_html }

  tempfile = Tempfile.new('phantomjs_file.js', Rails.root.join('tmp'))
  tempfile.tap do
    tempfile.write phantomjs_file.to_str
    tempfile.close
  end
end

And we are rady to go, we can invoke in service/controller:

def generate_pdf
    temporary_phantom_file = phantomjs_file
    system("phantomjs #{temporary_phantom_file.path}")
  rescue => e
    Rails.logger.error(e)
  ensure
    temporary_phantom_file.unlink if temporary_phantom_file
end

The system method will run the command and our pdf will be generated. Don't forget to unlink the temp file, otherwise it won't be so temp ;)

There is one issue with this solution. You would need to include the css inline into the view. Same with fonts. To avoid that we could pass variables dynamically as params. So your phantom.js.erb would look like this:

var page = require('webpage').create();

page.open("<%= YOUR_URL %>/some_path/<%= foo %>?bar=<%= bar %>&baz<%= baz%>", function(status) {
  page.render("our_page.pdf");
  phantom.exit();
});

It's up to you which solution you like (I like the second one, but only concern may be the url+params length)

Generating pdf with dynamic javascript content 

Sometimes you need to generate some map or a chart in the pdf. It would be smart if you tell somehow when phantom is ready to take a snapshot of the page. When it comes to drawing a map or chart it's sometimes take some time, but the page body loads. After you are certain that the page is ready you can set window.status on your page (you can set it after timeout so you are sure that the object is drawn). Then in phantom.js.erb file you can do something like this:

var page = require('webpage').create();

page.open("<%= YOUR_URL %>/some_path/<%= foo %>?bar=<%= bar %>&baz<%= baz%>", function(status) {
  if (status !== 'success') {
      console.log('Unable to load the address!');
      phantom.exit();
  } else {
      window.setInterval(function () {
          var windowStatus = page.evaluate(function () {
              return window.status;
          });
          if (windowStatus == 'your-ready-status') {
            page.render(".<%= file_path %>");
            phantom.exit();
          }
          if (windowStatus == 'you-can-set-failure-status-if-you-want') {
            phantom.exit(1);
          }
      }, 1000); 
  }
});

In this example we basically wait till window-status is set correctly. Notice that you can run phantom.exit with an argument, it may be used to track errors.

Summary

This blog post was created as a result of my work over a tricky task. I want to thank Maciej Brodecki for cracking the phantom.js for me and presenting me results. I think I will use phantom.js from now on to generate pdf's. Happy coding!

Post tags: