Resizing images to square, aspect-ratio friendly bits with Node, Express, and ImageMagick
October 5, 2011
I’ve been playing around with Node.js a little more recently and wanted to share a little short lesson I had. A quick little break from the phone client…
As part of the cloud environment for my application, I have a service that dynamically resizes a given image from Foursquare into a Windows Phone tile size (173x173 pixels). This is used for the “pin to start” feature of 4th and Mayor.
The initial implementation of this service was done in PHP.
However, the new version of my cloud infrastructure is powered by Node.js for the most part, and I ran into some fun problems trying to get ImageMagick to properly resize images to be aspect-aware. I am using Express for servicing notifications and other endpoints.
How the resized image is used
Here’s an image from a Foursquare user of Microsoft Building 40, where my office is right now. I like to keep it pinned to my start screen, and the source image from fsq is actually much larger and not properly sized or cropped for tile display.
When the pinned tile is created, the remote URI points to the 4thandmayor.com cloud presence, and then server-side I resize the Foursquare image provided in the query string. (I also validate that it’s actually an image from there, etc.)
Resizing images using node-imagemagick
I’m wanting to use the node-imagemagick module with Node to do the resizing (similar to what PHP was doing with ImageMagick before). It really just shells out to the ImageMagick ‘convert’ program.
There is a built-in “resize” function, however I don’t exactly want to use that: here’s what happens when I resize the image the standard way. I just assumed that it would figure out a good size and crop. It’s been a while since I used this stuff in college!
var im = require("imagemagick"); im.resize({ srcData : img, strip : false, width : 173, height : 173 }, function(err, stdout, stderr) { if (err){ redirectToStandardTile(res); } else { res.contentType("image/jpeg"); res.end(stdout, 'binary'); } });
The aspect ratio is maintained.
Trying to crop the image instead
There’s a separate crop function as well, but it works only on files, no streams. I’d like to not use temporary files if I can and instead just pipe around the raw image bytes. There is also a thumbnail command built into ImageMagick, but I don’t want to reinvent the wheel and not be able to use the helpful node library created by Rasmus.
Trying the width x height ! option
You can also force to be a specific size, such as this, by appending an exclamation point to the height (as a string literal). Not good for me (the photo is stretched):
var im = require("imagemagick"); im.resize({ srcData : img, strip : false, width : 173, height : "173!" // force the sizing. }, function(err, stdout, stderr) { if (err){ redirectToStandardTile(res); } else { res.contentType("image/jpeg"); res.end(stdout, 'binary'); } });
Using resize plus extents to create a square 173x173 thumbnail
Instead, I decided to append custom arguments for ImageMagick’s convert program, and finally came up with a combination of extent values, etc., that work for my needs. Here’s what finally worked for me:
- Using the hat appended to the height value (173^ in a string literal instead of 173)
- Including the gravity argument
- Setting the extent to be the square 173x173 area of interest
// Resize a photo for use in a live tile or secondary place tile. app.get('/tile.php', function (req, res) { var original = req.param("i"); if (original) { getImage(original, function (err, img) { if (!err && img) { var im = require("imagemagick"); im.resize({ srcData : img, strip : false, width : 173, height : "173^", customArgs: [ "-gravity", "center" ,"-extent", "173x173" ] }, function(err, stdout, stderr) { if (err){ redirectToStandardTile(res); } else { res.contentType("image/jpeg"); res.end(stdout, 'binary'); } }); } else { redirectToStandardTile(res); } }); } }); function redirectToStandardTile(res) { res.redirect("http://www.4thandmayor.com/app/genericTile.png"); } function getImage(uri, callback) { // (err, res) request( { encoding: 'binary', uri: uri, }, function (error, response, body) { if (!error && response.statusCode == 200) { callback(null, body); } else { callback(error, null); } }); }
This implementation includes the Express code, in case you were wondering about doing that part. I’m attaching to the same former PHP URI for this functionality so that I don’t have to update paths and can continue to serve older versions of the app as appropriate.
Hope this helps.