用Node.JS和Canvas自动生成图片的详细教程

前端 潘老师 3周前 (04-01) 25 ℃ (0) 扫码查看

开发过程中,我们常常会有自动生成图片的需求,比如为文章生成缩略图。今天就来详细讲讲如何借助Node.JS和Canvas实现这一功能。接下来,我们一步步深入学习。

一、前期准备

Node.JS本身并不具备canvas功能,所以我们需要借助外部组件来实现。这里我们选用canvas组件,在项目中运行npm i canvas命令即可完成安装。

要是还想在生成的图片中使用Emoji,普通的canvas包可能无法满足需求。此时,可以使用@napi-rs/canvas这个包的分支,我使用的版本是0.1.14。若在操作过程中遇到问题,不妨尝试通过npm i @napi-rs/canvas@0.1.14命令进行安装。

二、导入所需包

准备工作完成后,就要导入项目所需的包了:

import canvas from '@napi-rs/canvas' // 用于创建画布。
import fs from 'fs' // 用于为我们的图片创建文件。
import cwebp from 'cwebp' // 用于将图片转换为webp格式。

// 加载我们需要的字体
GlobalFonts.registerFromPath('./fonts/Inter-ExtraBold.ttf', 'InterBold');
GlobalFonts.registerFromPath('./fonts/Inter-Medium.ttf','InterMedium');
GlobalFonts.registerFromPath('./fonts/Apple-Emoji.ttf', 'AppleEmoji');

这里导入了canvas用于创建画布;fs模块负责将生成的图片写入服务器并保存;cwebp则用于将图片保存为优化过的webp文件。另外,还注册了三种字体,包括两种不同版本的Inter字体和Apple Emoji字体,大家可以在Inter字体页面和Apple Emoji字体页面获取这些字体 。

三、实现文本换行功能

在HTML画布上书写文本时,文本通常不会自动换行,所以我们得自定义一个函数来实现该功能。这个函数接收6个参数,具体代码如下:

// 这个函数接受6个参数:
// - ctx: 画布的上下文
// - text: 我们想要换行的文本
// - x: 文本的起始x坐标
// - y: 文本的起始y坐标
// - maxWidth: 最大宽度,即容器的宽度
// - lineHeight: 每行的高度(由我们定义)
const wrapText = function(ctx, text, x, y, maxWidth, lineHeight) {
    // 首先,按空格分割单词
    let words = text.split(' ');
    // 然后我们创建几个变量来存储行的信息
    let line = '';
    let testLine = '';
    // wordArray是我们将要返回的数组,它将保存
    // 行文本的信息,以及它的x和y起始位置
    let wordArray = [];
    // totalLineHeight将保存行高的信息
    let totalLineHeight = 0;

    // 接下来,我们遍历每个单词
    for(var n = 0; n < words.length; n++) {
        // 测试它的长度
        testLine += `${words[n]} `;
        var metrics = ctx.measureText(testLine);
        var testWidth = metrics.width;
        // 如果太长,则我们开始新的一行
        if (testWidth > maxWidth && n > 0) {
            wordArray.push([line, x, y]);
            y += lineHeight;
            totalLineHeight += lineHeight;
            line = `${words[n]} `;
            testLine = `${words[n]} `;
        } else {
            // 否则我们只有一行!
            line += `${words[n]} `;
        }
        // 当所有单词完成后,我们将剩余的内容推入数组
        if(n === words.length - 1) {
            wordArray.push([line, x, y]);
        }
    }

    // 返回包含单词的数组,以及总行高
    // 总行高将是 (总行数 - 1) * 行高
    return [ wordArray, totalLineHeight ];
}

这个函数的作用是根据设定的最大宽度,将传入的文本进行合理换行,并返回包含每行文本信息及其起始坐标的数组,还有总行高信息。

四、编写图片生成函数

接下来编写generateMainImage函数,它将整合各种信息,为文章或网站生成图片。在这个函数里,颜色等参数都可以自行设定。

