问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

前端水印实现方式详解:背景图、DOM、Canvas和SVG方案对比

创作时间:
作者:
@小白创作中心

前端水印实现方式详解:背景图、DOM、Canvas和SVG方案对比

引用
CSDN
1.
https://blog.csdn.net/xgangzai/article/details/143459205

在日常工作中,我们经常会遇到需要保护敏感数据的情况。为了防止数据泄露,给数据添加水印是一种常见的做法。本文将详细介绍几种前端水印实现方式,包括背景图、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获取图片信息呢?

答案:此操作暂时没有好的解决办法,建议采用后端实现方案。

总结

前端实现的水印方案始终只是一种临时方案,业务后端实现又耗费服务器资源,其实最理想的解决方式就是提供一个独立的水印服务,虽然加载过程中会略有延迟,但是相对与数据安全来说,毫秒级的延迟还是可以接受的,这样又能保证不影响业务的服务稳定性。在每天的答疑过程中,也会有很多业务方来找我沟通水印遮挡风险点的问题,每次只能用数据安全的重要性来回复他们,当然,水印的大小,透明度,密集程度也都在不断的调优中,相信会有一个版本,既能起到水印的作用,也能更好的解决遮挡问题。

希望这篇教程能对大家有所帮助。祝编码快乐!

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号