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
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:
Before exploring the script, let’s visit some considerations when working with image processing.
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.
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.
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:
//development URL
http://localhost:3010/exported-images
//production URL
https://<your_domain>:3010/exported-images
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 URL
http://localhost:3010/exported-images
//production URL
https://<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.
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)
Let’s visit how our Jimp script looks in its full implementation before breaking it down:
var Jimp = require('jimp');
//if you are following along, create the following 2 images relative to this script:
let imgRaw = 'raw/image1.png'; //a 1024px x 1024px backgroound image
let imgLogo = 'raw/logo.png'; //a 155px x 72px logo
//---
let imgActive = 'active/image.jpg';
let imgExported = 'export/image1.jpg';
let textData = {
text: '© JKRB Investments Limited', //the text to be rendered on the image
maxWidth: 1004, //image width - 10px margin left - 10px margin right
maxHeight: 72+20, //logo height + margin
placementX: 10, // 10px in on the x axis
placementY: 1024-(72+20)-10 //bottom of the image: height - maxHeight - margin
};
//read template & clone raw image
Jimp.read(imgRaw)
.then(tpl => (tpl.clone().write(imgActive)))
//read cloned (active) image
.then(() => (Jimp.read(imgActive)))
//combine logo into image
.then(tpl => (
Jimp.read(imgLogo).then(logoTpl => {
logoTpl.opacity(0.2);
return tpl.composite(logoTpl, 512-75, 512, [Jimp.BLEND_DESTINATION_OVER, 0.2, 0.2]);
});
)
//load font
.then(tpl => (
Jimp.loadFont(Jimp.FONT_SANS_32_WHITE).then(font => ([tpl, font]))
))
//add footer text
.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);
})
//export image
.then(tpl => (tpl.quality(100).write(imgExported)))
//log exported filename
.then(tpl => {
console.log('exported file: ' + imgExported);
})
//catch errors
.catch(err => {
console.error(err);
});
jimp-example.js
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.
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.
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.
.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.
.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.
.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.
.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:
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.
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.
The project on Github can be found at https://github.com/oliver-moran/jimp.
#nodejs #javascript #Jimp