功能需求
微信小程序实现图片水印功能,有6种位置模式:左上角、左下角、中间、右上角、右下角及平铺,并且平铺时支持旋转和文字交错。
最终实现效果
微信扫码查看或者搜索小程序:工具迷你仓https://i.postimg.cc/7Z2VB04L/qrcode.jpg
逻辑设计(分两部分)
第一部分是单个文字的水印(即左上角、左下角、中间、右上角、右下角位置),通过计算图片宽高和文字宽高计算出绘画水印的位置坐标。
第二部分是平铺水印,先生成透明水印图,再将水印图放在目标图上。
关键代码
一、左上角、左下角、中间、右上角、右下角水印
1、在canvas上画一张图片,canvas的宽高等于图片的宽高
2、通过计算图片的宽高以及文字的宽高(measureText方法),可计算出左上角、左下角、中间、右上角、右下角的位置坐标:
左上角=(0, 0)
左下角=(0, 图片的高度-文字的高度)
中间=(图片的宽度/2-文字的宽度/2, 图片的高度/2-文字的高度)
右上角=(图片的宽度-文字的宽度, 0)
右下角=(图片的宽度-文字的宽度, 图片的高度-文字的高度)





let textMetrics = ctx.measureText(maskText);
//水印文字宽度
let { width: textWidth, actualBoundingBoxAscent, actualBoundingBoxDescent } = textMetrics;
//水印文字高度
let textHeight = actualBoundingBoxAscent ? (actualBoundingBoxAscent + actualBoundingBoxDescent) : (textMetrics.fontBoundingBoxAscent + textMetrics.fontBoundingBoxDescent);
// 设置canvas宽高
canvas.width = imgWidth
canvas.height = imgHeight
// 创建目标图片对象
const image = canvas.createImage();
image.src = imgFilePath;
image.onload = () => {
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 将图片绘制到canvas上
ctx.drawImage(image, 0, 0, canvas.width, canvas.height)
let posXmargin = this.data.posXmargin // 自定义离左/右边的距离
let posYmargin = this.data.posYmargin // 自定义离上/下边的距离
switch (pos) {
case 'left top': //左上角
let lt_x = posXmargin
let lt_y = posYmargin + textHeight
ctx.font = font
ctx.fillStyle = fontColor
ctx.fillText(maskText, lt_x, lt_y)
ctx.restore()
ctx.save()
break;
case 'left bottom': //左下角
let lb_x = posXmargin
let lb_y = imgHeight - posYmargin
ctx.font = font
ctx.fillStyle = fontColor
ctx.fillText(maskText, lb_x, lb_y)
ctx.restore()
ctx.save()
break;
case 'center': //居中
ctx.translate(imgWidth / 2, imgHeight / 2)// 移动画布到中心位置,以便围绕中心点旋转图片
ctx.rotate(angle * Math.PI / 180)// 旋转图片
ctx.translate(-textWidth / 2, textHeight / 2)// 再次移动画布回到文字开始位置
ctx.font = font
ctx.fillStyle = fontColor
ctx.fillText(maskText, 0, 0)
ctx.restore()
ctx.save()
break;
case 'right top': //右上角
let rt_x = imgWidth - textWidth - posXmargin
let rt_y = posYmargin + textHeight
ctx.font = font
ctx.fillStyle = fontColor
ctx.fillText(maskText, rt_x, rt_y)
ctx.restore()
ctx.save()
break;
case 'right bottom': //右下角
let rb_x = imgWidth - textWidth - posXmargin
let rb_y = imgHeight - posYmargin
ctx.font = font
ctx.fillStyle = fontColor
ctx.fillText(maskText, rb_x, rb_y)
ctx.restore()
ctx.save()
break;
}
ctx.restore()
// 将canvas转为图片
wx.canvasToTempFilePath({
canvas: canvas,
success: (r) => {
wx.hideLoading()
this.setData({ wmImgFilePath: r.tempFilePath })
},
})
}
二、平铺水印
1、文字平铺
用2个for循环嵌套(画布X轴嵌Y轴),文平文字间隔和垂直文字间隔分别为:x_interval、y_interval

for (let x = 0; x < canvas.width; x += x_interval) {
for (let y = 0; y < canvas.height; y += y_interval) {
ctx.translate(x, y)
ctx.font = font
ctx.fillStyle = fontColor
ctx.fillText(maskText, 0, 0);
ctx.restore()
ctx.save()
}
}
2、文字交错
偶数行的文字整体往右移:半个文字宽度

let cross_intereval = textWidth / 2
let cross = this.data.cross // 隔行标志位
for (let x = 0; x < canvas.width; x += x_interval) {
let cross_flag = false
for (let y = 0; y < canvas.height; y += y_interval) {
let _x = cross_flag ? x + cross_intereval : x
cross && (cross_flag = !cross_flag)
ctx.translate(_x, y)
ctx.font = font
ctx.fillStyle = fontColor
ctx.fillText(maskText, 0, 0);
ctx.restore()
ctx.save()
}
}
3、水印旋转
(1) 生成一张正方形透明水印图,水印图的宽高=目标图的对角线长度。
(2)将水印图铺在目标图上面,并加上旋转即可。

// 第一步:生成 “边长=图片对角线长度” 的水印图
const borderWidth = Math.ceil(Math.sqrt(imgWidth * imgWidth + imgHeight * imgHeight)
)
// 设置canvas宽高
canvas.width = borderWidth
canvas.height = borderWidth
let wm_interval = this.data.interval
let y_interval = textHeight + wm_interval
let x_interval = textWidth + wm_interval
let cross_intereval = textWidth / 2
let cross = this.data.cross // 隔行标志位
for (let x = 0; x < canvas.width; x += x_interval) {
let cross_flag = false
for (let y = 0; y < canvas.height; y += y_interval) {
let _x = cross_flag ? x + cross_intereval : x
cross && (cross_flag = !cross_flag)
ctx.translate(_x, y)
ctx.font = font
ctx.fillStyle = fontColor
ctx.fillText(maskText, 0, 0);
ctx.restore()
ctx.save()
}
}
// 将canvas转为水印图片
wx.canvasToTempFilePath({
canvas: canvas,
success: (res) => {
// 临时水印图片
const tempWmImgPath = res.tempFilePath
// 第二步:创建目标图片对象
const image = canvas.createImage();
image.src = imgFilePath;
image.onload = () => {
canvas.width = imgWidth
canvas.height = imgHeight
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 将图片绘制到canvas上
ctx.drawImage(image, 0, 0, imgWidth, imgHeight)
// 第三步:将水印绘制到canvas上
const wmImage = canvas.createImage();
wmImage.src = tempWmImgPath;
wmImage.onload = () => {
// 保存当前环境的状态:这样可以在旋转后恢复到之前的状态
ctx.save();
// 移动画布到中心位置,以便围绕中心点旋转图片
ctx.translate(imgWidth / 2, imgHeight / 2);
// 旋转图片(以弧度计)
ctx.rotate(angle / 180 * Math.PI); // 45度 = Math.PI / 4 弧度
// 再次移动画布回到原始位置,以便图片绘制在正确的位置(考虑到旋转)
ctx.translate(-wmImage.width / 2, -wmImage.height / 2);
// 绘制图片
ctx.drawImage(wmImage, 0, 0);
// 恢复之前保存的画布状态(如果不调用,之后的绘制将会受到影响)
ctx.restore();
// 最后:将canvas转为图片,图+印
wx.canvasToTempFilePath({
canvas: canvas,
success: (r) => {
wx.hideLoading()
this.setData({ wmImgFilePath: r.tempFilePath })
},
})
}
}
},
})
总结
本人踩的坑,平铺水印时直接旋转文字,结果当文字长度或者旋转角度不同时,文字会重叠,计算了两天三角函数sin和cos。。。
踩坑原因:在需求分析阶段对功能的剖析不够,没认真分析平铺水印的规律,对市场现有的功能使用不足。