上一篇我们讲到前端水印实现的几种方式,这一篇我们继续讨论一下如何防止水印被用户恶意去除水印的问题。
在大多数情况下,我们一般都采用 Canvas 实现水印,下面我们就基于此方案上进行讨论,其他方案的防破解策略,本质上是一样的,围绕的核心就是实现水印元素本身的增删改等操作。
因此,在实现 Canvas 水印时,我们需要面对 Canvas 元素属性修改、内容篡改、元素删除、图层覆盖以及感知性问题。本文将逐步探讨每个问题的解决方案,并提供关键的代码片段。
一、水印的防篡改策略
1. 直接删除水印元素
用户发现了水印元素,直接删除 Canvas 元素,从而去除水印,这种方式最简单直接。
面对这种情况,该如何解决呢?
思路:我们可以监听水印元素,发现它被删除后,立刻使内容隐藏或者重新建立水印元素。
因此,我们可以使用 MutationObserver
监控 DOM 变动,确保 Canvas 元素在被删除后能够自动重新创建。
function setupMutationObserver() {
const targetNode = document.querySelector('.watermarked-content');
const observer = new MutationObserver(() => {
// 重新建立水印元素
createWatermarkCanvas();
});
observer.observe(targetNode, { childList: true, subtree: true });
}
2. 修改Canvas 元素属性
第二种场景,既然不能删除,那我就尝试修改它的属性,使它失效。
用户可能通过 CSS 修改 Canvas 元素的属性,或者直接在 HTML 上进行编辑 style 修改,使其隐藏,导致水印不显示。
解决方案:
依然使用 MutationObserver
监听 Canvas 元素的属性变化,确保其始终可见。核心代码如下:
function ensureCanvasVisibility() {
const canvas = document.querySelector('.watermark-canvas');
if (canvas) {
const style = window.getComputedStyle(canvas);
// 确保 Canvas 始终可见
if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) === 0) {
canvas.style.display = 'block';
canvas.style.visibility = 'visible';
canvas.style.opacity = '1';
createWatermarkCanvas(); // 重新绘制水印
}
}
}
function setupAttributeObserver() {
const targetNode = document.querySelector('.watermarked-content');
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
ensureCanvasVisibility();
}
});
});
observer.observe(targetNode, { attributes: true, subtree: true });
}
3. 覆盖水印图层
第三种情况就是,既然不能删除它,也不能修改它的属性,那就想办法绕过它,转而围攻它的相关的节点,比如它的兄弟节点。
比如修改它的兄弟节点,提升它的图层,使它覆盖 Canvas 元素,从而遮挡水印。如果用户修改 z-index
,也可能导致水印无法显示。
解决方案:
确保 Canvas 始终位于最上层,通过 CSS 和 JavaScript 维护 Canvas 的 z-index
。
核心代码:
function ensureCanvasOnTop(defaultMaxZIndex = 1000) {
const canvas = document.querySelector('.watermark-canvas');
if (canvas) {
// 获取所有元素的最高 z-index
let maxZIndex = Array.from(document.querySelectorAll('body *'))
.map(el => parseFloat(window.getComputedStyle(el).zIndex))
.filter(zIndex => !isNaN(zIndex))
.reduce((max, zIndex) => Math.max(max, zIndex), defaultMaxZIndex);
// 只有在发现新的最大 z-index 超过默认值时,才更新 canvas 的 z-index
if (maxZIndex >= defaultMaxZIndex) {
canvas.style.zIndex = maxZIndex + 1;
}
}
}
function setupLayerObserver() {
const targetNode = document.querySelector('.watermarked-content');
if (targetNode) {
const observer = new MutationObserver(() => {
ensureCanvasOnTop();
});
observer.observe(targetNode, { attributes: true, childList: true, subtree: true });
}
}
// 初始调用,确保 Canvas 一开始就在最上层,使用默认 z-index
ensureCanvasOnTop();
// 设置观察器,监听 DOM 变化
setupLayerObserver();
通过使用 MutationObserver
监听 Canvas 水印的属性和内容变化,可以有效提高水印的防破解能力。效果图如下:
通过这些策略,可以有效提高 Canvas 水印的安全性,防止其被恶意篡改或删除。
二、暗水印
上面描述的基本都是明水印,也就是用户看得到的,但它也有一定的缺陷,明水印有时会影响用户的体验,特别是一些敏感场合,阻碍用户的阅读。用户感知的情况下,可能会截取没有水印的区域,将绝密文件泄露出去。这时候,暗水印就开始发挥它的作用了。
1. 概念
暗水印(隐形水印)是一种将标识信息嵌入到图像中,而不显著影响图像外观的技术。在处理涉密场景时,添加暗印(水印)可以有效防止内容被未授权访问。
常见的实现暗水印的方式有:
1)透明度隐藏法:原理就是通过改变明水印的透明度,使其极小肉眼不可见,隐藏在网页中,等到要恢复显示时,再利用rgba改变它的色道和透明度,从而实现恢复显示水印内容。
2)最低有效位(LSB)嵌入法:通过修改图像每个像素的最低有效位来嵌入水印。这种方法对图像的可见度影响较小,但可以有效地隐藏信息。
3)层叠透明水印 :在网页中使用多个透明水印层,这些水印层可以跨越多个区块。这种方式通常将水印与内容分离,减少噪声的影响。
4)奇异值分解(SVD) :通过对图像进行奇异值分解,将水印嵌入到奇异值矩阵中。奇异值的微小变化不会显著改变图像外观。
5)嵌入于色彩通道:将水印信息嵌入到图像的某一个色彩通道中(如蓝色通道),因为人的视觉系统对蓝色通道的敏感度较低。等到恢复显示时,再通过特点的算法,进行提取和还原,即可拿到水印内容。
6)结构化水印:利用图像中的某些结构特征(如边缘、纹理)来嵌入水印。可以通过改变图像的纹理模式来隐蔽水印。
2. 暗水印的加码
上述几种办法,这里我们采用层叠透明水印注入的方式,将绘制的水印与内容分离,恢复时减少背景噪声的影响,从而达到更好的恢复显示效果。
使用 HTML5 Canvas 的 globalCompositeOperation
属性可以控制如何将图层合成。通过这种方法,我们可以将水印图层与内容图层分离,并控制它们的合成方式。如果对这个 API 感兴趣的童鞋,可前往developer mozilla了解它。
下面,通过 Canvas 绘制隐蔽的水印信息,使用低透明度或隐藏在图像中的方式实现暗印效果。
核心代码:
function drawWatermark(text = 'user 123', opacity = 0.005) {
const canvas = document.getElementById('watermarkCanvas');
const ctx = canvas.getContext('2d');
const width = window.innerWidth;
const height = window.innerHeight;
canvas.width = width;
canvas.height = height;
ctx.clearRect(0, 0, width, height);
ctx.font = '4em Arial';
ctx.fillStyle = `rgba(0, 0, 0, ${opacity})`; // Semi-transparent color
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.globalCompositeOperation = 'source-over'; // Default
// Draw multiple layers of watermark
for (let y = 0; y < height; y += 150) {
for (let x = 0; x < width; x += 150) {
ctx.save();
ctx.translate(x, y);
ctx.rotate((-30 * Math.PI) / 180); // Rotate for better effect
ctx.fillText(text, 0, 0);
ctx.restore();
}
}
}
实现效果如下:
上述其实已经将我们的水印内容注入到页面当中了,默认注入 user 123
的暗水印,用户肉眼不可见。
了解了暗水印的注入,那如何提取暗水印的内容呢?
3. 暗水印的解码
假设将上面这个截图发送出去,若要从截图中显示暗印,可以使用图像处理工具或脚本来增强暗印的可见性。如下:
<div class="content">
<input type="file" id="upload" accept="image/*" />
<canvas id="canvas" width="500" height="500"></canvas>
<!-- Fixed canvas size -->
</div>
<script>
document.getElementById('upload').addEventListener('change', handleDecode);
function handleDecode(event) {
const file = event.target.files[0];
const img = new Image();
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const reader = new FileReader();
reader.onload = function (e) {
img.src = e.target.result;
};
img.onload = function () {
// 重复绘制的次数
const deCount = 10;
// const canvasWidth = 500;
// const canvasHeight = 500;
const canvasWidth = canvas.width || img.width;
const canvasHeight = canvas.height || img.height;
// Clear the canvas
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
// Calculate aspect ratio and scaling
const scaleWidth = canvasWidth / img.width;
const scaleHeight = canvasHeight / img.height;
const scale = Math.min(scaleWidth, scaleHeight);
// Calculate positioning to center the image
const x = canvasWidth / 2 - (img.width / 2) * scale;
const y = canvasHeight / 2 - (img.height / 2) * scale;
// Draw the image on the fixed-size canvas
ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
// Enhance the hidden watermark
ctx.save();
ctx.globalCompositeOperation = 'overlay'; // Enhance watermark visibility
ctx.fillStyle = '#000'; // Semi-transparent color for enhancement
for (let i = 0; i < deCount; i++) {
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
}
ctx.restore();
};
reader.readAsDataURL(file);
}
</script>
效果图如下:
user 123
就是暗水印内容,但上面的正文内容和暗水印内容都在一起了,不是说水印和正文分离吗?这里说的是解密的过程中,水印是跟正文分离了,如下,ctx内容(暗印内容)重复绘制了10次,次数越多,内容就越清晰。
// 重复绘制的次数
const deCount = 10;
...
ctx.globalCompositeOperation = 'overlay';
for (let i = 0; i < deCount; i++) {
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
}
...
至此,一般水印的内容就基本结束了,但有时我们需要机器帮我们提取暗印的内容。这时,我们就需要用到ocr技术了。
这里是完整的项目地址。
三、扩展
一般场景下OCR技术的解决方案有两种,一种是调用公司的OCR服务接口,另一种是调用第三方云服务的OCR接口,比如阿里、腾讯、华为等。
然而调用第三方的会产生一定的费用,如果调用比较频繁的话,可以考虑自己建设OCR服务。
开源项目
目前比较流行的两个开源OCR库,分别为 Tesseract 和 OCRopus。
-
Tesseract:
-
适用于一般的OCR任务,尤其是对于印刷文本、简单排版的文档。推荐用于需要跨平台、易用、快速部署的场景。
-
GitHub: Tesseract OCR
-
-
OCRopus:
-
适用于需要处理复杂排版、手写体或自定义模型的OCR任务,适合有较高灵活性和精度要求的应用。
-
GitHub: OCRopus OCR
-
官方文档: OCRopus Documentation
-
两者有不同的应用场景,区别总结如下:
特性 | Tesseract | OCRopus |
---|---|---|
开源性 | 免费开源,由 Google 维护 | 免费开源,基于 Python 开发 |
支持语言 | 支持超过 100 种语言,易于添加新语言 | 默认不支持多语言,需要自行训练模型 |
识别准确度 | 对印刷文本和清晰图像准确度高 | 对复杂排版和手写体文档识别准确度高 |
图像质量敏感度 | 对图像噪点、低对比度和模糊较敏感 | 相对较强的抗噪能力,适合处理多样化图像 |
处理速度 | 对大型文档或高分辨率图像处理速度较慢 | 对复杂版面的文档处理速度较快,但对计算资源要求较高 |
复杂排版处理 | 对复杂排版(如表格、图形)处理效果一般 | 对复杂排版、混合语言文档有较好的处理能力 |
使用难度 | 简单易用,有丰富的命令行工具 | 安装和配置复杂,需熟悉 Python 环境 |
维护和社区 | 拥有活跃的社区,持续更新 | 开发和维护相对较少,更新频率较低 |
可扩展性 | 支持通过训练数据进行语言扩展,但较为有限 | 模块化设计,用户可自定义处理管道,适用于多种场景 |
跨平台支持 | 支持 Windows、Linux、macOS 等多种操作系统 | 主要基于 Python,跨平台性取决于 Python 环境 |
在许多实际应用中,常常会将 OpenCV 与 Tesseract 结合使用。首先使用 OpenCV 进行图像预处理(如去噪、二值化、透视变换),然后使用 Tesseract 进行文本识别,从而获得更高的准确度和稳定性。
下面我们就用 Tesseract 方案实现一个有趣的实验。
实例
之所以称之为有趣,是因为它跟 TensorFlow
一样,Tesseract
也有提供自己的 JS 版本,Tesseract.js 可以在浏览器或 Node.js 中运行。
这是一种非常有趣的尝试。
核心代码如下:
<input type="file" id="imageInput" accept="image/*" />
<canvas id="canvas" width="300" height="300"></canvas>
<button id="recognize">Recognize Text</button>
<p id="output">Detected Text:</p>
<script src="https://unpkg.com/tesseract.js@4.1.1/dist/tesseract.min.js"></script>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const imageInput = document.getElementById('imageInput');
let imageData = null;
// 加载用户上传的图像并显示在 Canvas 上
imageInput.addEventListener('change', (event) => {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = function () {
const img = new Image();
img.onload = function () {
// 调整 Canvas 尺寸以适应图像
canvas.width = img.width;
canvas.height = img.height;
// 将图像绘制到 Canvas 上
ctx.drawImage(img, 0, 0);
imageData = canvas.toDataURL(); // 保存图像数据
};
img.src = reader.result;
};
if (file) {
reader.readAsDataURL(file);
}
});
// 使用 tesseract.js 识别图像中的文本
document
.getElementById('recognize')
.addEventListener('click', async () => {
if (!imageData) {
alert('Please upload an image first!');
return;
}
try {
// 确保 tesseract.js 完全加载
const result = await Tesseract.recognize(canvas, 'eng', {
logger: (m) => console.log(m), // 可选的日志输出
});
console.log('result', result);
document.getElementById(
'output'
).innerText = `Detected Text: ${result.data.text.trim()}`;
} catch (error) {
console.error('Error recognizing text:', error);
}
});
</script>
效果图如下:
这是一种简单解决方案,适合需要快速集成 OCR 到前端项目的用户。
优点是简单易用。
缺点是识别率低,只能应用于简单场景。
当然,实际业务中还是有很多步骤要处理的,比如处理降噪等,这里只是演示,不再过多赘述,感兴趣的童鞋,赶紧去试试吧。