Image Processing with Node.js

Image processing is very important if you are going to allow anyone, even if it's only you, to upload images to your site. The fact of the matter is, not everyone knows how to resize an image, and forcing users to do it will mean less user submitted content on your site, since it's a pain, and other sites let you upload non-processed images. If it's just you uploading images, laziness will take over and you will stop doing it because it's not easy. So get with the times!

I found a Node.js plugin for GraphicsMagick. I learned about both the plugin and GraphicsMagick itself simultaneously. GraphicsMagick is pretty sweet, minus setting it up. After you get it set up though, you can perform many operations on any image format that you configured within GraphicsMagick.

This post will not cover setting up GraphicsMagick (although I will point out that setting LDFLAGS=-L/usr/local/include in my instance saved me from problems with missing LIBPNGxxx.so files), and I couldn't get it to work on my Mac. Here's how I'm using GraphicsMagick, via the excellent Node.js module, gm.

var gm = require("gm"), fs = require("fs");

var basePath = "/path/to/images/";
var maxDimension = 800;
var maxThumbDimension = 170;
var thumbQuality = 90;

var processImages = function(images){
console.log("processing : " + images.length + " image(s)");
images.forEach(function (image, imageIndex){
var fullPath = basePath + image;
var newFilename = basePath + "scaled/" + image;
gm(fullPath).size(function(err, value){
var newWidth = value.width, newHeight = value.height, ratio = 1;
if (value.width > maxDimension || value.height > maxDimension){
if (value.width > maxDimension){
ratio = maxDimension / value.width;
}
else if (value.height > maxDimension){
ratio = maxDimension / value.height;
}
newWidth = value.width * ratio;
newHeight = value.height * ratio;
}

if (newWidth != value.width){
console.log("resizing " + image + " to " + newWidth + "x" + newHeight);
gm(fullPath).resize(newWidth, newHeight).write(newFilename, function(err){
if (err) console.log("Error: " + err);
console.log("resized " + image + " to " + newWidth + "x" + newHeight);
});
}
else copyTheFileToScaledFolder(); // ?? how do you do this?!? :P
}
}
}


I run this as a service instead of putting it in the web application. It is on an interval, and you just let node.js handle it! That part was simple:

var interval = setInterval(function(){ processImages(getImages());}, 4000);

Your getImages function might look like this:

var getImages = function(){
fs.readdir(basePath, function(err, files){
// this won't work...
// filter out folders, non-image files and files that have already been processed
processImages(files);
// maybe delete these images so you don't have to keep track of previously processed images
});
}


This is not how my code works, since my images are in a MongoDB database and my document has a "resizeImages" boolean property on it, to trigger this to get images to resize. So I don't know if it will work, or what the fs.readdir sends in its files argument on the callback! But you can try :)

With GraphicsMagick, you could also change the format of the image, if you were a stickler and wanted only PNG or JPG files. You can apply filters like motion blur, or transforms like rotation, add text, etc. It is pretty magical...

File System Operations

Somewhat related, how to you simply copy a file in Node.js? I found a method that uses the util.pump method but it didn't work for me. Also, deleting a file in node.js is "unlink", since it will work on symlinks and files. This one did work but I found that I was deleting them too soon, attributed to the non-blocking nature of Node.js, and had to take it out.

git: Because I alway forget

And it takes me 20 minutes to figure it out again. This is how you specify a url to a remote repository to a linux box under a user (git's) home directory, using ssh...

git remote add [name] ssh://git@ipaddress/~git/reponame

I usually name them reponame.git.

That should turn the process into a one minute one in the future :)