前言
上篇文章 纯前端js给表情包gif加文字,你就是"表情包大战"最靓的仔! 介绍了如何通过纯前端js给表情包gif加文字。但毕竟客户端无论能力,还是性能、兼容性都有限,比如介绍的库都只支持Web,不支持其它客户端例如小程序。尤其这种文件编码能力,其实在服务端处理能力更好,能用的库或者服务端的工具都很多,所以这种需求尽量还是放在后端实现。
本文就介绍下后端Nodejs上,使用Nest框架,利用第三方库Sharp,给表情包gif加文字。
Nest
如果没有安装过需要全局安装
bash
npm i -g @nestjs/cli
新建 Nest 工程
bash
nest new nest-sharp-gif
Sharp
Sharp 是一个高性能的 Node.js 图像处理库,可以非常快的速度来调整图片的大小,同时也支持压缩图片、合成、旋转、提取图片等等。支持多种格式图片,包括:JPEG,PNG,WebP,GIF,AVIF 和 TIFF。
光神之前也介绍过sharp的压缩功能:Nest + Sharp 实现了一个 gif 压缩工具,帮我省不少钱
gif加文字,就得用到sharp的合并功能。
首先安装npm包:
bash
npm install sharp
合并用到了sharp api里的composite方法,用法:
javascript
import * as sharp from 'sharp';
const gifBuffer // gif 文件流
const image = await sharp(gifBuffer, { animated: true })
const metadata = await image.metadata();
// 获取svg buffer,注意需要设置svg的高宽同原图一致
const textImage = Buffer.from(`
<svg width="${metadata.width}px" height="${metadata.pageHeight}px" xmlns="http://www.w3.org/2000/svg">
<text x="0" y="30px" font-size="30px" fill="red">Text</text>
</svg>`);
const buffer = await image
.composite([
{ input: textImage, tile: true } // 可以添加多个图层
])
.toBuffer();
先弄个gif原图片,这里就用掘金的cdn url:p1-juejin.byteimg.com/tos-cn-i-k3...

通过fetch获取文件流:
typescript
import fetch from 'node-fetch';
const url = "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/eb1e0ec360f041729bde3420d0988b45~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=300&h=240&s=1541600&e=gif&f=30&b=8c7c5b";
// 用fetch获取gif文件流
const image1 = await fetch(url);
const gifBuffer = await image1.buffer();
效果图:

需要注意两点:
- 需要设置
tile: true
,才可以在gif每帧上都合成,否则只会合成第一帧。 - 另一个需要注意,svg的高度必须跟原gif图片高度一致,否则会重复叠加图层,会出现文字循环滚动效果,如下例。
javascript
<svg width="100px" height="30px" xmlns="http://www.w3.org/2000/svg">
<text x="0" y="30px" font-size="30px" fill="red">Text</text>
</svg>
效果图:

例子
弄清楚了composite用法,就可以写例子了,添加nest controller test方法:
javascript
@Get('test1')
async test1(
@Res() res: Response
) {
const url = "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/eb1e0ec360f041729bde3420d0988b45~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=300&h=240&s=1541600&e=gif&f=30&b=8c7c5b";
// 用fetch获取gif文件流
const image1 = await fetch(url);
const gifBuffer = await image1.buffer();
// 用sharp获取图片高宽
const image = await sharp(gifBuffer, { animated: true });
const metadata = await image.metadata();
const width = metadata.width;
const height = metadata.pageHeight; // 注意是pageHeight,不是height
const text = '不错不错';
const x = width / 2;
const y = 50;
const fontFamily = 'Arial, sans-serif';
const fontSize = 30;
const color = 'white';
const stroke = 'red';
// 获取svg buffer,注意需要设置svg的高宽同原图一致
const textImage = Buffer.from(`
<svg width="${width}px" height="${height}px" xmlns="http://www.w3.org/2000/svg">
<text
text-anchor="middle"
y="${y}" x="${x}"
font-family="${fontFamily}"
font-size="${fontSize}px"
fill="${color}"
stroke="${stroke}"
>${text}</text>
</svg>`
);
const buffer = await image
.composite([
{ input: textImage, tile: true } // 可以添加多个图层
])
.toBuffer();
// response输出文件流
res.set('Content-Type', 'image/gif');
res.send(buffer);
}
直接浏览器访问下http://localhost:3000/test1就能看到效果,因为response设置了Content-Type,浏览器会自动preview图片。

Web
有两种交互方式:
- 上例的cdn url方式,gif文件是一个url。
- 通过客户端上传文件。
html
<div>
<!-- url 转换 -->
<button id="btn1">test1</button>
<br>
<img id="img1" />
</div>
<div>
<!-- 上传文件 转换 -->
<input id="input1" type="file">
<br>
<img id="img2" />
</div>
<script>
// url 转换
const btn1 = document.getElementById('btn1');
const img1 = document.getElementById('img1');
btn1.onclick = async function () {
const res = await fetch('http://localhost:3000/test1', { method: "GET" });
const blob = await res.blob();
const base64 = URL.createObjectURL(blob);
img1.src = base64;
}
// 上传文件 转换
const input1 = document.getElementById('input1');
const img2 = document.getElementById('img2');
input1.onchange = async function (e) {
const file = e.target.files[0] || e.dataTransfer.files[0];
const formData = new FormData();
formData.append('file', file);
const res = await fetch('http://localhost:3000/upload', { method: "POST", body: formData });
const blob = await res.blob();
const base64 = URL.createObjectURL(blob);
img2.src = base64;
}
</script>
nest upload方法,需要引用npm install @types/multer -D
:
typescript
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
async gifAddTextsUpload(
@UploadedFile() file: Express.Multer.File,
@Res() res: Response
) {
const image = await sharp(file.buffer, { animated: true });
const metadata = await image.metadata();
// 同上实现
效果:
其它创建图层方式
composite.input
也支持直接定义text信息,但是经测试,如果不设置width\height,会有重复文字问题;设置了则会有滚动效果,都没打到预期,可能是我用的不对。
typescript
.composite([
// { input: textImage, tile: true } // 可以添加多个图层
{
input: {
text: {
text: 'Text',
rgba: true,
font: 'Arial',
width: metadata.width,
height: metadata.pageHeight,
},
},
tile: true
}
])
效果:
也可以用sharp先把文字转成png图片,然后再当做图层合成,但是经测试也会有文字滚动效果,问题同上,效果也跟上面一样。
javascript
const overlay = await sharp({
text: {
text: 'Text',
rgba: true,
font: 'Arial',
width: metadata.width,
height: metadata.pageHeight,
},
}).png().toBuffer();
更多用法
- 水印
- composite.input是个文件流,所以也支持传入图片,再加上背景透明,就可以做成水印效果。
- 多个文字
- 因为composite参数是数组,所以支持多个图层,也就支持添加多个文字或水印。
- 分段显示文字
- 有的gif需要先后不同位置显示文字,就跟台词一样。思路是先把gif按帧拆分成多个gif,然后分别给合并对应文字,然后再拼接成一个gif。
- 拆分gif可以用
options
的pages
和page
俩参数,可以按帧拆分。 - 拼接的话,找了几个api,试过没效果,有找到的可以分享下。
总结
本文主要介绍了如何使用Sharp第三方图像处理库,通过Nest调用api,给gif图片添加文字内容。相比前端方案 纯前端js给表情包gif加文字,你就是"表情包大战"最靓的仔!,后端nodejs里转换更稳定,更精确,支持的功能也就更多。