CSS diffs #2

May 9th, 2014. Tagged: CSS

Continuing from last night...

First, two twitter responses pointed to even more readily-available options for comparing screenshots. One is Wraith from BBC engineers which supports Firefox/Gecko (via SlimerJS) in addition to PhantomJS. The other is the almost-ready siteeffect.io which is based on http://dalekjs.com/ which seems to support all the browsers!

Anyway, I thought I should add Firefox support to my brave little script via SlimerJS and also do what I hinted at the end of the previous post - do the image diff in nodejs-land instead of ImageMagick.

SlimerJS

It's pretty cool, just like PhantomJS. I didn't manage to install it using regular package managers (like brew) but luckily you can just download it standalone and run from anywhere. So I shoved in ~/Downloads/slimerjs

A quick script to add Mozilla/Gecko support to the url2png functionality:

var system = require('system');
var url = system.args[1];
var png = system.args[2];
 
var page = require('webpage').create();
page.viewportSize = { width: 800, height: 600 };
page
  .open(url)
  .then(function() {
    page.render(png);
    phantom.exit();
  });

Testing:

$ ~/Downloads/slimerjs/slimerjs url2png-moz.js http://google.com goo.png

This makes a screenshot of Google's homepage into goo.png

Image diff in nodejs

I found this library called pngparse (hmm, no GD in NodeJS?) which does exactly what I need - reads a png and gives width/height and array of RGBA pixel values, just like image data you'd get from HTML canvas.

So here's a function that simply returns true if the two images are the same:

function sameImage(image_a, image_b, cb) {
  var pngparse = require('pngparse');
  pngparse.parseFile(image_a, function(err, a) {
    if (err) {
      console.log('Where the first file? ' + image_a + ' is not it.');
      process.exit(2);
    }
    pngparse.parseFile(image_b, function(err, b) {
      if (err) {
        console.log('Where the second file? ' + image_b + ' is not it.');
        process.exit(3);
      }
 
      // easy stuffs first
      if (a.data.length !== b.data.length) {
        return cb(false);
      }
 
      // loop over pixels, but
      // skip 4th thingie (alpha) as these images should not be transparent
      for (var i = 0, len = a.data.length; i < len; i += 4) {
        if (a.data[i]     !== b.data[i] ||
            a.data[i + 1] !== b.data[i + 1] ||
            a.data[i + 2] !== b.data[i + 2]) {
          return cb(false);
        }
      }
      return cb(true);
    });
  });
}

The benefits of doing this in Node instead of ImageMagick is just speed. (Disclaimer: Not that I tested but...). There's no separate cmd like process to exec(), there's no weird metrics to calculate, just give up as soon as just one of the R, G or B channels is different between the two images. Skip Alpha. Or even give up as soon as one of the images has more data, meaning different geometry, meaning definitely not the same image.

End result

So with a bit of reshuffling and adding support for both webkit and gecko and changing the image diff logic, here's what I ended up with:

var exec = require('child_process').exec;
var read = require('fs').readFileSync;
var sprintf = require('sprintf').sprintf;
var write = require('fs').writeFileSync;
 
var tmp = process.cwd() + '/tmp/';
 
var html = read('source/zen.html', 'utf8');
var before = html.replace('{{{FILE}}}', '../source/before.css');
var after = html.replace('{{{FILE}}}', '../source/after.css');
 
write('tmp/before.html', before);
write('tmp/after.html',  after);
 
var phantoms = [
  {name: 'webkit', path: 'phantomjs'},
  {name: 'moz', path: '~/Downloads/slimerjs/slimerjs'},
];
 
phantoms.forEach(function(tool) {
  var before = 'tmp/before-' + tool.name + '.png';
  var after =  'tmp/after-'  + tool.name + '.png';
  var giff =   'tmp/giff-'   + tool.name + '.gif';
  var before_cmd = sprintf('%s url2png-%s.js "%s" %s',
    tool.path,
    tool.name,
    'file://'+ tmp + 'before.html',
    before);
  var after_cmd = sprintf('%s url2png-%s.js "%s" %s',
      tool.path,
      tool.name,
      'file://'+ tmp + 'after.html',
      after);
  var giff_cmd = sprintf('convert -delay 50 -loop 0 %s %s %s', before, after, giff);
 
  exec(before_cmd, function() {
    exec(after_cmd, function() {
      sameImage(before, after, function(same) {
        if (same) {
          console.log('all good in ' + tool.name);
        } else {
          exec(giff_cmd);
          console.log('Bad, bad! Failure in ' +
            tool.name + '. See stuff in ' + tmp +
            ', e.g. ' + giff + ' to help you debug');
            process.exit(1);
        }
      });
    });
  });
});

You can browse all the files right here: http://www.phpied.com/files/diffcss2/

Next?

1. I'm pretty sure there's gotta be a way to do animated GIFs with NodeJS so dump ImageMacick completely
2. What about the elephant in the room - IE6-11?

Tell your friends about this post: Facebook, Twitter, Google+

Sorry, comments disabled and hidden due to excessive spam. Working on restoring the existing comments...

Meanwhile, hit me up on twitter @stoyanstefanov