Node.js + gm 实现图片上传并添加地址时间水印功能

2026-04-14 88 浏览 0 评论

在 Web 开发中,图片上传并添加水印是非常常见的业务需求,比如图片素材溯源、版权保护、场景信息标注等。本文将基于 Egg.js 框架,结合 gm 图像处理库和 OSS 存储服务,实现一套“图片上传-添加地址+时间水印-上传至 OSS”的完整接口方案。

一、接口设计与文档说明

首先我们先定义接口的基础信息,包括请求方式、路径、权限校验、参数和返回值,这是接口开发的第一步,也是前后端协作的重要依据。

1.1 接口基础信息

/**
@api {post} /v1/uploadAndWatermark 上传文件并添加地址和时间水印同时上传到 OSS
@apiGroup upload
@apiVersion 1.0.0

@apiHeader {String} authorization 用户凭证
@apiHeaderExample {json} Header-Example
{
  "Authorization": "Bearer " + token
}

@apiparam {Binnary} upload File content
@apiparam {String} address 地址

@apiSuccessExample {json} 返回样例
{
  "data": '',
  "code": '',
  "success": true,
  "message": ""
}
*/

这段代码是基于 Swagger 规范的接口注释,核心信息解读:

  • 请求方式&路径 :POST /v1/uploadAndWatermark,明确接口的访问方式和路由;
  • 接口分组 :归属 upload 分组,便于接口文档的分类管理;
  • 权限校验 :请求头必须携带 Authorization 字段(Bearer + token),用于用户身份验证;
  • 请求参数 :包含二进制的图片文件(upload)和字符串类型的地址(address);
  • 返回格式 :统一的 JSON 结构,包含 data(业务数据)、code(状态码)、success(是否成功)、message(提示信息)。

二、核心业务逻辑实现

接下来是接口的核心处理函数,我们将拆分成多个步骤逐一解析,让逻辑更清晰。

2.1 初始化参数与文件校验

async uploadAndWatermark() {
  // 1. 获取请求参数和上传文件
  const body = this.ctx.request.body; // 获取表单参数(包含 address)
  const file = this.ctx.request.files[0]; // 获取上传的第一个文件

  // 2. 校验文件类型:仅允许图片格式
  if (!/image/.test(file.mimeType)) {
    this.ctx.returnError('上传文件不是图片'); // 自定义错误返回方法
  }

这一步的核心作用:

  • 从 Egg.js 的上下文(ctx)中提取关键数据: body 是表单参数(包含地址信息), files[0] 是前端上传的文件对象;
  • 文件类型校验:通过正则匹配 mimeType (如 image/jpeg、image/png),确保上传的是图片,非图片则直接返回错误提示。

2.2 图片水印处理(核心)

  // 3. 定义水印处理的异步方法
  const handler = () => {
    return new Promise(r => {
      gm(file.filepath) // 基于 gm 库读取本地临时文件
        .fill('white') // 设置水印文字颜色为白色
        .fontSize(16) // 设置水印字体大小
        .font('./app/public/font/SourceHanSerifCN-Medium.ttf') // 指定字体文件(解决中文乱码)
        .drawText(10, 40, body.address) // 绘制地址水印:坐标(10,40),内容为传入的 address
        .drawText(10, 80, this.ctx.helper.date('Y-m-d H:i:s')) // 绘制时间水印:坐标(10,80),内容为当前时间
        .write('D:/test/1.png', err => { // 将加水印后的图片写入指定路径
          r(err); // Promise 回调:成功返回 null,失败返回错误对象
        });
    });
  };

  // 4. 执行水印处理
  const res = await handler();

这是水印添加的核心逻辑,重点说明:

  • 异步处理 :将 gm 库的回调式操作封装为 Promise,适配 async/await 语法;
  • gm 库核心 API
  • gm(file.filepath) :读取 Egg.js 临时存储的上传文件;
  • fill/fontSize/font :设置水印文字的样式(颜色、大小、字体),指定中文字体文件可避免水印文字乱码;
  • drawText(x, y, text) :在图片的指定坐标绘制文字,第一个是地址水印,第二个是当前时间水印(通过 Egg.js 自定义工具函数格式化时间);
  • write(path, callback) :将处理后的图片写入本地路径(示例为 D:/test/1.png),回调函数返回处理结果(err 为 null 则成功)。

2.3 结果返回与后续扩展

  // 5. 组装返回结果
  this.ctx.body = {
    message: res ? res.message : '', // 有错误则返回错误信息,否则为空
    data: file.filepath, // 返回原文件路径(可替换为 OSS 地址)
    success: !res, // 无错误则 success 为 true
    code: 0, // 状态码(0 代表成功,可根据业务定义)
  };
}

这一步是接口的最终返回逻辑:

  • 根据水印处理结果(res)判断是否成功:res 为 null 代表处理成功,否则为错误对象;
  • 返回数据说明:示例中 data 返回的是本地临时文件路径,实际业务中需替换为 OSS 上传后的远程地址(下文会补充)。

三、关键补充:OSS 上传(完善业务闭环)

