Resizing images using Node.js image processing tools, without external dependencies or image servers

; Date: September 18, 2019

Tags: Node.JS

While getting ready to post a set of images on another blog, I wondered how to most effectively resize the images. They were high resolution product images that came with a press release, and to be nice to my readers it's important to shrink the image size to promote faster downloads. What if I want to supply several image sizes based on the device used by the visitor? It's not enough to resize the image on my own, it would be better to generate several image sizes. Which led to exploring Node.js tools for resizing images, and finding them lacking.

In my case I use a static website generator - AkashaCMS. The model is that I have a directory full of Markdown files and images, another directory with page layout templates, and another directory full of template snippets. AkashaCMS weaves all that together to render an HTML website, and then the files are uploaded to the docroot of the website for serving to the public.

My initial thought was to add a filter to AkashaCMS allowing this:

<img src="path/to/file.jpg" resize-width="700">

To indicate the image should be resized during the rendering process. Other attributes could be added describing other image manipulations. In this case the resized image would be 700px wide, and the height would be whatever is correct for the width/height ratio of the original image.

This seemed simple enough, and I had the unit tests for the filter coded pretty quickly. But finding a good tool in Node.js to handle resizing the images took a long time since I had to try several libraries. There are many image processing libraries in Node.js that make promising claims and I wanted to try them all to see which was the best for my purpose.

Without dependencies or image servers

Tools like ImageMagick and GraphicsMagick have existed for decades and are very powerful. But that is an external dependency, and the best practice is to have all dependencies explicitly declared. In a Node.js application this means listing dependencies in package.json so that npm or Yarn can install the package. With ImageMagick or GraphicsMagick you have to remember to also, in addition, run some other command that will install that other tool. That extra requirement creates a risk that your users will forget to install that thing, then complain your tool failed.

Similarly an image server is an external dependency. For a specific project the architecture may allow you to implement an image server. But why is an image server required in order to utilize an image manipulation library? In other programming platforms the image manipulation tools are part of the platform rather than requiring an external server. Why shouldn't it be the same with Node.js?

In my case, my sites are rendered by running a command-line tool and there is no rendering server of any kind running. Therefore I dismissed several packages because the description talked about an image processing service.

Sample image

For the purpose of experimentation I used this image:

Source: Wikipedia

resize-img

See: (www.npmjs.com) https://www.npmjs.com/package/resize-img

This is a fairly popular package that does one thing - resizing images. With a little rewriting of the sample code we get this script:

const fs = require('fs-extra');
const resizeImg = require('resize-img');
 
(async () => {
    let img = await fs.readFile('Human-Skeleton.jpg');
    let buf = await resizeImg(img, {
        width: 250
    });
    await fs.writeFile('Human-Skeleton-250-resize-img.jpg', buf);
})();

For this and other examples it should be straight-forward to convert the sample code here into a function to insert into a larger system.

For example, this is a start at refactoring the code for a larger system:

const fs = require('fs-extra');
const resizeImg = require('resize-img');

async function resizor(from, to, width) {
    let img = await fs.readFile(from);
    let buf = await resizeImg(img, {
        width: width
    });
    await fs.writeFile(to, buf);
}

await resizor(
        'Human-Skeleton.jpg', 
        'Human-Skeleton-250-resize-img.jpg', 
        250)

And the result:

Resized using resize-img

jimp

See: (www.npmjs.com) https://www.npmjs.com/package/jimp

This is a very popular package that has a full array of image processing functionality.

Rewriting the demo code I get this:

var Jimp = require('jimp');
 
(async () => {
    let img = await Jimp.read('Human-Skeleton.jpg');
    let resized = img.resize(250, Jimp.AUTO, Jimp.RESIZE_BEZIER);
    await resized.write('Human-Skeleton-250-jimp.jpg');
})();

In Jimp there are several algorithms for resizing images:

Jimp.RESIZE_NEAREST_NEIGHBOR;
Jimp.RESIZE_BILINEAR;
Jimp.RESIZE_BICUBIC;
Jimp.RESIZE_HERMITE;
Jimp.RESIZE_BEZIER;

The Jimp.AUTO flag specifies the resizing behavior I wanted, namely to auto-size the height based on the ratio of the original image.

Resized using Jimp

Sharp

See: (www.npmjs.com) https://www.npmjs.com/package/sharp

This is another very popular library for general image manipulation.

const sharp = require('sharp');

(async () => {
    let img = await sharp('Human-Skeleton.jpg');
    let resized = await img.resize(250);
    await resized.toFile('Human-Skeleton-250-sharp.jpg');
})();

This is straight-forward enough. But in AkashaCMS I found that Sharp throws an error if you toFile to the same file name you read the image from.

Resized using Jimp

Easyimage

See: (www.npmjs.com) https://www.npmjs.com/package/easyimage

This is a modestly popular package that is based on ImageMagick. If you've used the ImageMagick command-line tools the functions exported by this package will be familiar.

const { resize } = require('easyimage');

(async () => {
    await resize({
        src: 'Human-Skeleton.jpg',
        dst: 'Human-Skeleton-250-easyimage.jpg',
        width: 250
    });
})();

The documentation seems written to be friendly to TypeScript users. This was developed with a little bit of refactoring.

Resized using Jimp

lwip

See: (www.npmjs.com) https://www.npmjs.com/package/lwip

Does not compile for Node.js 12

Otherwise this package claims to support a very nice large set of functionality.

easy-gd

See: (www.npmjs.com) https://www.npmjs.com/package/easy-gd

Does not compile for Node.js 12

Veronica

See: (www.npmjs.com) https://www.npmjs.com/package/veronica

I rejected this out of hand because it requires you manually install external dependencies. This violates a best practice principle of having all dependencies explicitly declared.

Summary

There are now several very good choices for image manipulation in Node.js.

In my case I chose to use Sharp since there is potential for further features in the future.