前端水印实现方式详解:背景图、DOM、Canvas和SVG方案对比
前端水印实现方式详解:背景图、DOM、Canvas和SVG方案对比
在日常工作中,我们经常会遇到需要保护敏感数据的情况。为了防止数据泄露,给数据添加水印是一种常见的做法。本文将详细介绍几种前端水印实现方式,包括背景图、DOM、Canvas和SVG等技术手段,并分析它们各自的优缺点。
业务场景分析
在实际工作中,我们常常需要处理一些敏感数据,比如审核平台上的风险图片。为了防止数据泄露,我们需要在图片上添加水印。考虑到业务场景,现阶段的问题主要是在审核过程中防止数据泄露,因此我们暂时只考虑显式水印,即在图片上增加一些可以区别个人身份的文字或其他数据。这样,一旦数据泄露,我们可以根据水印追查到责任人。
前端水印实现方式
1. 背景图实现全屏水印
原理:使用后端生成的水印图片作为背景图。
优点:水印图片由后端生成,安全性较高。
缺点:需要发起HTTP请求获取水印图片信息。
2. DOM 实现全图水印和图片水印
实现方式:在图片的 onload
事件中获取图片宽高,根据图片大小生成水印区域,遮挡在图片上层,水印内容为文字或其他信息。
代码示例:
const wrap = document.querySelector('#ReactApp');
const { clientWidth, clientHeight } = wrap;
const waterHeight = 120;
const waterWidth = 180;
// 计算个数
const [columns, rows] = [~~(clientWidth / waterWidth), ~~(clientHeight / waterHeight)]
for (let i = 0; i < columns; i++) {
for (let j = 0; j <= rows; j++) {
const waterDom = document.createElement('div');
// 动态设置偏移值
waterDom.setAttribute('style', `
width: ${waterWidth}px;
height: ${waterHeight}px;
left: ${waterWidth + (i - 1) * waterWidth + 10}px;
top: ${waterHeight + (j - 1) * waterHeight + 10}px;
color: #000;
position: absolute`
);
waterDom.innerText = '测试水印';
wrap.appendChild(waterDom);
}
}
优点:实现简单。
缺点:图片过大或过多时可能会影响性能。
3. Canvas 实现方式
方法一:直接在图片上操作
代码示例:
useEffect(() => {
// gif 图不支持
if (src && src.includes('.gif')) {
setShowImg(true);
}
image.onload = function () {
try {
// 太小的图不加载水印
if (image.width < 10) {
setIsDataError(true);
props.setIsDataError && props.setIsDataError(true);
return;
}
const canvas = canvasRef.current;
canvas.width = image.width;
canvas.height = image.height;
// 设置水印
const font = `${Math.min(Math.max(Math.floor(innerCanvas.width / 14), 14), 48)}px` || fontSize;
innerContext.font = `${font} ${fontFamily}`;
innerContext.textBaseline = 'hanging';
innerContext.rotate(rotate * Math.PI / 180);
innerContext.lineWidth = lineWidth;
innerContext.strokeStyle = strokeStyle;
innerContext.strokeText(text, 0, innerCanvas.height / 4 * 3);
innerContext.fillStyle = fillStyle;
innerContext.fillText(text, 0, innerCanvas.height / 4 * 3);
const context = canvas.getContext('2d');
context.drawImage(this, 0, 0);
context.rect(0, 0, image.width || 200, image.height || 200);
// 设置水印浮层
const pattern = context.createPattern(innerCanvas, 'repeat');
context.fillStyle = pattern;
context.fill();
} catch (err) {
console.info(err);
setShowImg(true);
}
};
image.onerror = function () {
setShowImg(true);
};
}, [src]);
优点:纯前端实现,右键复制的图片也有水印。
缺点:不支持GIF,图片必须支持跨域。
方法二:Canvas 生成水印 URL 赋值给 CSS background 属性
代码示例:
export const getBase64Background = (props) => {
const { nick, empId } = GlobalConfig.userInfo;
const {
rotate = -20,
height = 75,
width = 85,
text = `${nick}-${empId}`,
fontSize = '14px',
lineWidth = 2,
fontFamily = 'microsoft yahei',
strokeStyle = 'rgba(255, 255, 255, .15)',
fillStyle = 'rgba(0, 0, 0, 0.15)',
position = { x: 30, y: 30 },
} = props;
const image = new Image();
image.crossOrigin = 'Anonymous';
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = width;
canvas.height = height;
context.font = `${fontSize} ${fontFamily}`;
context.lineWidth = lineWidth;
context.rotate(rotate * Math.PI / 180);
context.strokeStyle = strokeStyle;
context.fillStyle = fillStyle;
context.textAlign = 'center';
context.textBaseline = 'hanging';
context.strokeText(text, position.x, position.y);
context.fillText(text, position.x, position.y);
return canvas.toDataURL('image/png');
};
// 使用方式
<img src="https://xxx.xxx.jpg" />
<div className="warter-mark-area" style={{ backgroundImage: `url(${getBase64Background({})})` }} />
优点:纯前端实现,支持跨域,支持GIF图水印。
缺点:生成的Base64 URL较大。
4. SVG 方式
代码示例:
export const WaterMark = (props) => {
// 获取水印数据
const { nick, empId } = GlobalConfig.userInfo;
const boxRef = React.createRef();
const [waterMarkStyle, setWaterMarkStyle] = useState('180px 120px');
const [isError, setIsError] = useState(false);
const {
src, text = `${nick}-${empId}`, height: propsHeight, showSrc, img, nick, empId
} = props;
// 设置背景图和背景图样式
const boxStyle = {
backgroundSize: waterMarkStyle,
backgroundImage: `url("data:image/svg+xml;utf8,<svg width='100%' height='100%' xmlns='http://www.w3.org/2000/svg' version='1.1'><text width='100%' height='100%' x='20' y='68' transform='rotate(-20)' fill='rgba(0, 0, 0, 0.2)' font-size='14' stroke='rgba(255, 255, 255, .2)' stroke-width='1'>${text}</text></svg>")`,
};
const onLoad = (e) => {
const dom = e.target;
const {
previousSibling, nextSibling, offsetLeft, offsetTop,
} = dom;
// 获取图片宽高
const { width, height } = getComputedStyle(dom);
if (parseInt(width.replace('px', '')) < 180) {
setWaterMarkStyle(`${width} ${height.replace('px', '') / 2}px`);
};
previousSibling.style.height = height;
previousSibling.style.width = width;
previousSibling.style.top = `${offsetTop}px`;
previousSibling.style.left = `${offsetLeft}px`;
// 加载 loading 隐藏
nextSibling.style.display = 'none';
};
const onError = (event) => {
setIsError(true);
};
return (
<div className={styles.water_mark_wrapper} ref={boxRef}>
<div className={styles.water_mark_box} style={boxStyle} />
{isError
? <ErrorSourceData src={src} showSrc={showSrc} height={propsHeight} text="图片加载错误" helpText="点击复制图片链接" />
: (
<>
<img onLoad={onLoad} referrerPolicy="no-referrer" onError={onError} src={src} alt="图片显示错误" />
<Icon className={styles.img_loading} type="loading" />
</>
)
}
</div>
);
};
优点:支持GIF图水印,不存在跨域问题,使用 repeat
属性,无插入DOM过程,无性能问题。
效果图展示
Canvas和SVG实现的效果在展示上没有很大的区别,因此这里只展示一张图。
常见问题解答
问题一:如果把 watermark 的 dom 删除了,图片不就是无水印了吗?
答案:可以利用 MutationObserver
监听 water 的节点,如果节点被修改,图片也随之隐藏。
问题二:鼠标右键复制图片?
答案:全部的图片都禁用了右键功能。
问题三:如果从控制台的network获取图片信息呢?
答案:此操作暂时没有好的解决办法,建议采用后端实现方案。
总结
前端实现的水印方案始终只是一种临时方案,业务后端实现又耗费服务器资源,其实最理想的解决方式就是提供一个独立的水印服务,虽然加载过程中会略有延迟,但是相对与数据安全来说,毫秒级的延迟还是可以接受的,这样又能保证不影响业务的服务稳定性。在每天的答疑过程中,也会有很多业务方来找我沟通水印遮挡风险点的问题,每次只能用数据安全的重要性来回复他们,当然,水印的大小,透明度,密集程度也都在不断的调优中,相信会有一个版本,既能起到水印的作用,也能更好的解决遮挡问题。
希望这篇教程能对大家有所帮助。祝编码快乐!