Processing Uploaded Images with Node and JIMP

I have used JIMP - the JavaScript Image Manipulation Program - in a couple of previous posts and here I will use it for what may be its most common use case, processing images uploaded to a Node-based website.

I have been working on a Node site for my photographs for an embarrassingly long time, adding small bits of functionality every once in a while. It is approaching a stage where I can launch it with at least basic functionality and this post is a condensed version of the code I wrote to process images after they have been uploaded.

The processing involves three tasks:

  • Create a thumbnail
  • Create a standard sized image
  • Add a watermark

The source code can be downloaded as a ZIP or cloned from the Github repository from the links below.

Source Code Links

ZIP File
GitHub

The full documentation for JIMP is https://www.npmjs.com/package/jimp.

This is the source code in its entirety.

processuploadedimage.js

processImage({ imagepath: "mall.jpg",            // the original image
               thumbpath: "mall_thumbnail.jpg",  // where to save the thumbnail
               thumbmax: 128,                    // maximum thumbnail dimension
               saveimagepath: "mall_600.jpg",    // where to save the processed image
               imagemax: 600,                    // maximum image dimension
               watermarkpath: "watermark.png"}); // watermark image


console.log("starting...");


function processImage(options)
{
    const Jimp = require("jimp");

    Jimp.read(options.imagepath)
        .then(async image =>
        {
            console.log("image opened");

            // thumbnail
            const thumbnail = image.clone();
            thumbnail.scaleToFit(options.thumbmax, options.thumbmax);
            thumbnail.writeAsync(options.thumbpath)
                     .then(() => console.log("thumbnail saved"))
                     .catch(err => { console.error(err); });

            // main image
            image.scaleToFit(options.imagemax, options.imagemax);
            await addWatermark(image, options.watermarkpath);
            image.quality(95);
            image.writeAsync(options.saveimagepath)
                 .then(() => console.log("image saved"))
                 .catch(err => { console.error(err); });
        })
        .catch(err =>
        {
            console.error(err);
        });
}


function addWatermark(image, watermarkpath)
{
    const Jimp = require("jimp");

    return Jimp.read(watermarkpath)
               .then(watermark =>
               {
                    console.log("watermark opened");
                    const x = image.bitmap.width - 32 - watermark.bitmap.width;
                    const y = image.bitmap.height - 32 - watermark.bitmap.height;
                    image.composite(watermark, x, y, { opacitySource: 0.5 });
                })
                .catch(err =>
                {
                    console.error(err);
                });
}

The philosophy is to create a single function which carries out all three tasks. It therefore has a single argument - an object containing all the information needed to do so. The file starts with a call to this function, although a real-life implementation would call the function externally; in the case of my nascent site this is done from within a REST service.

After the function call is a log to the console. This will appear first and is just to demonstrate that the process is asynchronous.

Within the processImage function we first need to require JIMP, and then call its read method using Promise then/catch syntax. Most of what happens next is straightforward and self-explanatory but there are a few features worth pointing out:

  1. The Lambda passed to then is async (see 4)

  2. We create a clone of the image for the thumbnail

  3. JIMP provides several resizing functions but scaleToFit is probably the most useful as we just give it maximum dimensions and it does the necessary calculations for us.

  4. We await addWatermark otherwise we would rush ahead and save the image before the watermark is added

  5. The quality is set to 95. It can be any value from 0 to 100 but low values are bad (REALLY bad) and it is generally accepted that values over 95 increase filesize significantly without much noticeable improvement in apparent visual quality.

  6. As this code was written for my own site I have assumed I will never upload images smaller than my designated standard size. If you were to let the general public loose on a site you could either leave smaller images as they are or enforce a minimum size.

  7. I have used files both as a source and destination for demonstration purposes. You can also use buffers which you would almost certainly do in practice for the uploaded image, and also for the processed images if you wish to store them in a database rather than the file system.

  8. In a production system you might wish to pass an "onerror" callback to processImage rather than just outputting the errors to the console.

The addWatermark function reads the watermark file and then calculates the position of the watermark; I have hard-coded this to be 32 pixels up and left of the bottom right corner. The composite function then combines the two images which in this case effectively pastes the watermark image onto the main image. I have used the optional opacitySource argument to make the watermark semi transparent.

Now let's run the code with this command. (I have hard-coded the name of the image file, mall.jpg, which is supplied with the ZIP and repository. You might like to use your own photo instead.)

Run

node processuploadedimage.js

This is the output.

Output

starting...
image opened
thumbnail saved
watermark opened
image saved

The output is pretty boring but at least it shows what is going on, in particular demonstrating that the processing is asynchronous by printing "starting..." first.

This is the original 1000 x 667 image.

And this is the resized 600 x 400 image complete with watermark.

And finally the thumbnail.

Looking for a VPN?

StrongVPN is running a promotion where customers can get a permanent discount on the monthly plan for just $5 for life! Ends June 30, 2020.