图片处理2026-03-20 9 分钟

前端图片水印方案: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 的 图片水印工具 支持添加文字水印和图片水印,自定义透明度、角度、间距等参数,实时预览效果。所有处理都在浏览器本地完成,你的图片不会被上传到任何服务器。