如何使用 Node.JS 和 Canvas 自动生成图像

每次我发布一篇文章时,我都会创建一个缩略图来配合它。通常这部分是最乏味的。我通常在 Photoshop 或其他图像编辑器中完成。为了尝试使这更容易,我最近使用 Javascript 和 Node.JS 自动生成了这张图片的帖子缩略图。在本教程中,我们将了解如何使用 Node.JS 和 Canvas 自动生成您自己的文章图像。

在本指南中,我将向您展示如何使用 Node.JS 自动生成帖子缩略图。这是我使用此方法生成的图像示例:

本文的完整代码可以在这个Git Gist中找到

如何在 Node.JS 中使用画布#

由于 Node.JS 是一种后端语言,因此它没有开箱即用的画布。我们必须使用一个名为 的组件canvas,并将其导入到我们的 Node.JS 中。这可以通过行安装npm i canvas,并导入到任何 Node.JS 文件中。

如何在 Node.JS Canvas 中使用表情符号#

您可以使用默认模块完成我在这里要做的大部分工作canvas——但对于我生成的图像,我还想使用表情符号。因此,我正在使用该包的一个分支,称为@napi-rs/canvas,它支持表情符号。我使用的版本是0.1.14,所以如果您在复制本指南时遇到问题,请尝试使用以下命令安装它npm i @napi-rs/canvas@0.1.14

现在我们已经介绍了基础知识,让我们开始吧。首先,让我们导入所有的包。我在这里导入一些东西:

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

如何使用 Javascript 自动生成帖子缩略图#

接下来我们需要编写一个实用函数来包装文本。这是我们要在画布上做的事情的先决条件。当我们在 HTML 画布上书写文本时,它通常不会自动换行。相反,我们需要创建一个函数来测量容器的宽度,并决定是否包装。一般来说,这是一个有用的画布实用程序功能,因此可能值得保存!注释函数如下所示

// 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 ];
}

现在我们的效用函数已经完成,我们可以编写我们的generateMainImage函数了。这将获取我们提供的所有信息,并为您的文章或网站生成图像。

对于上下文,在code8cn上,我为数据库中的每个类别提供了两种颜色——这让我为每个类别的每个图像生成一个渐变背景。在此功能中,您可以传递任何您想要的颜色并实现相同的效果 – 或者您可以完全更改功能!这是你的选择。

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

Node.JS生成文章图片详解#

让我们详细看看这个函数,这样我们就可以完全理解发生了什么。我们首先准备我们的数据 – 将我们的类别设为大写,并设置默认渐变。然后我们创建我们的画布,并getContext用来启动一个我们可以画画的空间。

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

现在我们开始使用包装函数wrapText. 我们将传递相当长的articleName,并在靠近图像底部的 处开始它85, 753。由于wrapText返回一个数组,我们将遍历该数组以找出每条线的坐标,并将它们绘制到画布上:

在那之后,我们可以添加我们的类别,它应该在表情符号和标题文本之上——我们现在已经计算了这两者。

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

如何使用 Node.JS 将画布图像保存到服务器#

好的,现在我们已经创建了我们的图像,让我们将它保存到我们的服务器:

  • 首先,我们将检查文件是否存在。如果是,我们将返回图像存在并且不做任何其他事情。
  • 如果该文件不存在,我们将尝试创建它的 png 版本,使用canvas.encode,然后使用fs.writeFileSync保存它。
  • 如果一切顺利,我们将使用文件cwebp的替代.webp版本保存,该版本应该比.png版本小得多。
    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!';
    }

现在我们有一个功能可以为我们自动生成图像。如您所料,要运行此文件,您需要使用:

node index.js

每次我写一篇新文章时我都会运行这个 – 所以当文章被保存到数据库时,也会为它生成一个图像。这是以这种方式生成的图像的另一个示例:

如何将 Node.JS 图像添加到您的站点#

现在您的图像应该保存到您的服务器。如果您将其放在可通过 URL 访问的位置,则可以将这些图像添加为帖子和网页上的“精选图像”。要将这些图像作为帖子缩略图添加到您的帖子中,以便它们显示在社交媒体中,您只需将以下两个元标记添加到页面的头部。如果您对HTML 和 SEO 元标记的完整列表感兴趣,可以在此处找到相关指南。

    <meta property="og:image" content="">
    <meta name="twitter:image" content="">

结论#

谢谢阅读。在本指南中,我们介绍了如何使用 Node.JS 创建帖子缩略图。我们还介绍了如何在 Node.JS canva 中使用表情符号。以下是一些对您有用的链接: