Image Processing in NodeJS with Jimp
Image Processing in NodeJS with Jimp
Full code example: combining images, watermarking, fonts and text
You are requested by a client to watermark an entire library of images, as well as cropping them to a consistent resolution. In fact, your solution needs to be integrated within the client’s existing Javascript infrastructure. This can be done with a simple yet capable (relatively new) package for NodeJS, named Jimp.
Jimp stands for JavaScript Image Manipulation Program, which is simply explained on it’s project page as “The “JavaScript Image Manipulation Program” :-)” — The project page can be found here:
Jimp adoption is on a growing trajectory, currently attracting over 180,000 weekly downloads at the time of this writing.
I found that they are quick to investigate and address issues on Github. Having found an issue myself on version 0.4.0 relating to text alignment, it was addressed within 24 hours and closed within 48 hours with a commit lined up for the next version.
The open issues right now are only in the 40s, which is quite impressive for such a large library that is also relatively new. There is clearly an active community here, therefore is a reliable bet to have under your npm belt.
Install the package into your project with the following command:
npm i jimp
Before jumping into Jimp development, make sure that it supports the image types you are working with — it is a bitmap manipulation library, so do not expect any SVG or vector based support here (this also means we need to convert popular font file formats (.otf, .ttf) into bitmap font files with the .fnt file format — more on this later).
Supported file types:
@jimp/jpeg
@jimp/png
@jimp/bmp
@jimp/tiff
@jimp/gif
Working with Images in NodeJS
Jimp can be imported directly into a serverside node script. We can adopt a promise based command flow that allows us to manipulate one thing at a time — build up our edits on our image and finally save our final image with the Jimp.write() function.
Using the Jimp library is actually a great way to practice your promises as each task can be broken down into a number of then() extensions, which we will soon see being demonstrated.
The way we will break down our image editing task is by doing the following:
- Reading a template / base image to work with in a raw directory.
- Clone it into a separate active image file that we know will be manipulated.
- Read the cloned image ready to make manipulations
- Load a watermark logo and place it onto the image
- Load a font file in order to bake text into the image
- Export the final image into an export directory.
Before exploring the script, let’s visit some considerations when working with image processing.
Consideration 1: Folder structure
With these kinds of tasks we need to make sure we do not overwrite original image files. For this reason, at a minimum, we should structure our project with at least 3 folders:
project_folder/ raw/ image1.jpg image2.jpg ... active/ export/ generate_image.js
A self explanatory but necessary procedure, separating raw images, active and completed exported image — just like you would not mix raw data with normalised feature data for a neural network in machine learning. The same principles apply here.
Consideration 2: External Image Libraries
Your client may not have their raw image files on their servers ready for you to manipulate — they may be on an external service, such as Dropbox, Google Drive or Amazon AWS. Well,this is not an issue — this actually saves us the task of separating raw files from our active and exported files. For completeness, here are the developer pages for those services:
Of course, you have the option to move your exported images to these services too. If you send the image byte data as you may do for Amazon S3, then your image file would not need to be publicly accessible over HTTP. But in the case you wish to copy an image from one URL to the other, then you will want your image to be accessed via an HTTP address, which leads me onto consideration 3.
Consideration 3: Public static folder with NodeJS Express
Express makes it extremely easy for us to set folders to be public. If I wanted to store all my exported images inside a static folder, I could firstly place it inside my root directory like so:
app/ public static routes views app.js ...
And from here, edit my app.js file to create a route to this folder:
app.use('/exported-images', express.static('static'));
You do not need to adhere to the name of the folder either — I can configure any URI I choose. Let’s say my server is running on port 3010; the URL to access this folder would be:
//development URLhttp://localhost:3010/exported-images
//production URLhttps://<your_domain>:3010/exported-images
As a final consideration, you may only want your images to be sitting inside a public folder as they are being copied to an external service, and to be deleted straight after the transfer takes place. Another consideration is to use a random string as the image name for added security.
Our Watermark and Text Jimp Script
With our considerations out of the way, let’s take a raw template and attach a logo and text onto it. The final image may resemble something like this: (note the centered watermark and copyright text at the bottom)
The Script
Let’s visit how our Jimp script looks in its full implementation before breaking it down:
I like the progressive nature of this script, with the simplicity of our then()
workflow that makes the code easy to read and follow. And because of this, there probably is not too much documentation to follow on from this script — but let’s visit some areas that may be of interest to us fellow Javascript developers.
Defining variables
All script variables are defined at the top of the script so as to make it more readable and easier to update. Everything that follows our let
variables is pure functionality.
Cloning Raw and Opening Active Image
Jimp.read(imgRaw) .then(tpl => (tpl.clone().write(imgActive)))
.then(() => (Jimp.read(imgActive)))
We use Jimp.read()
to effectively “open” an image file to start manipulating it. Jimp.read()
is a Promise, which returns the image object to work with, named tpl
.
With tpl
at hand, we call tpl.clone().write()
, duplicating the raw image file we just opened and saving it in our active/ directory.
Combining Watermark Image
.then(tpl => ( Jimp.read(imgLogo).then(logoTpl => { logoTpl.opacity(0.2); return tpl.composite( logoTpl, 512-75, 512, [Jimp.BLEND_DESTINATION_OVER]); }); )
Within the following then()
block, we call Jimp.read()
once more to load our logo watermark. The opacity of the logo is changed firstly with logoTpl.opacity()
, which does not require a Promise!
Because of this we then move onto placing the logoTpl
image into our main tpl
image, with tpl.composite()
. The parameters here are quite straight forward, passing the logo itself, its x and y positions, followed by a blend mode. Here we just need the logo to be placed over the image, Jimp.BLEND_DESTINATION_OVER
.
Note: Take a look at the Jimp Basic Methods section of NPMJS to explore more about composite and the range of methods the package offers.
We return the result of tpl.composite()
to move onto our text placement.
Loading our Text Font
.then(tpl => ( Jimp.loadFont(Jimp.FONT_SANS_32_WHITE .then(font => ([tpl, font])) ))
Here we are doing 2 things — firstly loading Jimps’ built-in size 32 white Sans font, allowing us to use it in any tpl.print()
calls we make later to bake text into the image.
The second is extending this Promise and returning an array for use in the next then()
block. You see, the next then()
block needs our main tpl
image object, and our loaded font
object. Since Javascript does not support tuples as such, we can simply return an array of the required objects.
Note: Check out Jimp’s Writing Text documentation to see everything about loading bitmap fonts and printing them in your images. The font conversion tools at the end are needed to convert your fonts to bitmap .fnt files - Hiero in particular is very useful. Remember, you can only export one size - colour combination for each font, therefore it is likely you will need to load multiple fonts into your image processing scripts.
The image files themselves are commonly PNG files, where you can further mask or change the font opacity.
Printing Text on Our Template
.then(data => { tpl = data[0]; font = data[1];
return tpl.print( font, textData.placementX, textData.placementY, { text: textData.text, alignmentX: Jimp.HORIZONTAL_ALIGN_CENTER, alignmentY: Jimp.VERTICAL_ALIGN_MIDDLE }, textData.maxWidth, textData.maxHeight); })
Using our data
array, we retreive the font
and tpl
objects, and call tpl.print()
for adding text to our image.
Here the text is being added in the bottom center of the image. print
takes our font object, x and y position, followed by an object defining the text itself with its alignments. maxWidth
and maxHeight
dictate how those alignments react in the image scene.
Exporting and Post Processing
.then(tpl => (tpl.quality(100).write(imgExported)))
.then(tpl => { console.log('exported file: ' + imgExported); })
.catch(err => { console.error(err); });
With our last 2 then()
blocks, we use tpl.quality().write()
to export the image into our chosen export directory, and simply log that the process is finished. Handle any post processing here, including:
- Resolving a Promise which this process may be hosted in
- Returning the full URL of the image for an API response
- Logging / storing this image record in a database
- Delete your active file or uneeded leftovers (although in reality a supervisor script could be in charge of tidy-up operations)
- Move onto another image processing job if more are queued up.
As you are a great Javascript programmer, the catch()
clause will most likely not come in use — however let’s keep one there just in case! Handle any errors as you wish.
Where to go from here
This is just one use case with Jimp. Check out the full documentation on their NPMJS page if you are considering Jimp as your image processor of choice.