Image uploading and processing with Node.js and GraphicsMagick

In this tutorial I will go over how to upload images from a form with Node.js, manipulate them with GraphicsMagick, and then return the new file to the browser. In a recent project of mine I needed to create square thumbnail images, so we'll be using GrahicsMagick to do that.

Note - I've created a related post on uploading images with React.js and Node.js to AWS S3.

Step 1. Setup a node app

First we need a Node app with Express.js. If you are new to node, please refer to this past tutorial of mine where I explain how to do so.

Step 2. Install Dependencies

We're going to use a couple npm modules:

Go ahead and npm install the packages

$ npm install gm
$ npm install multer

Step 3. Setup our index view

Next let's setup the HTML form we will use to upload our image and add variables to display our new image. In your views folder open your index.ejs file and add the following:

<% include partials/header %>

<h1>Graphic Machine Maker</h1>
<form action="/image" enctype="multipart/form-data" method="post">
  <input id="image" name="image" type="file"><br>
  <input type="submit" value="Upload">
</form>

<p>Status: <%= status %></p>
<img src="<%= newImage %>">

<% include partials/footer %>

Step 4. Add Multer

Next let's add Multer which is node middleware and will help us handle multipart/form-data. First, require it in your app.js file:

var multer = require('multer');

Next, place the second line below after your bodyParser middleware.

UPDATE - As all things, nothing lasts forever. This is becoming a dated post so you may come across a typeerror when implementing the multer middleware below, depending on versions, if so, please follow this lnk: http://stackoverflow.com/questions/31656178/typeerror-app-use-requires-middleware-function

app.use(bodyParser.urlencoded({ extended: false }));
app.use(multer({ dest: "./images/uploads/" })); 

We will store our files in a directory called "uploads". Go ahead and add that folder in the root directory of your app. We're also going to need a place to hold our images after we manipulate them and serve them back to the browser, so let's also create a folder named "finished" and place it in "./public/images/finished/. Our file structure should now look like:

app
 - bin
 - images
   - uploads
 - node_modules
 - public
   - images
     - finished
 - routes
 - views
 app.js
 package.json

Step 5. Setup our routes

Next let's prepare our image uploading route. In your app.js add the following:

    var image = require('./routes/image');
    app.use('/image', image);

Next, in the "./routes" directory add the new image.js file and then add the following:

'use strict';

var express = require('express');
var router = express.Router();
var MAU = require('./modify-and-upload');

router.post('/', function(req, res) {
  var mau = new MAU(req.files.image, function(err, newImagePath){
    if(err){ res.render('index', { 
      status: 'Error uploading' }); 
    }
    res.render('index', {
      status: 'Finished uploading',
      newImage: newImagePath
    });
  });
});

module.exports = router;

Let's talk through a bit of this code. First we're going to require a file we haven't created yet. This file is where we will upload and process the image.

var MAU = require('./modify-and-upload');

Next we have our post route where we will be posting our image to

router.post('/', function(req, res) {

Finally we are going to create a new instance of our uploading and processing module, pass it the file that was uploaded ( res.files.image), and then pass it a callback where we will have access to our new image path.

var mau = new MAU(req.files.image, function(err, newImagePath){
  if(err){ res.render('index', { 
    status: 'Error uploading' }); 
  }
  res.render('index', {
    status: 'Finished uploading',
      newImage: newImagePath
     });
});

Before moving on, we need to make one update in our index.js route. Open it up and let's add a status variable that says we are ready to upload, and include a placeholder image.

router.get('/', function(req, res) {
  res.render('index', { 
    status: 'Ready to upload',
    newImage: 'http://placehold.it/175x175'
  });
});

Step 6. Build the upload and processing module

Let's create our "modify-and-upload.js" file and save it in our routes directory for convenience, but you can place it wherever you wish.

Open this file and add the following code:

  'use strict';

  var gm = require('gm').subClass({ imageMagick: true });

  function ModifyAndUpload(img, callback){
    this.img = img;
    this.callback = callback;
    this.init();
  }

  ModifyAndUpload.prototype = {

    modifyImage: function(){
      var that = this;
      gm(that.originalImagePath)
        .resize(175, 175 + '^')
        .gravity('center')
        .extent(175, 175)
        .write(that.newThumbPath, function (err){
          if (that.callback && typeof(that.callback) === 'function'){
            that.callback(err, that.publicPathOfThumb);
          }
        }); 
    },

    init: function(){
      var saveFolder = process.cwd() + '/public/images/finished/';
      this.originalImagePath = process.cwd() + '/' + this.img.path;
      this.newThumbPath = saveFolder + 'thumb--' + this.img.originalname;
      this.publicPathOfThumb = '/images/finished/thumb--' + this.img.originalname;    
      this.modifyImage();
    }

  };

  module.exports = ModifyAndUpload;

Let's walk through this module.

We start by requiring the GraphicsMagick module.

var gm = require('gm').subClass({ imageMagick: true });

Next we have a constructor where we will pass in the uploaded image, and a callback, and then call the init method.

function ModifyAndUpload(img, callback){
  this.img = img;
  this.callback = callback;
  this.init();
}

In our init method we start by setting up some file paths. Our "saveFolder" variable defines the final path where our images will be accessible. Next we store the original image path that will be passed into our GraphicsMagick method. Next, we create the "newThumbPath" which contains the location for our new image that will be created by GraphicsMagick. Following this, we define our "publicPathOfThumb" which will contain the publicly accessible path of our new file that our view will need in order to load it in the browser. Last, we begin the process of modifying the image by called the "modifyImage" method.

init: function(){
  var saveFolder = process.cwd() + '/public/images/finished/';
  this.originalImagePath = process.cwd() + '/' + this.img.path;
  this.newThumbPath = saveFolder + 'thumb--' + this.img.originalname;
  this.publicPathOfThumb = '/images/finished/thumb--' + this.img.originalname;    
  this.modifyImage();
}

Next, let's take a look at the modifyImage method. The key here is our gm() method. We first pass in the originalImagePath which contains the path to the uploaded image from our form. We then call a series of methods (resize, gravity, extent) that will give us our perfect thumbnail. For a detailed explanation of how this all works check this great post by Jeff Wilcox. Next, we call the write method which will save our modified image in our new thumbnail path. Last, we call our callback function and pass it any errors and our new public path for our image.

modifyImage: function(){
  var that = this;
  gm(that.originalImagePath)
    .resize(175, 175 + '^')
    .gravity('center')
    .extent(175, 175)
    .write(that.newThumbPath, function (err){
      if (that.callback && typeof(that.callback) === 'function'){
        that.callback(err, that.publicPathOfThumb);
      }
    }); 
}

That's it for our "modify-and-upload" file. Let's head back to the image.js route file and take another look at our Post method. When our callback is triggered, if we have any errors we send a message in our status variable saying so, otherwise we send a status that we are finished uploading, and a variable that contains our new image path that is then loaded by our view.

var mau = new MAU(req.files.image, function(err, newImagePath){
  if(err){ res.render('index', { 
    status: 'Error uploading' }); 
  }
  res.render('index', {
    status: 'Finished uploading',
    newImage: newImagePath
  });
});

That's it, I hope this helps anyone who was looking to get their feet wet with node.js image uploading and processing with GraphicsMagick.

-Ben

Questions, comments, improvements?