原代码中标注了“上传到 OSS”但未实现,这里补充核心代码,完善完整业务流程:

// 补充:水印处理完成后上传至 OSS
const OSS = require('ali-oss'); // 引入阿里云 OSS SDK
// 初始化 OSS 客户端
const client = new OSS({
  region: 'oss-cn-hangzhou', // 你的 OSS 地域
  accessKeyId: 'your-accessKeyId', // 阿里云 AccessKey
  accessKeySecret: 'your-accessKeySecret', // 阿里云 AccessKeySecret
  bucket: 'your-bucket-name' // OSS 存储空间名称
});

// 在 handler 的 write 回调中添加 OSS 上传逻辑
.write('D:/test/1.png', async (err) => {
  if (!err) {
    // 上传加水印后的图片到 OSS
    const ossResult = await client.put(`watermark/${Date.now()}-${file.filename}`, 'D:/test/1.png');
    // OSS 上传成功后返回远程地址
    r({ ossUrl: ossResult.url }); 
  } else {
    r(err);
  }
});

补充说明:

  • 需先安装依赖: npm install ali-oss gm
  • gm 库依赖系统环境:需安装 GraphicsMagick 或 ImageMagick(Windows 需配置环境变量,Linux 可通过 yum/apt 安装);
  • OSS 上传后,返回结果中的 data 应替换为 ossResult.url (OSS 远程地址),而非本地路径。

四、完整代码(整合版)

/**
@api {post} /v1/uploadAndWatermark 上传文件并添加地址和时间水印同时上传到 OSS
@apiGroup upload
@apiVersion 1.0.0

@apiHeader {String} authorization 用户凭证
@apiHeaderExample {json} Header-Example
{
  "Authorization": "Bearer " + token
}

@apiparam {Binnary} upload File content
@apiparam {String} address 地址

@apiSuccessExample {json} 返回样例
{
  "data": "https://your-bucket.oss-cn-hangzhou.aliyuncs.com/watermark/1710000000000-test.png",
  "code": 0,
  "success": true,
  "message": ""
}
*/

const OSS = require('ali-oss');
async uploadAndWatermark() {
  // 1. 获取请求参数和文件
  const body = this.ctx.request.body;
  const file = this.ctx.request.files[0];

  // 2. 校验图片类型
  if (!/image/.test(file.mimeType)) {
    this.ctx.returnError('上传文件不是图片');
  }

  // 3. 初始化 OSS 客户端
  const client = new OSS({
    region: 'oss-cn-hangzhou',
    accessKeyId: 'your-accessKeyId',
    accessKeySecret: 'your-accessKeySecret',
    bucket: 'your-bucket-name'
  });

  // 4. 处理水印并上传 OSS
  const handler = () => {
    return new Promise(async (resolve) => {
      try {
        // 加水印并写入本地
        await gm(file.filepath)
          .fill('white')
          .fontSize(16)
          .font('./app/public/font/SourceHanSerifCN-Medium.ttf')
          .drawText(10, 40, body.address)
          .drawText(10, 80, this.ctx.helper.date('Y-m-d H:i:s'))
          .write('D:/test/1.png');

        // 上传到 OSS
        const ossResult = await client.put(`watermark/${Date.now()}-${file.filename}`, 'D:/test/1.png');
        resolve({ ossUrl: ossResult.url }); // 返回 OSS 地址
      } catch (err) {
        resolve(err); // 返回错误信息
      }
    });
  };

  // 5. 执行处理逻辑
  const res = await handler();

  // 6. 组装返回结果
  this.ctx.body = {
    message: res.message || '',
    data: res.ossUrl || '', // 返回 OSS 远程地址
    success: !res.message,
    code: res.message ? -1 : 0, // 错误状态码设为-1,成功为 0
  };
}

五、注意事项与优化建议

  1. 环境依赖 :gm 库依赖 GraphicsMagick/ImageMagick,需先在服务器安装(如 CentOS: yum install ImageMagick );
  2. 文件清理 :处理完成后需删除本地临时文件(如 D:/test/1.png),避免磁盘占用;
  3. 异常捕获 :补充 try/catch 包裹核心逻辑,避免单个步骤失败导致接口崩溃;
  4. 配置解耦 :OSS 的 accessKey、region 等配置应放在 Egg.js 的配置文件(config/config.default.js)中,而非硬编码;
  5. 性能优化 :大图片处理可能耗时,可结合队列(如 BullMQ)异步处理,避免接口超时。

总结

  1. 接口开发需先定义清晰的文档规范(Swagger 注释),明确参数、权限和返回格式;
  2. 图片水印核心依赖 gm 库,需注意字体配置(解决中文乱码)和坐标定位(避免水印重叠);
  3. 完整业务闭环需结合 OSS 实现图片的远程存储,同时做好异常处理和环境依赖配置。

本文的代码可直接适配 Egg.js 框架,稍作调整(如替换 OSS 配置、字体路径)即可投入生产环境使用,满足图片上传+地址/时间水印+远程存储的核心需求。


发布评论

发布评论前请先 登录
取消
0 评论
点赞
收藏

评论列表 0

暂无评论