开发过程中,我们常常会有自动生成图片的需求,比如为文章生成缩略图。今天就来详细讲讲如何借助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自动生成图片了,赶紧动手试试吧!