浅谈明暗水印

news2024/9/22 17:19:20

前言

水印(Watermark)是一种能让人识别纸上图案的技术,当光线照射纸张时,纸张上会显现出各种不同阴影,这些阴影组成的图案就是水印。

水印常常起到验证货币、护照、邮票、政府文件或者其他纸制文件的真实性的作用。

随着互联网的发展,我们经常在网页或者 App 上看到一些具有独特设计的文字或者图片,并且下载后依旧会存在于其中,这种常见的标识叫做水印。

随着人们版权意识的觉醒,越来越多的人为了防止信息泄露或知识产权被侵犯,人们会在发布的文章、文件、图片、音频、视频中添加水印。

水印内容可以包含多种编码后的信息,包括用户名、用户ID、时间等。比如我们只是想保存用户唯一的用户ID,需要把用户ID 用 md5 方法加密,就可以生成唯一标识。编码后的信息是不可逆的,但可以通过全局遍历所有用户的方式进行追溯,增加敏感数据外泄的门槛,对用户起到很好威慑作用;同时水印也能够很好的证明数字产品的版权所在, 能够作为盗版维权时的有力证据。

本文重点介绍前端如何使用程序合成手段添加水印。

明水印

明水印即可见水印,在图像、视频上添加的 Logo、Id 等特定标识信息,非常容易辨识。

基于DOM实现水印效果

效果:在页面上充满透明度较低的重复的代表身份的信息。

  • 重复的dom元素覆盖实现

在页面上覆盖一个 position: fixed 的 div 盒子,在这个盒子内通过 js 循环生成小的水印 div,每个水印 div 内展示一个要显示的水印内容;

样式上盒子透明度设置较低,设置 pointer-events: none 样式实现点击穿透,设置 user-select: none 让文字无法被选中。

function loadWatermark(width, height, content) {const box = document.getElementById('watermark-box');const boxWidth = box.clientWidth;const boxHeight = box.clientHeight;for (let i = 0; i < Math.floor(boxHeight / height); i++) {for (let j = 0; j < Math.floor(boxWidth / width); j++) {const item = document.createElement('div');item.style.width = width + 'px';item.style.height = height + 'px';item.innerText = content;item.setAttribute('class', 'watermark');box.appendChild(item);}}
}
window.onload = loadWatermark(300, 100, 'watermark'); 
  • 背景图实现

canvas 的实现很简单,主要是利用 canvas 绘制一个水印,然后将它转化为 base64 的图片,通过 canvas.toDataURL() 来拿到文件流的 url ,然后将获取的 url 填充在一个元素的背景中,然后我们设置背景图片的属性为重复。

svg 与 canvas 生成背景图的方法类似,只不过是生成背景图的方法换成了通过 svg 生成,canvas 的兼容性略好于 svg。

// canvas生成图片
function createWatermark(width, height, content) {const canvas = document.createElement('canvas');canvas.width = width;canvas.height = height;const ctx = canvas.getContext('2d');ctx.clearRect(0, 0, width, height); // 清除指定的矩形区域,然后这块区域会变的完全透明ctx.fillStyle = '#000';ctx.globalAlpha = 0.2;ctx.font = '16px serif';const angle = -15;ctx.rotate(Math.PI / width * angle);ctx.fillText(content, 0, 50);return canvas.toDataURL();
}
// svg 生成图片
function createWatermark(width, height, content) {const svgStr = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}px" height="${height}px"><text x="0px" y="30px" dy="16px"text-anchor="start"stroke="#000"stroke-opacity="0.2"fill="none"transform="rotate(-15)"font-weight="100"font-size="16">${content}</text></svg>`;// window.btoa 返回一个 base-64 编码的字符串 encodeURIComponent 将字符转换成utf-8格式的编码unescape 解码return `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svgStr)))}`;
}
// 创建元素设置背景图
const watermark = document.createElement('div');
watermark.className = 'watermark';
watermark.style.backgroundImage = `url(${createWatermark(180, 100, 'watermark')})`; 

这种水印方法存在一个问题,由于是前端生成 dom 元素覆盖到页面上的,对于知道控制台操作的人,可以在开发者工具中找到水印所在的元素,将元素整个删掉,水印就消失了,那么有什么办法能防御住这样的操作呢?

明水印的防御

