Skip to content

Instantly share code, notes, and snippets.

@tobiashm
Created August 30, 2017 08:02
Show Gist options
  • Save tobiashm/4cd44d2a223c744059f570dd722906e2 to your computer and use it in GitHub Desktop.
Save tobiashm/4cd44d2a223c744059f570dd722906e2 to your computer and use it in GitHub Desktop.
Use PhantomJS to render PDF in Rails

Use PhantomJS to render PDF in Rails

Avoid disk IO

If you're deploying your app to somewhere where you can't be sure to have reliable disk read/write access, the normal strategy of writing to a temp-file doesn't work. Instead we can open a pipe to the Phantom.js process, and then pass in the HTML via stdin, and then have the rasterize.js script write out the resulting PDF to stdout, which we can then capture. Any log messages from the Phantom.js process can be passed via stderr if we want.

Session heist

If we're using the default Rails setup for session handling, i.e. a _session_id cookie, we can just pass in the session ID and have the rasterize.js script fake a session cookie.

Configuring Phantom.js

We can provide some additional configuration for the Phantom.js process using a config.json file. (I can't remember exactly why we set the things as we did, but some of them were needed.)

{
"diskCacheEnabled": true,
"webSecurityEnabled": false,
"ignoreSslErrors": true,
"localUrlAccess": true,
"localToRemoteUrlAccessEnabled": true
}
# lib/phantomjs/pdf.rb
require "phantomjs"
module Phantomjs
def self.pdf(html, request)
Phantomjs::PDF.new.render(html, request)
end
class PDF
def render(html, request)
url = request.url
session_id = request.session.id
ActiveSupport::Notifications.instrument("render_template.action_view", identifier: rasterize_js) do
open(%(|#{Phantomjs.path} --config="#{config_json}" "#{rasterize_js}" "#{url}" "#{session_id}"), File::RDWR) do |io|
io.write(html)
io.close_write
io.read
end
end
end
private
def config_json
File.join(__dir__, "config.json")
end
def rasterize_js
File.join(__dir__, "rasterize.js")
end
end
end
var fs = require('fs');
var system = require('system');
var webpage = require('webpage');
var content = fs.read('/dev/stdin');
var url = system.args[1];
var sessionId = system.args[2];
phantom.addCookie({
'name': '_session_id',
'value': sessionId,
'domain': 'localhost',
'path': '/',
'httponly': true,
'secure': false
});
var page = webpage.create();
page.setContent(content, url);
function checkReadyState() {
var readyState = page.evaluate(function() { return document.readyState; });
if (readyState === 'complete') {
onPageReady();
} else {
setTimeout(checkReadyState);
}
}
function onPageReady() {
page.render('/dev/stdout', { format: 'pdf' });
phantom.exit();
}
checkReadyState();
var fs = require('fs');
var system = require('system');
var webpage = require('webpage');
var page = webpage.create();
var output = system.stderr;
page.onConsoleMessage = function(msg, lineNum, sourceId) {
output.writeLine('CONSOLE: ' + msg + ' (from line #' + lineNum + ' in "' + sourceId + '")');
};
function logError(source, msg, trace) {
output.writeLine(source + ' ERROR: ' + msg);
trace.forEach(function(item) {
output.writeLine(' ' + item.file + ':' + item.line);
});
}
page.onError = function(msg, trace) {
logError('PAGE', msg, trace);
};
phantom.onError = function(msg, trace) {
logError('PHANTOM', msg, trace);
phantom.exit(1);
};
page.onResourceRequested = function(request) {
output.writeLine('REQUEST: ' + JSON.stringify(request, undefined, 4));
};
var content = fs.read('/dev/stdin');
var url = system.args[1];
var sessionId = system.args[2];
phantom.addCookie({
'name': '_session_id',
'value': sessionId,
'domain': 'localhost',
'path': '/',
'httponly': true,
'secure': false
});
page.setContent(content, url);
var paperSize = {
format: 'A4',
margin: {
top: '1cm',
bottom: '1cm',
left: '2cm',
right: '2cm'
}
};
// Define PDF header and footer using HTML template elements.
// Example: `<template id="pdf-footer" data-height="1cm">Page <strong>%{pageNum}</strong></template>`
['header', 'footer'].forEach(function(section) {
var template = page.evaluate(function(s) {
var element = document.querySelector('template#pdf-' + s);
return element && { height: element.dataset.height, contents: element.innerHTML, style: element.getAttribute('style') };
}, section);
if (!template) return;
paperSize[section] = {};
paperSize[section].height = template.height;
paperSize[section].contents = phantom.callback(function(pageNum, numPages) {
var html = template.contents.replace(/%{pageNum}/g, pageNum).replace(/%{numPages}/g, numPages);
return addPrintStyle(html, template.style);
});
});
function addPrintStyle(html, bodyStyle) {
return '<style media="print">\n' +
'body {' + bodyStyle + '}\n' +
printStyle() +
'</style>\n' +
html;
}
var cachedPrintStyle;
function printStyle() {
if (!cachedPrintStyle) {
cachedPrintStyle = page.evaluate(function() {
var p = Array.prototype;
return p.filter.call(document.styleSheets, function(s) {
return p.some.call(s.media, function(m) { return m === 'print'; });
}).map(function(s) {
return p.map.call(s.rules, function(r) { return r.cssText; }).join('\n');
}).join('\n');
});
}
return cachedPrintStyle;
}
page.paperSize = paperSize;
function checkReadyState() {
var readyState = page.evaluate(function() { return document.readyState; });
if (readyState === 'complete') {
onPageReady();
} else {
setTimeout(checkReadyState);
}
}
function onPageReady() {
page.render('/dev/stdout', { format: 'pdf' });
phantom.exit();
}
checkReadyState();
@Sri-K
Copy link

Sri-K commented Mar 16, 2020

Thank you very much Tobias.

@Sri-K
Copy link

Sri-K commented Mar 16, 2020

Thanks tobiashm.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment