章
目
录
开发过程中,我们常常会有自动生成图片的需求,比如为文章生成缩略图。今天就来详细讲讲如何借助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 '图片已成功创建!';
}
}
下面详细分析一下这个函数的执行过程:
- 数据准备:将文章类别转换为大写形式,并在未传入渐变颜色数组时,设置默认的渐变颜色值。
articleCategory = articleCategory.toUpperCase();
// gradientColors是一个数组 [ c1, c2 ]
if(typeof gradientColors === "undefined") {
gradientColors = [ "#8005fc", "#073bae"]; // 备用值
}
- 创建画布与设置渐变:创建指定尺寸的画布,并获取绘图上下文。利用
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);
- 绘制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行的行高
- 保存图片:检查指定路径下是否已存在同名的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自动生成图片了,赶紧动手试试吧!