在本指南中,我将展示如何用 Node.JS 生成文章缩略图。以下是我用这种方法生成的一张图片:
本文的完整代码可以在 Git Gist 中找到。
由于 Node.JS ,它本身并不具备 canvas 功能。我们使用一个名为 canvas
的组件,将其导入到我们的 Node.JS 项目中。可以通过运行 npm i canvas
来安装它。
如何在 Canvas 中使用 Emoji
对于我生成的图片,我还想使用 Emoji。因此,我使用了该包的一个分支,名为 @napi-rs/canvas
,它支持 Emoji。我使用的版本是 0.1.14
,所以如果你在本指南操作时遇到问题,尝试用 npm i @napi-rs/[email protected]
命令安装它。
现在我们已经了解了基础知识,让我们开始吧。首先,让我们导入所有需要的包。在这里我导入了几个东西:
- canvas --- 这是我们创建图片的方式。
- fs --- 将图片写入服务器并保存。
- cwebp --- 这是我们将图片保存为 webp 文件的方式,这样它就能针对网络进行优化。
- fonts --- 导入 3 种字体------其中两种是 Inter 的不同版本,这是一种很好的字体,最后一种是 Apple Emoji 字体。你可以在 Inter 字体页面 找到 Inter 字体,在 Apple Emoji 字体页面 找到 Apple Emoji 字体。
js
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');
如何用 JavaScript 生成文章缩略图
当我们在 HTML 画布上书写文本时,它通常不会自动换行。相反,我们需要创建一个函数来测量容器的宽度,并决定是否换行。注释后的函数如下所示:
js
// 这个函数接受 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
函数。这个函数将接受我们提供的所有信息,并为你的文章或网站生成一张图片。
在这个函数中,你可以传入任何你想要的颜色,选择权在你手中。
js
// 这个函数接受 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 '图片已成功创建!';
}
}
用 Node.JS 生成文章图片
让我们仔细观察一下这个函数,以便完全理解其中的原理。我们首先准备数据------将类别转换为大写,并设置一个默认渐变。然后我们创建画布,并使用 getContext
初始化一个绘制的空间。
js
articleCategory = articleCategory.toUpperCase();
// gradientColors 是一个数组 [ c1, c2 ]
if(typeof gradientColors === "undefined") {
gradientColors = [ "#8005fc", "#073bae"]; // 备用值
}
// 创建画布
const canvas = createCanvas(1342, 853);
const ctx = canvas.getContext('2d')
然后绘制渐变:
js
// 添加渐变------我们使用 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 文本。
js
// 在画布上书写我们的 Emoji
ctx.fillStyle = 'white';
ctx.font = '95px AppleEmoji';
ctx.fillText(emoji, 85, 700);
现在我们使用我们的换行函数 wrapText
。我们将传入相当长的 articleName
,并从图片底部附近的 85, 753
开始。由于 wrapText
返回一个数组,我们将遍历该数组以确定每行的坐标,并将它们绘制到画布上:
js
// 添加我们的标题文本
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 行的行高
将画布图片保存到服务器
好了,现在我们已经创建了图片,让我们将它保存到服务器上:
- 首先,我们将检查文件是否存在。如果存在,我们将返回图片。
- 如果文件不存在,我们将尝试使用
canvas.encode
创建 png 版本,并使用fs.writeFileSync
保存它。 - 如果一切顺利,我们将使用
cwebp
保存一个.webp
版本的文件,这比 .png 版本小得多。
js
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
以下是通过这种方式生成的一张图片的示例: