前端图片水印方案:Canvas 实现可见与隐形水印
水印不只是在图片上写几个字。从平铺到自适应,从可见到隐形,一文讲透前端水印技术。
做过 ToB 产品的同学应该都遇到过这个需求:「给页面截图加上用户工号水印,万一泄露了方便追溯」。我第一次做的时候图省事,直接用 CSS 的 background-image 贴了个半透明文字层。结果被安全团队打回来了——用户打开 F12 把那层 div 删掉就没了,形同虚设。后来我改用 MutationObserver 监听 DOM 变化,删了就自动重建,总算过了安全评审。
说到隐形水印,很多人觉得这是什么黑科技。其实原理特别朴素——修改图片像素的最低位(LSB),人眼完全看不出变化,但用程序就能提取出来。不过 LSB 水印有个致命弱点:对方只要截图、加滤镜或者重新压缩就可能丢失。真正工业级的方案是 DCT(离散余弦变换)域水印,嵌入在频域信息里,鲁棒性好得多,但实现复杂度也高很多。
1. 可见水印:Canvas 文字叠加
最常见的水印形式:在图片上叠加半透明文字或 Logo。核心思路是创建一个与原图同尺寸的 Canvas,先绘制原图,再叠加旋转的文字。
// 单行文字水印
function addTextWatermark(
image: HTMLImageElement,
text: string,
options = {}
) {
const { opacity = 0.15, fontSize = 16, angle = -25, gap = 150 } = options
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')!
canvas.width = image.naturalWidth
canvas.height = image.naturalHeight
// 绘制原图
ctx.drawImage(image, 0, 0)
// 水印样式
ctx.fillStyle = `rgba(0, 0, 0, ${opacity})`
ctx.font = `${fontSize}px sans-serif`
ctx.rotate((angle * Math.PI) / 180)
// 平铺水印(覆盖旋转后的扩展区域)
const diagonal = Math.sqrt(canvas.width ** 2 + canvas.height ** 2)
for (let y = -diagonal; y < diagonal; y += gap) {
for (let x = -diagonal; x < diagonal; x += gap) {
ctx.fillText(text, x, y)
}
}
return canvas.toDataURL('image/png')
}水印参数调优建议:
- • 透明度:0.1-0.2 之间,既可见又不影响阅读
- • 旋转角度:-20 到 -30 度,斜着放比水平更难被裁掉
- • 间距:太密影响观感,太疏容易被截取到无水印区域
- • 字体大小:按图片短边的 2-3% 来计算,保证不同尺寸图片效果一致
2. 页面全局水印:防删除方案
ToB 场景常见的需求是给整个页面加水印。实现思路是用一个固定定位的 Canvas 覆盖在页面上,同时用 MutationObserver 防止用户通过 DevTools 删除水印节点。
function createPageWatermark(text: string) {
// 1. 创建水印 Canvas(小块,用于 repeat)
const patternCanvas = document.createElement('canvas')
patternCanvas.width = 200
patternCanvas.height = 150
const pCtx = patternCanvas.getContext('2d')!
pCtx.fillStyle = 'rgba(0, 0, 0, 0.06)'
pCtx.font = '14px sans-serif'
pCtx.rotate(-0.4)
pCtx.fillText(text, 20, 100)
// 2. 创建全屏覆盖层
const container = document.createElement('div')
container.style.cssText = `
position: fixed; inset: 0; z-index: 99999;
pointer-events: none;
background: url(${patternCanvas.toDataURL()}) repeat;
`
document.body.appendChild(container)
// 3. 防删除:MutationObserver
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const node of m.removedNodes) {
if (node === container) {
document.body.appendChild(container)
}
}
}
})
observer.observe(document.body, { childList: true })
return () => {
observer.disconnect()
container.remove()
}
}注意:纯前端水印无法做到 100% 防破解。用户可以禁用 JavaScript、用截图工具截图后 P 掉水印、或者直接在 Network 面板拦截请求。前端水印更多是「提高破解成本」,真正关键的数据保护还是要在后端做。
3. 隐形水印:LSB 像素嵌入
隐形水印(盲水印)的原理是修改图片像素的最低有效位(Least Significant Bit)。由于最低位只影响颜色值的 1/256,人眼完全无法分辨,但程序可以精确提取。
原始像素值
RGB(142, 87, 201)
人眼看到的颜色
嵌入水印后
RGB(143, 86, 200)
肉眼完全看不出差异
// LSB 隐形水印嵌入
function embedLSBWatermark(
imageData: ImageData,
message: string
) {
const data = imageData.data // RGBA 像素数组
const bits = textToBits(message) // 将文本转为二进制位
for (let i = 0; i < bits.length; i++) {
// 只修改 R 通道的最低位
const pixelIndex = i * 4
if (bits[i] === '1') {
data[pixelIndex] = data[pixelIndex] | 1 // 置 1
} else {
data[pixelIndex] = data[pixelIndex] & 0xFE // 清 0
}
}
return imageData
}
function extractLSBWatermark(
imageData: ImageData,
length: number
) {
const data = imageData.data
let bits = ''
for (let i = 0; i < length * 8; i++) {
bits += (data[i * 4] & 1).toString()
}
return bitsToText(bits)
}LSB 方案简单有效,但鲁棒性较差。图片经过 JPEG 有损压缩、截图、缩放等操作后,最低位信息可能被破坏。如果需要更强的抗攻击能力,需要使用 DCT 变换域水印或小波变换水印,这些方案将信息嵌入在图片的频域特征中,即使图片被裁剪或压缩也能提取。
4. 性能优化:大图水印不卡顿
当处理 4K 甚至更大的图片时,Canvas 的像素操作会变得很慢。几个关键的优化策略:
OffscreenCanvas + Web Worker
把 Canvas 操作放到 Worker 线程。OffscreenCanvas 可以在 Worker 中使用,完全不阻塞主线程。这在批量处理多张图片时效果尤其明显。
水印 Pattern 预生成
不要每次都逐个绘制水印文字。先在一个小 Canvas 上画好水印 pattern,然后用 createPattern() 一次性铺满。绘制调用从 N 次减少到 1 次。
ImageBitmap 替代 HTMLImageElement
createImageBitmap() 返回的对象可以在 Worker 中使用,而且解码过程是异步的。配合 Transferable 传输,实现真正的零拷贝图片处理。
5. 水印方案选型对比
| 方案 | 实现难度 | 可见性 | 鲁棒性 | 适用场景 |
|---|---|---|---|---|
| Canvas 文字叠加 | 低 | 可见 | 高(烧录到像素) | 图片版权标识 |
| CSS 覆盖层 | 低 | 可见 | 低(可被删除) | 页面防截图追溯 |
| LSB 隐形水印 | 中 | 不可见 | 低(不耐压缩) | 无损格式溯源 |
| DCT 频域水印 | 高 | 不可见 | 高(耐压缩/裁剪) | 工业级版权保护 |
在线添加图片水印
OneKit 的 图片水印工具 支持添加文字水印和图片水印,自定义透明度、角度、间距等参数,实时预览效果。所有处理都在浏览器本地完成,你的图片不会被上传到任何服务器。