Nest + Sharp 实现给表情包gif加文字

前言

上篇文章 纯前端js给表情包gif加文字,你就是"表情包大战"最靓的仔! 介绍了如何通过纯前端js给表情包gif加文字。但毕竟客户端无论能力,还是性能、兼容性都有限,比如介绍的库都只支持Web,不支持其它客户端例如小程序。尤其这种文件编码能力,其实在服务端处理能力更好,能用的库或者服务端的工具都很多,所以这种需求尽量还是放在后端实现。

本文就介绍下后端Nodejs上,使用Nest框架,利用第三方库Sharp,给表情包gif加文字。

Nest

如果没有安装过需要全局安装

bash 复制代码
npm i -g @nestjs/cli

新建 Nest 工程

bash 复制代码
nest new nest-sharp-gif

Sharp

github.com/lovell/shar...

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

有两种交互方式:

  1. 上例的cdn url方式,gif文件是一个url。
  2. 通过客户端上传文件。
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可以用optionspagespage俩参数,可以按帧拆分。
    • 拼接的话,找了几个api,试过没效果,有找到的可以分享下。

总结

本文主要介绍了如何使用Sharp第三方图像处理库,通过Nest调用api,给gif图片添加文字内容。相比前端方案 纯前端js给表情包gif加文字,你就是"表情包大战"最靓的仔!,后端nodejs里转换更稳定,更精确,支持的功能也就更多。

源码:github.com/markz-demo/...

相关推荐
你的人类朋友7 小时前
✍️Node.js CMS框架概述:Directus与Strapi详解
javascript·后端·node.js
smallzip12 小时前
node大文件拷贝优化(显示进度)
前端·性能优化·node.js
蓝胖子的多啦A梦13 小时前
npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚
前端·npm·node.js
Spider_Man14 小时前
“AI查用户”也能这么简单?手把手带你用Node.js+前端玩转DeepSeek!
javascript·人工智能·node.js
龚思凯16 小时前
Node.js 模块导入语法变革全解析
后端·node.js
冷凌爱17 小时前
Fetch与Axios:区别、联系、优缺点及使用差异
前端·node.js·js
Sailing17 小时前
Grafana-mcp-analyzer:基于 MCP 的轻量 AI 分析监控图表的运维神器!
前端·node.js·mcp
XI锐真的烦18 小时前
横向对比npm和yarn
前端·npm·node.js
穗余19 小时前
WEB3全栈开发——面试专业技能点P1Node.js / Web3.js / Ethers.js
javascript·node.js·web3
None3211 天前
【NestJs】集成Prisma数据库配置
nestjs