Generating semi dynamic html templates using Phantom js and Drupal
Recently a client approached us with a task to help him generate hundreds of business cards in html, to be used as email signatures for his staff. They already had a basic template that was created by hand, but reproducing it for every employee was very time consuming.
There were 4 main challenges to this:
The first was making updates very easy for the client, without having to resort to us or his IT staff to make changes or generate additional cards. The second challenge was that the card template wouldn’t remain the same for every employee as, besides their personal data (name, position, contact information), it had a series of clickable tabs at the top and the bottom that led to different url’s of their organization. The tabs were implemented using an image map with <area> tags (Hello 1997!) and could vary in number, positioning and the pages they linked to.
The third obstacle that we faced was that the templates would have to be accessible from everywhere (otherwise it wouldn’t work when someone opened the email) and the resulting html would have to be presented to the user for easy copying and pasting in his email client.
Finally, the resulting file had to look exactly like the template in terms of font rendering, colors and presentation in general as the client was already using it for a select number of people and didn’t want to have them update their signatures.
Solving the first challenge was no problem for our trusted companion Drupal and a new custom content type, containing all the information that was going to be placed in the card, along with cck link fields for storing the tab links.
Thus, we came up with the following workflow:
User creates a new business card node, entering his information, picture and links. When the node is saved a html template is filled with the info and placed on the public files directory. A HTML to png converter is called, reading the html file, calculating the dimensions of the tabs and saving it as a png image. The email signature template is populated using the resulting image as a background, the tab dimensions to define the clickable ’s, and saved in the node. When the node is viewed, we present the email signature’s html in a readonly textarea and place it in a file that an <iframe> is pointing (for previewing the results)
After evaluating a number of command line HTML to x generators, we chose PhantomJS as it’s rendering engine is webkit (the same that’s used in Safari and up until recently Chrome) and it offers excellent CSS rendering capabilities that would allow us to leverage our existing HTML/CSS expertise without having to debug issues with subpar results from other rendering engines.
PhantomJS works using simple Javascript files for controlling it’s parameters, passed as arguments from the command line:
phantomjs --disk-cache=false generate_template.js
The generate_template.js file contains the following code:
var page = require('webpage').create(); page.onConsoleMessage = function(msg, lineNum, sourceId) { console.log(msg ); }; page.viewportSize = { width: 352, height: 278 }; page.clipRect = { top: 0, left: 0, width: 352, height: 278 }; page.open('$html_file_url', function () { window.setTimeout(function () { page.render('".$save_location."'); phantom.exit(); }, 200); });
Interesting points in the above are the onConsoleMessage event, that instructs Phantom to output in stdout any messages received in it’s console, viewportSize and clipRect that define the size of the simulated “screen” and cutoff size for the image and finally the setTimeout function that’s used to make sure that the html file was rendered successfully and all of it’s JS has run before PhantomJS begins to create the png image.
The tabs were calculated using JS in the html file that PhantomJS loads and then logged in Phantom’s console (and then outputted to stdout):
$(document).ready(function(e) { var dimensions = {}; $("#top_tabs").children().each(function( index ) { var offset = $(this).offset(); var fix1 = fix2 = 0; if (index == 0) {fix1 = 5;} if (index == 1) {fix2 = 5;} dimensions[$(this).text()] = (offset.left - 6 - fix2) + "," + offset.top + "," + (offset.left + $(this).outerWidth() - 6 - fix1) + "," + (offset.top + $(this).outerHeight()); }); $("#below_picture").offset({left: new_left}); console.log(JSON.stringify(dimensions)); });