Javascript

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:

Example of an auto generated article image using Node.JS and Canvas

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:

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 use fs.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:

Example of an auto generated article image using Node.JS and Canvas

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:

Last Updated Monday, 21 February 2022
Johnny Simpson
Johnny Simpson

More Tips and Tricks Javascript

Subscribe for Weekly Dev Tips

Subscribe to our weekly newsletter, to stay up to date with our latest web development and software engineering posts via email. You can opt out at any time.

Not a valid email