Skip to content

Watermark · 页面水印

零依赖的页面水印组件。Canvas 生成背景图 + MutationObserver 防篡改保护。支持多行文字、明暗主题、隐藏标识追踪、像素域隐形水印和动态刷新。

⚠️ 注意: 默认水印文字为白色(适配本页面深色主题)。在浅色页面使用时请将 colorScheme 改为 'dark'(黑字)。

快速开始

ts
import { createWatermark } from '@bilibaba/ts-lab/ui'

const wm = createWatermark({
  text: '内部资料',
  opacity: 0.1,
  rotate: -25,
  colorScheme: 'light',  // 白字(深色主题)。浅色页面请用 'dark'
})

页面会立即覆盖一层半透明水印背景(pointer-events: none,不影响任何交互)。默认黑字(colorScheme: 'dark'),深色主题页面需改为 'light'


API

createWatermark(options)

创建水印实例并挂载到 <body>

ts
const wm = createWatermark({
  // ===== 基础 =====
  text: ['内部资料', '张三'],   // 水印文字,string | string[]
  opacity: 0.15,                // 透明度 0–1,默认 0.15
  rotate: -30,                  // 旋转角度 (°),默认 -30
  fontSize: 16,                 // 字号 px,默认 16
  fontFamily: 'sans-serif',     // 字体
  color: '#000',                // 文字颜色,默认 '#000'
  colorScheme: 'light',          // 主题:'light' 白字 | 'dark' 黑字,默认 'light'
  gap: [200, 150],              // [水平间距, 垂直间距] px,默认 [200, 150]
  width: 300,                   // Canvas 宽度(默认自动计算)
  height: 200,                  // Canvas 高度(默认自动计算)
  zIndex: 9999,                 // z-index,默认 9999

  // ===== 保护 =====
  protect: true,                // MutationObserver 防篡改,默认 true

  // ===== 身份追踪(仅隐形)=====
  userId: '10001',              // 用户 ID,作为隐形水印载荷(不追加可见文字)

  // ===== 隐形水印(实验性)=====
  invisibleId: false,           // 将 userId hash 嵌入像素域(扩频噪声),截图取证
  stegoDebug: false,            // DEBUG:将隐形水印振幅放大到肉眼可见,验证嵌入逻辑

  // ===== 动态刷新 =====
  dynamic: false,               // 定时刷新水印(日期自动更新)
  interval: 30000,              // 刷新间隔 ms,默认 30s
})

返回值 WatermarkInstance

ts
interface WatermarkInstance {
  update: (options: Partial<WatermarkOptions>) => void
  destroy: () => void
  show: () => void
  hide: () => void
}

wm.update(options)

运行时更新水印。传入部分配置即可,未传的项保持不变:

ts
wm.update({ text: '新水印文字' })
wm.update({ opacity: 0.2, rotate: -45 })
wm.update({ colorScheme: 'light' })

protectdynamicinterval 等也可在 update 中修改,内部会自动同步 protector 和定时器。


wm.destroy()

销毁水印:断开 MutationObserver、清除定时器、移除 DOM 节点。调用后实例不可再用。


wm.show() / wm.hide()

临时显示 / 隐藏水印层。hide() 会同时暂停动态刷新的定时器(避免不可见时无效 Canvas 重绘),show() 会恢复定时器。

ts
wm.hide()
// ... 不需要水印的阶段
wm.show()   // 定时器自动恢复

明暗主题

通过 colorScheme 控制水印文字颜色以适应页面背景色。默认 'light'(白字,适配深色主题):

ts
// 深色背景 → 浅色文字(默认)
createWatermark({ text: '内部资料' })
// 等价于
createWatermark({ text: '内部资料', colorScheme: 'light' })

// 浅色背景 → 深色文字
createWatermark({ text: '内部资料', colorScheme: 'dark' })
colorScheme文字颜色适用场景
'light'(默认)白色 #fff深色页面背景
'dark'黑色 #000浅色页面背景

colorScheme 优先级高于 color——设置了 colorScheme 后会忽略 color 的自定义值。


防篡改保护

protect: true(默认)时,MutationObserver 监控并恢复以下受保护样式:

监测情形行为
水印 DOM 被 remove() 删除自动重新挂载到 body
style.display 被设为 none重置为可见(合法 hide() 除外)
visibility / opacity / zIndex / pointerEvents 被篡改恢复为预期值
backgroundImage 被替换恢复
祖先节点被清空重新挂载

这是提高篡改门槛的前端防护,并非绝对安全——DevTools 完全可控的浏览器环境中无法 100% 防删除。


身份追踪

userId 仅作为隐形水印的载荷使用,不会在水印上追加任何可见文字。如需可见的追踪信息,直接在 text 中写入:

ts
createWatermark({
  text: ['内部资料', '张三', 'ID:6B8A2F'],  // 手动写入追踪文字
  userId: '10001',                           // 仅用于隐形水印
  invisibleId: true,
})

隐形水印(实验性)

invisibleId: true 会将 userId hash 的低 16 位以像素域扩频方式嵌入 Canvas 图块。原理:

  • 将 Canvas 划分为 16×16 px 的 block
  • 每个 block 用确定性伪随机 ±1 噪声(mulberry32 PRNG)叠加到 RGB 通道
  • 振幅仅 3/255,肉眼不可见
  • 生产级 JPEG 重压缩可能破坏此模式

调试stegoDebug: true 将振幅放大到 60/255,block 网格肉眼可见,用于验证嵌入逻辑是否正常工作。切勿在生产环境开启

ts
createWatermark({
  text: '机密',
  userId: '10001',
  invisibleId: true,
  stegoDebug: true,     // ← 仅调试用
})

解码

decodeWatermark 可从截图或图片中反向提取嵌入的 16-bit code:

ts
import { decodeWatermark } from '@bilibaba/ts-lab/ui'

const img = document.querySelector('img.wm-screenshot') as HTMLImageElement
const code = decodeWatermark(img)
if (code !== null) {
  console.log('提取码:', code.toString(16).toUpperCase())
}
参数类型说明
sourceHTMLImageElement | HTMLCanvasElement | ImageData截图或图片源
返回值number | null解码出的 16-bit code,图片太小或非浏览器环境返回 null

限制:需 1:1 原始分辨率的截图;缩放、裁剪偏移、重度 JPEG 压缩后解码可能失败。


动态水印

dynamic: true 时水印按 interval 周期自动刷新。hide() 时暂停定时器,show() 恢复:

ts
const wm = createWatermark({
  text: '机密文件',
  userId: '10001',
  dynamic: true,
  interval: 15000,  // 每 15 秒刷新
})

wm.hide()  // 同时暂停定时器
wm.show()  // 恢复定时器

SSR 安全

非浏览器环境(windowdocument 不可用)时返回 no-op 实例,所有方法调用安全无操作:

ts
// Node / SSR 中安全调用
const wm = createWatermark({ text: 'test' })
wm.update({ text: 'changed' })  // no-op
wm.destroy()                     // no-op