// 这个函数接受5个参数:
// canonicalName: 这是我们用来保存图片的名字
// gradientColors: 一个包含两种颜色的数组,例如 [ '#ffffff', '#000000' ],用于我们的渐变
// articleName: 你希望在图片中显示的文章或网站的标题
// articleCategory: 该文章所属的类别——或者文章的副标题
// emoji: 你希望在图片中显示的emoji
const generateMainImage = async function(canonicalName, gradientColors, articleName, articleCategory, emoji) {

    articleCategory = articleCategory.toUpperCase();
    // gradientColors是一个数组 [ c1, c2 ]
    if(typeof gradientColors === "undefined") {
        gradientColors = [ "#8005fc", "#073bae"]; // 备用值
    }

    // 创建画布
    const canvas = createCanvas(1342, 853);
    const ctx = canvas.getContext('2d')

    // 添加渐变——我们使用createLinearGradient来实现这一点
    let grd = ctx.createLinearGradient(0, 853, 1352, 0);
    grd.addColorStop(0, gradientColors[0]);
    grd.addColorStop(1, gradientColors[1]);
    ctx.fillStyle = grd;
    // 填充我们的渐变
    ctx.fillRect(0, 0, 1342, 853);

    // 在画布上书写我们的Emoji
    ctx.fillStyle = 'white';
    ctx.font = '95px AppleEmoji';
    ctx.fillText(emoji, 85, 700);

    // 添加我们的标题文本
    ctx.font = '95px InterBold';
    ctx.fillStyle = 'white';
    let wrappedText = wrapText(ctx, articleName, 85, 753, 1200, 100);
    wrappedText[0].forEach(function(item) {
        // 我们将填充数组中的文本item[0],在坐标 [x, y]
        // x是数组中的item[1]
        // y是数组中的item[2],减去行高(wrappedText[1]),再减去emoji的高度(200px)
        ctx.fillText(item[0], item[1], item[2] - wrappedText[1] - 200); // 200是emoji的高度
    })

    // 将我们的类别文本添加到画布上
    ctx.font = '50px InterMedium';
    ctx.fillStyle = 'rgba(255,255,255,0.8)';
    ctx.fillText(articleCategory, 85, 553 - wrappedText[1] - 100); // 853 - 200用于emoji,-100用于1行的行高

    if(fs.existsSync(`./views/images/intro-images/${canonicalName}.png`)) {
        return '图片已存在!我们没有创建任何图片'
    } else {
        // 将画布设置为png格式
        try {
            const canvasData = await canvas.encode('png');
            // 保存文件
            fs.writeFileSync(`./views/images/intro-images/${canonicalName}.png`, canvasData);
        } catch(e) {
            console.log(e);
            return '这次无法创建png图片。'
        }
        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 '这次无法创建webp图片。'
        }

        return '图片已成功创建!';
    }
}

下面详细分析一下这个函数的执行过程:

  1. 数据准备:将文章类别转换为大写形式,并在未传入渐变颜色数组时,设置默认的渐变颜色值。
articleCategory = articleCategory.toUpperCase();
// gradientColors是一个数组 [ c1, c2 ]
if(typeof gradientColors === "undefined") {
    gradientColors = [ "#8005fc", "#073bae"]; // 备用值
}
  1. 创建画布与设置渐变:创建指定尺寸的画布,并获取绘图上下文。利用createLinearGradient方法创建渐变对象,设置渐变颜色并填充整个画布。
// 创建画布
const canvas = createCanvas(1342, 853);
const ctx = canvas.getContext('2d')

// 添加渐变——我们使用createLinearGradient来实现这一点
let grd = ctx.createLinearGradient(0, 853, 1352, 0);
grd.addColorStop(0, gradientColors[0]);
grd.addColorStop(1, gradientColors[1]);
ctx.fillStyle = grd;
// 填充我们的渐变
ctx.fillRect(0, 0, 1342, 853);
  1. 绘制Emoji、标题和类别文本:分别设置Emoji、标题和类别文本的字体、颜色等样式,调用之前编写的wrapText函数处理标题文本换行,并将这些文本绘制到画布的相应位置。
// 在画布上书写我们的Emoji
ctx.fillStyle = 'white';
ctx.font = '95px AppleEmoji';
ctx.fillText(emoji, 85, 700);

// 添加我们的标题文本
ctx.font = '95px InterBold';
ctx.fillStyle = 'white';
let wrappedText = wrapText(ctx, articleName, 85, 753, 1200, 100);
wrappedText[0].forEach(function(item) {
    // 我们将填充数组中的文本item[0],在坐标 [x, y]
    // x是数组中的item[1]
    // y是数组中的item[2],减去行高(wrappedText[1]),再减去emoji的高度(200px)
    ctx.fillText(item[0], item[1], item[2] - wrappedText[1] - 200); // 200是emoji的高度
})

// 将我们的类别文本添加到画布上
ctx.font = '50px InterMedium';
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.fillText(articleCategory, 85, 553 - wrappedText[1] - 100); // 853 - 200用于emoji,-100用于1行的行高
  1. 保存图片:检查指定路径下是否已存在同名的png图片,如果存在则直接返回提示信息;若不存在,则先将画布内容编码为png格式并保存,接着使用cwebp将png图片转换为webp格式保存,最后返回相应的创建结果提示。
if(fs.existsSync(`./views/images/intro-images/${canonicalName}.png`)) {
    return '图片已存在!我们没有创建任何图片'
} else {
    // 将画布设置为png格式
    try {
        const canvasData = await canvas.encode('png');
        // 保存文件
        fs.writeFileSync(`./views/images/intro-images/${canonicalName}.png`, canvasData);
    } catch(e) {
        console.log(e);
        return '这次无法创建png图片。'
    }
    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 '这次无法创建webp图片。'
    }

    return '图片已成功创建!';
}

五、运行生成图片

完成上述代码编写后,在命令行中运行node index.js,就能执行图片生成操作了。按照上述步骤和代码,我们就能用Node.JS和Canvas自动生成图片了,赶紧动手试试吧!


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/front/16726.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】