DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Deno vs. Node.js: The Showdown Nobody Asked For But Everyone Needed
  • Building a Tic-Tac-Toe Game Using React
  • Buh-Bye, Webpack and Node.js; Hello, Rails and Import Maps
  • Bridging JavaScript and Java Packages: An Introduction to Npm2Mvn

Trending

  • A Developer's Guide to Mastering Agentic AI: From Theory to Practice
  • Measuring the Impact of AI on Software Engineering Productivity
  • Mastering Advanced Traffic Management in Multi-Cloud Kubernetes: Scaling With Multiple Istio Ingress Gateways
  • AI's Dilemma: When to Retrain and When to Unlearn?
  1. DZone
  2. Coding
  3. JavaScript
  4. Auto Generating Post Thumbnails With Node.JS

Auto Generating Post Thumbnails With Node.JS

Let's look at how to auto generate post thumbnails with Node.JS and JavaScript.

By 
Johnny Simpson user avatar
Johnny Simpson
DZone Core CORE ·
Feb. 22, 22 · Tutorial
Likes (3)
Comment
Save
Tweet
Share
6.9K Views

Join the DZone community and get the full member experience.

Join For Free

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. The final code can be found in this Git Gist.

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 optimized for the 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.

Don't forget to install dependencies, using npm i @napi-rs/canvas, and npm i cwebp!

JavaScript
 
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');

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 that 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:

JavaScript
 
// 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, I give each category in the database two colors - which lets 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.

JavaScript
 
// This function 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 it.

JavaScript
 
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:

JavaScript
 
// 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.

JavaScript
 
// 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 wrapTextreturns 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.

JavaScript
 
// 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
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 a version of the file, which should be much smaller than the .png version.

JavaScript
 
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!';
}

Now we have a function that will auto-generate images for us. As you might expect, if you need to run this function where you want to auto-generate the image. If you had this saved and running in a file called index.js, we could run it in Node.js with the following command:

PowerShell
 
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.

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 canvas. Here are some useful links for you:

  • The final code can be found in this Git Gist
  • Our Complete Javascript Guide
  • More Javascript Content
Node.js POST (HTTP) JavaScript

Published at DZone with permission of Johnny Simpson, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Deno vs. Node.js: The Showdown Nobody Asked For But Everyone Needed
  • Building a Tic-Tac-Toe Game Using React
  • Buh-Bye, Webpack and Node.js; Hello, Rails and Import Maps
  • Bridging JavaScript and Java Packages: An Introduction to Npm2Mvn

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!