Watermark images with node.js and GraphicsMagick

I wrote a small node app that watermarks images as an inside joke between friends, and I learned some pretty neat tricks along the way. The app will take a URL to an image and overlay a gigantic hand over it. On the useful scale, it is hovering at around a 3 (someone must have a use for this somewhere, right?) but it took a lot of Googling to make this work.

One of the big challenges for this was that I wanted to host it on heroku, and being a read-only filesystem I had to process all the images without touching the filesystem. Finding out how to do this in node was surprisingly difficult at the time.

I ended up getting around this by just streaming straight through using the gm and request npm modules. One thing to keep in mind is that since we’re on heroku we need to use ImageMagick, we can do that in gm with imageMagick = gm.subClass({imageMagick: true}).

If you'd like to browse through some of the actual source code to see how it all fits together, it is hosted on GitHub

The gnarly details

Once we have imageMagick we can start the watermarking process. Each request that comes in will take the following steps:

  1. Find which “hand” we want to use (the “type” param)
  2. Open up the sourceUrl (the “url” param) using the request package
  3. Stream that into imageMagick
  4. Resize the hand to match the source images’s dimensions
  5. Overlay the hand
  6. Stream the whole thing back to the use as a PNG
// Partial code from: https://github.com/robhurring/bighand/blob/master/controllers/hand.js
var imageMagick = gm.subClass({imageMagick: true});

// 2 & 3. stream the sourceUrl into imageMagick
imageMagick(request(sourceUrl)).size(function(err, size){
  if (err) return next(err);
  // tell the hand the source image's dimensions and ask it what size to be
  var resizeTo = hand.size(size.width, size.height);

  // 5. overlay the hand we want (NOTE: we make the request twice, not optimal)
  imageMagick(request(sourceUrl))
    // 4. resize the hand to fit the image's dimensions
    .overlayHand(hand, resizeTo)
    // 6. Stream it all back to the user
    .stream('png', function (err, stdout) {
      if (err) return next(err);
      res.setHeader('Expires', new Date(Date.now() + 604800000));
      res.setHeader('Content-Type', 'image/png');
      stdout.pipe(res);
    });
});

// 5. helper to overlay the hand
imageMagick.prototype.overlayHand = function(hand, resize) {
  return this.gravity(hand.gravity)
    .out('(', hand.path, ' ', '-resize', resize, ')')
    .out('-composite');
}

each hand is pulled from a config file and has the following properties:

module.exports = {
  hands: {
    'hand': {
      description: 'Just a hand',
      path: 'hands/hand.png',
      gravity: 'SouthWest',
      size: function(width, height) {
        // how to resize ourself
        return width + 'x' + height + '^';
      }
    }
  }
  // ... more hands
};

Some (goofy) Screenshots

Here is the main app:

Main app The main app

And of course the hands are customizable!

default hand overlay
"thumb" hand style
"reach" hand style

Overall, not incredibly useful as a hand-overlaying app, but you may need to have an image watermarking service in your project and this could come in “handy.”