Mutation Observer API 用来监视 DOM 变动,DOM 的任何变动,比如子节点的增减、属性的变动、文本内容的变动,这个 API 都可以得到通知。

Mutation Observer 有以下特点。

  • 它等待所有脚本任务完成后,才会运行(即异步触发方式)。
  • 它把 DOM 变动记录封装成一个数组进行处理,而不是一条条个别处理 DOM 变动。
  • 它既可以观察 DOM 的所有类型变动,也可以指定只观察某一类变动。

注意:MutationObserver 只能监测到诸如属性改变、子结点变化等,对于自己本身被删除,是没有办法监听的,但是可以通过监测父结点来达到要求。

我们要实现效果主要观察的有三点

  • 水印元素本身是否被移除
  • 水印元素属性是否被篡改(display: none …)
  • 水印元素的子元素是否被移除和篡改 (element 生成的方式 )
(function () {function __canvasWM({container = document.body,content = 'watermark',...} = {}) {const base64Url = createWatermark(content); // 生成水印图片const __wm = document.querySelector('.__wm');const watermarkDiv = __wm || document.createElement('div');const styleStr = `position:fixed;...background-repeat:repeat;background-image:url('${base64Url}')`;watermarkDiv.setAttribute('style', styleStr);watermarkDiv.classList.add('__wm');if (!__wm) {container.style.position = 'relative';container.insertBefore(watermarkDiv, container.firstChild);}const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;if (MutationObserver) {let mo = new MutationObserver(function () {const __wm = document.querySelector('.__wm');// 只在__wm元素变动才重新调用 __canvasWMif ((__wm && __wm.getAttribute('style') !== styleStr) || !__wm) {// 避免一直触发 关闭监听mo.disconnect();mo = null;__canvasWM(JSON.parse(JSON.stringify(args)));}});mo.observe(container, {attributes: true,subtree: true,childList: true});}};window.__canvasWM = __canvasWM;
})(); 

当然,设置了 MutationObserver 之后也只是相对安全了一些,还是可以通过控制台禁用 js复制 dom 元素、删除 水印相关的代码等方法来跳过我们的监听,总体来说在单纯的在前端页面上加水印总是可以通过一些骚操作来跳过的,防外行不防内行。

图片加水印

有时我们需要在图片上加水印用来标示归属或者其他信息,在图片上加水印的实现思路是,图片加载成功后画到 canvas 中,随后在 canvas 中绘制水印,完成后通过 canvas.toDataUrl() 方法获得 base64 并替换原来的图片路径。

(function () {function __picWM({url = '',content = 'watermark',cb = null,...} = {}) {const img = new Image();img.src = url;img.crossOrigin = 'anonymous';img.onload = function () {const canvas = document.createElement('canvas');canvas.width = img.width;canvas.height = img.height;const ctx = canvas.getContext('2d');ctx.drawImage(img, 0, 0);ctx.fillStyle = fillStyle;ctx.fillText(content, img.width - textX, img.height - textY);const base64Url = canvas.toDataURL();cb && cb(base64Url);}}window.__picWM = __picWM;
})();
__picWM({url: 'https://pics4.baidu.com/feed/e61190ef76c6a7efb6ec01e0373d0157f1de6678.jpeg?token=c330d12246bdbb2fc320a687e35e7662&s=1414ED37191276C20A7CD2FC03005027',content: 'vx_xxx',cb: (base64Url) => {document.querySelector('img').src = base64Url;},
}); 

暗水印

暗水印是一种肉眼不可见的水印,能够很好的证明数字产品的版权所在, 能够作为盗版维权时的有力证据。

暗水印的特性

  • 隐蔽性 由于不希望被察觉、不希望干扰用户体验、不希望被模仿等原因,我们的水印不可见,也就是隐匿性。
  • 不易移除性 不易移除性跟鲁棒性有些相似, 不同的是:鲁棒性更加强调的是数字资源在传播过程中不要被不自觉地干扰和破坏。不易移除性是在别有用心者察觉了暗水印的存在后,不被他们自觉地移除或者破坏。
  • 强健性 强健性通常也被称作鲁棒性,一般要能抗(压缩 、裁剪、涂画、旋转);需要说明的一点是,鲁棒性和隐蔽性通常不可兼得。
  • 明确性 暗水印需要表示出明确的信息。

暗水印的生成方式有很多,常见的为RGB 分量值的小量变动、离散傅里叶变换(DFT) 、离散余弦变换(DCT) 和离散小波变换(DWT)等方法。

图片加隐性水印

本文主要介绍前端实现RGB 分量值的小量变动的思路。

图片的像素信息里存储着 RGB 的色值,对于RGB 分量值的小量变动,是肉眼无法分辨的,不会影响对图片的识别,我们可以对图片的 RGB 以一种特殊规则进行小量的改动。

首先我们需要创建一个规律,通过我们的规律将图片编码生成带隐形水印的图片;现在假设这个规律为,我们将所有像素的 R 通道的值为奇数的时候就是我们创建的通道密码(遍历原图片像素,将对应水印像素有信息的像素的 R 通道值都转成奇数,对应水印像素没有信息的像素 R 通道值都转成偶数),举个简单的例子:

例如我们把它当做是一个图形,每个格子是图片的像素点,格子里的数字是像素的 R 通道的值,现在我们要一个 “Z” 字母最大比例放进图像,按照我们的算法规则,我们的图像会变成下图的样子:

解码的时候,我们拿到所有的奇数像素将它渲染出来,正好是个 “Z” 字母,我们看下代码具体实现:

// 编码过程

<canvas id="canvas" width="220" height="220"></canvas>

//水印的通道密码规则: 将所有像素的 R 通道的值设为奇数
const originalUrl = 'https://img1.baidu.com/it/u=649078162,2200738056&fm=253&fmt=auto&app=138&f=JPEG?w=220&h=220';
function mergeData(ctx, markData, color, imgData) {let oData = imgData.data;for (let i = 0; i < oData.length; i++) {let bit;let offset;// offset的作用是找到alpha通道值switch (color) {case 'R':bit = 0;offset = 3;break;case 'G':bit = 1;offset = 2;break;case 'B':bit = 2;offset = 1;break;}// 处理 R 目标通道信息if (i % 4 === bit) {// 没有水印信息的像素,将其对应通道的值设置为偶数if (markData[i + offset] === 0 && (oData[i] % 2 === 1)) { // 水印像素 R 通道没有值 && 图片像素 R 通道值为奇数if (oData[i] === 255) {oData[i]--;} else {oData[i]++;}// 有水印信息的像素,将其对应通道的值设置为奇数} else if (markData[i + offset] !== 0 && (oData[i] % 2 === 0)) {oData[i]++;}}}ctx.putImageData(imgData, 0, 0);return document.getElementById('canvas').toDataURL();
}
function getMarkImage(url) {const canvas = document.getElementById('canvas');const ctx = canvas.getContext('2d');ctx.font = '40px Microsoft Yahei';ctx.fillText('watermark', 0, 80);const markData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data;const img = new Image();img.src = url;img.crossOrigin = 'anonymous';img.onload = function () {ctx.drawImage(img, 0, 0);const imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);// 水印合并到图片上const newUrl = mergeData(ctx, markData, 'R', imgData);// 生成新的隐形图片const invisibleMaskImg = new Image();invisibleMaskImg.src = newUrl;invisibleMaskImg.id = 'newImg';document.querySelector('.img-box2').appendChild(invisibleMaskImg);};
}
getMarkImage(originalUrl); 
// 解码过程

var processData = function (ctx, color, originalData) {let data = originalData.data;let bit;switch (color) {case 'R':bit = 0;break;case 'G':bit = 1;break;case 'B':bit = 2;break;}for (let i = 0; i < data.length; i++) {if (i % 4 === bit) {// R通道if (data[i] % 2 === 0) {data[i] = 0;} else {data[i] = 255;}} else if (i % 4 === 3) { // alpha 通道,不用处理continue;} else { // G B 通道data[i] = 0;}}ctx.putImageData(originalData, 0, 0);
};
function getMarkData() {const ctx = document.getElementById('maskCanvas').getContext('2d');const img = new Image();const newImg = document.getElementById('newImg').src;img.src = newImg;img.crossOrigin = 'anonymous';img.onload = function () {ctx.drawImage(img, 0, 0);const originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);processData(ctx, 'R', originalData);};
} 

正常情况下看这个图片是没有水印的,但是经过对应规则(上边例子对应的解密规则是:遍历图片的像素数据中对应的 R,奇数则将其 rgba 设置为(255,0,0,0),偶数则设置为(0,0,0,0)的解密处理后就可以看到水印了。

RGB 分量值的小量变动是一种比较简单的加密方式,当用户采用截图、旋转、保存图片后转换格式等方法获得图片后,图片的色值可能是会变化的,会影响水印效果。

在实际过程需要更专业的加密方式,例如利用傅里叶变化公式,来进行频域制定数字暗水印,大家可以去深入学习下,这里就不阐述了。

总结

本文主要介绍了明暗水印比较简单的实现方式,在实际的应用场景中,我们可以通过组合使用水印的方案,这样能最大程度给浏览者警示的作用,减少泄密的情况,即使泄密了,也有可能追踪到泄密者。

最后

最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/341598.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

什么是热迁移?90%的企业都理解错误

科技的发展&#xff0c;新冠的冲击&#xff0c;让市场竞争愈发激烈。尽管云计算服务为企业免除了基础硬件的建设和维护成本&#xff0c;当企业需要进行业务跨架调整、升级维护、环境测试等场景而进行云迁移&#xff0c;其过程中所带来的停机时间&#xff0c;就变得尤为头疼了。…

清亡之路(4):最受误解的东南互保

很多人一提“东南互保”&#xff0c;就认为是东南是在反叛。如果仔细看&#xff0c;其实根本谈不上造反&#xff0c;反而是更像是一种“遵旨行事”。本文就是说说这个问题。宣战是来真的吗&#xff1f;1900年6月21日&#xff0c;慈禧忍无可忍&#xff0c;决定和各公使馆翻脸&am…

给你的边框加点渐变

目录前言border-imageborder-image实现background父子divbackgorund一个div一个伪元素background-clip&#x1f9e8;&#x1f9e8;&#x1f9e8; 大家好&#xff0c;我是搞前端的半夏 &#x1f9d1;&#xff0c;一个热爱写文的前端工程师 &#x1f4bb;. 如果喜欢我的文章&…

spring cloud

文章目录 目录 文章目录 前言 一、spring cloud 二、ribbon负载均衡 三、openfeign 总结 前言 微服务就是一种将一个单一应用程序拆分为一组小型服务的方法&#xff0c;拆分完成后&#xff0c;每一个服务都运行在独立的进程中&#xff0c;服务于服务之间采用轻量级的通信机制来…

我不允许你还不知道CSS的filter的drop-shadow阴影用法以及与box-shadow的区别详解

这里有两个图片的阴影&#xff0c;你觉得哪个好看&#xff1f; 一个是使用box-shadow另一个是使用filter: drop-shadow 一、我们来了解一下CSS的filter&#xff08;过滤器&#xff09; 该CSS的filter属性可以实现很多效果 &#xff08;一&#xff09;filter: blur(5px) // 高…

Elasticsearch索引库和文档的相关操作

前言&#xff1a;最近一直在复习Elasticsearch相关的知识&#xff0c;公司搜索相关的技术用到了这个&#xff0c;用公司电脑配了环境&#xff0c;借鉴网上的课程进行了总结。希望能够加深自己的印象以及帮助到其他的小伙伴儿们&#x1f609;&#x1f609;。 如果文章有什么需要…

if从入门到出轨

if从入门到出轨(java版本) 为什么会产生很多if分支 在我们的日常生活中,会遇到很多判断逻辑,屁如,如果你在2月14号,心情很好,那么就给女朋友买了个iPhone 14 Pro Max 1TB 银白色,如果你女朋友在2月14号没有收到您老人家的礼物,那么你可能睡沙发或者轨搓衣板,或者直接和其他帅…

【Kafka】【十九】新消费组的消费offset规则

新消费组的消费offset规则 新消费组中的消费者在启动以后&#xff0c;默认会从当前分区的最后⼀条消息的offset1开始消费&#xff08;消费新消息&#xff09;。可以通过以下的设置&#xff0c;让新的消费者第⼀次从头开始消费。之后开始消费新消息&#xff08;最后消费的位置的…

电脑分盘怎么合并?只需1分钟,轻松学会

有些小伙伴喜欢将电脑进行分盘&#xff0c;以此将文件放进不同的分盘进行管理。但有时候&#xff0c;电脑磁盘分盘过多&#xff0c;管理起来又会有些麻烦。将一些闲置的磁盘进行合并很有必要。电脑分盘怎么合并&#xff1f;下面就跟着小编一起来看看吧。 电脑分盘怎么合并&…

Java 变量和数据类型,超详细整理,适合新手入门

目录 一、什么是变量&#xff1f; 二、变量 变量值互换 三、基本数据类型 1、八种基本数据类型 2、布尔值 3、字符串 四、从控制台输入 一、什么是变量&#xff1f; 变量是一种存储值的容器&#xff0c;它可以在程序的不同部分之间共享&#xff1b;变量可以存储数字、字…

二维数组的定义

1. 概念二维数组就是一种数组的数组&#xff0c;其本质上还是一个一维数组&#xff0c;只是它的数据元素又是一个一维数组。如果你对这个概念想象不出来&#xff0c;给大家举个栗子&#xff0c;相信吸烟的同学一下子就会明白。一根烟 一个变量一包烟 20根烟 一维数组一条烟 …

TIA博途中DB数据块清零的具体方法示例

TIA博途中DB数据块清零的具体方法示例 TIA中数据块如何实现清零? 在TIA指令集内有多个移动指令可对DB块内数据进行清零处理。对于S7-1500 CPU或ET200SP CPU来说,可使用BLKMOV、FILL以及SCL的POKE_BLK指令。但是这些指令对DB块清零时,要求DB块必需为非优化DB。 对于优化的DB…

国内ChatGPT日趋成熟后,可以优先解决的几个日常小问题

现在ChatGPT的发展可谓如日中天&#xff0c;国内很多大的公司例如百度、京东等也开始拥抱新技术&#xff0c;推出自己的应用场景&#xff0c;但可以想象到的是&#xff0c;他们必定利用这个新技术在巩固自己的现有应用场景&#xff0c;比如某些客服&#xff0c;你都不用想&…

Android 进阶——Framework 核心之Binder 对象及其生命周期小结(四)

文章大纲引言一、Binder概述二、Binder 对象三、Binder 对象生命周期的管理1、Binder本地对象&#xff08;BBinder&#xff09;的生命周期管理2、Binder 实体对象&#xff08;binder_node&#xff09;生命周期的管理3、Binder 引用对象&#xff08;binder_ref&#xff09;生命周…

ChatGPT入门案例|商务智能对话客服(一)

ChatGPT是人工智能研究实验室OpenAI新推出的一种人工智能技术驱动的自然语言处理工具&#xff0c;使用了Transformer神经网络架构&#xff0c;也是GPT-3.5架构&#xff0c;这是一种用于处理序列数据的模型&#xff0c;拥有语言理解和文本生成能力&#xff0c;尤其是它会通过连接…

32个关于FPGA的学习网站

语言类学习网站 1、HDLbits 网站地址&#xff1a;https://hdlbits.01xz.net/wiki/Main_Page 在线作答、编译的学习Verilog的网站&#xff0c;题目很多&#xff0c;内容丰富。非常适合初学Verilog的人&#xff01;&#xff01;&#xff01; 2、牛客网 网站地址&#xff1a;http…

2.12、进程互斥的软件实现方法

学习提示: 理解各个算法的思想、原理结合上小节学习的 “实现互斥的四个逻辑部分”&#xff0c;重点理解各算法在进入区、退出区都做了什么分析各算法存在的缺陷&#xff08;结合 “实现互斥要遵循的四个原则” 进行分析&#xff09; 1、单标志法 算法思想&#xff1a;两个进…

SonicWall:请立即修复SMA 1000 漏洞

近日&#xff0c;网络安全供应商SonicWall发布了关于安全移动访问 (SMA) 1000设备的三个安全漏洞的紧急报告&#xff0c;其中包括一个高威胁性的身份验证绕过漏洞。SonicWall指出&#xff0c;攻击者可以利用这些漏洞绕过授权&#xff0c;并可能破坏易受攻击的设备。 从报告中可…

Cow Acrobats ( 临项交换贪心 )

题目大意&#xff1a; N 头牛 &#xff0c; 每头牛有一个重量(Weight)和一个力量(Strenth) &#xff0c; N头牛进行排列 &#xff0c; 第 i 头牛的风险值为其上所有牛总重减去自身力量 &#xff0c; 问如何排列可以使最大风险值最小 &#xff0c; 求出这个最小的最大风险值&am…

Java JCP

Java JCP目录概述需求&#xff1a;设计思路参考资料和推荐阅读Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip hardness,make a better result,wait for change,challenge Survive. happy for hardess to solv…