Node.js + gm 实现图片上传并添加地址时间水印功能
在 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
};
}五、注意事项与优化建议
- 环境依赖 :gm 库依赖 GraphicsMagick/ImageMagick,需先在服务器安装(如 CentOS:
yum install ImageMagick); - 文件清理 :处理完成后需删除本地临时文件(如 D:/test/1.png),避免磁盘占用;
- 异常捕获 :补充 try/catch 包裹核心逻辑,避免单个步骤失败导致接口崩溃;
- 配置解耦 :OSS 的 accessKey、region 等配置应放在 Egg.js 的配置文件(config/config.default.js)中,而非硬编码;
- 性能优化 :大图片处理可能耗时,可结合队列(如 BullMQ)异步处理,避免接口超时。
总结
- 接口开发需先定义清晰的文档规范(Swagger 注释),明确参数、权限和返回格式;
- 图片水印核心依赖 gm 库,需注意字体配置(解决中文乱码)和坐标定位(避免水印重叠);
- 完整业务闭环需结合 OSS 实现图片的远程存储,同时做好异常处理和环境依赖配置。
本文的代码可直接适配 Egg.js 框架,稍作调整(如替换 OSS 配置、字体路径)即可投入生产环境使用,满足图片上传+地址/时间水印+远程存储的核心需求。




