How to Auto Generate Images with Node.JS and Canvas
📣 Sponsor
Every time I post an article, I create a thumbnail to go along with it. Often this part is the most tedious. I usually do it in Photoshop or another image editor. To try and make this easier, I've recently automated the generation of post thumbnails of this image with Javascript and Node.JS. In this tutorial we'll be looking at how you can generate your own article images automatically, using Node.JS and Canvas.
In this guide, I'll be showing you how to auto generate post thumbnails with Node.JS. Here is an example of an image I generated using this method:
The full code for this article can be found in this Git Gist
How to use Canvas in Node.JS
Since Node.JS is a backend language, it doesn't have canvas right out of the box. We have to use a component called canvas
, and import it into our Node.JS. This can be installed with the line npm i canvas
, and imported into any Node.JS file.
How to use Emojis with Node.JS Canvas
You can do most of what I'm going to do here with the default canvas
module - but for the images I generate, I also wanted to use emojis. As such, I'm using a fork of that package, called @napi-rs/canvas
, which supports Emojis. The version I am using is 0.1.14
, so if you start running into issues replicating this guide, try installing it with the command npm i @napi-rs/canvas@0.1.14
Now that we've covered the basics, let's get started. First off, let's import all of our packages. I am importing a few things here:
- canvas - this is how we will create our image.
- fs - this is how we will write our image to our server and save it.
- cwebp - this is how we'll save our image as a webp file - so it's optimised for web.
- fonts - I'm importing 3 fonts - two are versions Inter, which is a great font, and the last is the Apple Emoji font. You can find Inter here, and the Apple Emoji Font here.
import canvas from '@napi-rs/canvas' // For canvas.
import fs from 'fs' // For creating files for our images.
import cwebp from 'cwebp' // For converting our images to webp.
// Load in the fonts we need
GlobalFonts.registerFromPath('./fonts/Inter-ExtraBold.ttf', 'InterBold');
GlobalFonts.registerFromPath('./fonts/Inter-Medium.ttf','InterMedium');
GlobalFonts.registerFromPath('./fonts/Apple-Emoji.ttf', 'AppleEmoji');
How to auto generate post thumbnails with Javascript
Next up we need to write a utility function for wrapping text. This is a pre-requisite to what we're going to do in our canvas. When we write text on an HTML canvas, it typically doesn't wrap automatically. Instead, we need to create a function which measures the width of the container, and decides whether to wrap or not. This is a useful canvas utility function in general, so it may be worth saving! The annotated function is shown below:
// This function accepts 6 arguments:
// - ctx: the context for the canvas
// - text: the text we wish to wrap
// - x: the starting x position of the text
// - y: the starting y position of the text
// - maxWidth: the maximum width, i.e., the width of the container
// - lineHeight: the height of one line (as defined by us)
const wrapText = function(ctx, text, x, y, maxWidth, lineHeight) {
// First, split the words by spaces
let words = text.split(' ');
// Then we'll make a few variables to store info about our line
let line = '';
let testLine = '';
// wordArray is what we'l' return, which will hold info on
// the line text, along with its x and y starting position
let wordArray = [];
// totalLineHeight will hold info on the line height
let totalLineHeight = 0;
// Next we iterate over each word
for(var n = 0; n < words.length; n++) {
// And test out its length
testLine += `${words[n]} `;
var metrics = ctx.measureText(testLine);
var testWidth = metrics.width;
// If it's too long, then we start a new line
if (testWidth > maxWidth && n > 0) {
wordArray.push([line, x, y]);
y += lineHeight;
totalLineHeight += lineHeight;
line = `${words[n]} `;
testLine = `${words[n]} `;
}
else {
// Otherwise we only have one line!
line += `${words[n]} `;
}
// Whenever all the words are done, we push whatever is left
if(n === words.length - 1) {
wordArray.push([line, x, y]);
}
}
// And return the words in array, along with the total line height
// which will be (totalLines - 1) * lineHeight
return [ wordArray, totalLineHeight ];
}
Now that we have our utility function complete, we can write our generateMainImage
function. This will take all the info we give it, and produce an image for your article or site.
For context, on Fjolt, I give each category in the database two colors - which let's me generate a gradient background for each image per category. In this function, you can pass whatever colors you want in and achieve the same effect - or you can change the function entirely! The choice is yours.
// This functiona accepts 5 arguments:
// canonicalName: this is the name we'll use to save our image
// gradientColors: an array of two colors, i.e. [ '#ffffff', '#000000' ], used for our gradient
// articleName: the title of the article or site you want to appear in the image
// articleCategory: the category which that article sits in - or the subtext of the article
// emoji: the emoji you want to appear in the image.
const generateMainImage = async function(canonicalName, gradientColors, articleName, articleCategory, emoji) {
articleCategory = articleCategory.toUpperCase();
// gradientColors is an array [ c1, c2 ]
if(typeof gradientColors === "undefined") {
gradientColors = [ "#8005fc", "#073bae"]; // Backup values
}
// Create canvas
const canvas = createCanvas(1342, 853);
const ctx = canvas.getContext('2d')
// Add gradient - we use createLinearGradient to do this
let grd = ctx.createLinearGradient(0, 853, 1352, 0);
grd.addColorStop(0, gradientColors[0]);
grd.addColorStop(1, gradientColors[1]);
ctx.fillStyle = grd;
// Fill our gradient
ctx.fillRect(0, 0, 1342, 853);
// Write our Emoji onto the canvas
ctx.fillStyle = 'white';
ctx.font = '95px AppleEmoji';
ctx.fillText(emoji, 85, 700);
// Add our title text
ctx.font = '95px InterBold';
ctx.fillStyle = 'white';
let wrappedText = wrapText(ctx, articleName, 85, 753, 1200, 100);
wrappedText[0].forEach(function(item) {
// We will fill our text which is item[0] of our array, at coordinates [x, y]
// x will be item[1] of our array
// y will be item[2] of our array, minus the line height (wrappedText[1]), minus the height of the emoji (200px)
ctx.fillText(item[0], item[1], item[2] - wrappedText[1] - 200); // 200 is height of an emoji
})
// Add our category text to the canvas
ctx.font = '50px InterMedium';
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.fillText(articleCategory, 85, 553 - wrappedText[1] - 100); // 853 - 200 for emoji, -100 for line height of 1
if(fs.existsSync(`./views/images/intro-images/${canonicalName}.png`))) {
return 'Images Exist! We did not create any'
}
else {
// Set canvas as to png
try {
const canvasData = await canvas.encode('png');
// Save file
fs.writeFileSync(`./views/images/intro-images/${canonicalName}.png`), canvasData);
}
catch(e) {
console.log(e);
return 'Could not create png image this time.'
}
try {
const encoder = new cwebp.CWebp(path.join(__dirname, '../', `/views/images/intro-images/${canonicalName}.png`));
encoder.quality(30);
await encoder.write(`./views/images/intro-images/${canonicalName}.webp`, function(err) {
if(err) console.log(err);
});
}
catch(e) {
console.log(e);
return 'Could not create webp image this time.'
}
return 'Images have been successfully created!';
}
}
Generating Article Image with Node.JS in detail
Let's look at this function in detail, so we can fully understand what's going on. We start by prepping our data - making our category uppercase, and setting a default gradient. Then we create our canvas, and use getContext
to initiate a space where we can draw on.
articleCategory = articleCategory.toUpperCase();
// gradientColors is an array [ c1, c2 ]
if(typeof gradientColors === "undefined") {
gradientColors = [ "#8005fc", "#073bae"]; // Backup values
}
// Create canvas
const canvas = createCanvas(1342, 853);
const ctx = canvas.getContext('2d')
Then we draw our gradient:
// Add gradient - we use createLinearGradient to do this
let grd = ctx.createLinearGradient(0, 853, 1352, 0);
grd.addColorStop(0, gradientColors[0]);
grd.addColorStop(1, gradientColors[1]);
ctx.fillStyle = grd;
// Fill our gradient
ctx.fillRect(0, 0, 1342, 853);
And write our emoji text onto the image.
// Write our Emoji onto the canvas
ctx.fillStyle = 'white';
ctx.font = '95px AppleEmoji';
ctx.fillText(emoji, 85, 700);
Now we get to use our wrapping function, wrapText
. We'll pass in our quite long articleName
, and start it near the bottom of our image at 85, 753
. Since wrapText
returns an array, we'll then iterate through that array to figure out the coordinates of each line, and paint them onto the canvas:
After that, we can add on our category, which should be above both the emoji and title text - both of which we now have calculated.
// Add our title text
ctx.font = '95px InterBold';
ctx.fillStyle = 'white';
let wrappedText = wrapText(ctx, articleName, 85, 753, 1200, 100);
wrappedText[0].forEach(function(item) {
// We will fill our text which is item[0] of our array, at coordinates [x, y]
// x will be item[1] of our array
// y will be item[2] of our array, minus the line height (wrappedText[1]), minus the height of the emoji (200px)
ctx.fillText(item[0], item[1], item[2] - wrappedText[1] - 200); // 200 is height of an emoji
})
// Add our category text to the canvas
ctx.font = '50px InterMedium';
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.fillText(articleCategory, 85, 553 - wrappedText[1] - 100); // 853 - 200 for emoji, -100 for line height of 1
How to save Canvas Images to Server with Node.JS
Alright, now we've created our image, let's save it to our server:
- First of all, we'll check if the file exists. If it does, we'll return that the image exists and do nothing else.
- If the file doesn't exist, we'll try to create a png version of it, using
canvas.encode
, and then usefs.writeFileSync
to save it. - If all goes well, we'll then use
cwebp
to save an alternative,.webp
version of the file, which should be much smaller than the .png version.
if(fs.existsSync(`./views/images/intro-images/${canonicalName}.png`))) {
return 'Images Exist! We did not create any'
}
else {
// Set canvas as to png
try {
const canvasData = await canvas.encode('png');
// Save file
fs.writeFileSync(`./views/images/intro-images/${canonicalName}.png`), canvasData);
}
catch(e) {
console.log(e);
return 'Could not create png image this time.'
}
try {
const encoder = new cwebp.CWebp(path.join(__dirname, '../', `/views/images/intro-images/${canonicalName}.png`));
encoder.quality(30);
await encoder.write(`./views/images/intro-images/${canonicalName}.webp`, function(err) {
if(err) console.log(err);
});
}
catch(e) {
console.log(e);
return 'Could not create webp image this time.'
}
return 'Images have been successfully created!';
}
ow we have a function which will auto generate images for us. As you might expect, to run this file, you need to use:
node index.js
I run this every time I write a new article - so when the article is saved to the database, an image is also produced for it. Here is another example of an image generated this way:
How to add Node.JS Images to Your Site
Now your image should be saved to your server. If you have it in a location that is accessible via URL, you can add these images as the "featured images" on posts and web pages. To add these images to your posts as post thumbnails so they show up in social media, you simply need to add the following two meta tags to the head of your page. If you're interested in the full list of HTML and SEO meta tags, you can find out guide on that here.
<meta property="og:image" content="">
<meta name="twitter:image" content="">
Conclusion
Thanks for reading. In this guide we've covered how to use Node.JS to create post thumbnails. We've also covered how to use emojis in your Node.JS canva. Here are some useful links for you:
- The final code can be found in this Git Gist
- Our Complete Javascript Guide
- More Javascript Content
More Tips and Tricks for Javascript
- How to get the last element of an Array in Javascript
- Javascript Array Every Method
- Javascript Promise.all() - Everything you need to know
- Javascript Array Concat Method
- What is Nullish Coalescing (or ??) in Javascript
- Setting the Default Node.JS version with nvm
- The Complete Guide to JavaScript Set Type
- Javascript Shallow Copies - what is a Shallow Copy?
- Art Generator with Javascript and WebGL
- Javascript Dates and How they Work