使用背景图图片
单独使用 css 实现,使用 backgroundImage,backgroundRepeat 将背景图片平铺到需要加水印的容器中即可。
如果希望实现旋转效果,可以借助伪元素,将背景样式放到伪元素中,旋转伪元素实现:
<style>
.watermark {
position: relative;
overflow: hidden;
background-color: transparent;
}
.watermark::before {
content: '';
position: absolute;
width: 160%;
height: 160%;
top: -20%;
left: -20%;
z-index: -1;
background-image: url('./watermark.png');
background-position: 0 0;
background-origin: content-box;
background-attachment: scroll;
transform: rotate(-20deg);
background-size: auto;
background-repeat: round;
opacity: 0.3;
pointer-events: none;
}
</style>
动态生成div
根据水印容器的大小动态生成div,div内可以任意设置文本样式和图片,借助userSelect禁止用户选中文本水印;
const addDivWaterMark = (el, text) => {
const { clientWidth, clientHeight } = el;
const waterWrapper = document.createElement('div');
waterWrapper.className = "waterWrapper";
const column = Math.ceil(clientWidth / 100);
const rows = Math.ceil(clientHeight / 100);
// 根据容器宽高动态生成div
for (let i = 0; i < column * rows; i++) {
const wrap = document.createElement('div');
wrap.className = "water";
wrap.innerHTML = `<div class="water-item">${text}</div>`
waterWrapper.appendChild(wrap)
}
el.append(waterWrapper)
}
Canvas写入图片做背景水印
将图片写入Canvas然后将Canvas作为背景图
const img = new Image();
const { ctx, canvas } = createWaterMark(config);
img.onload = function () {
ctx.globalAlpha = 0.2;
ctx.rotate(Math.PI / 180 * 20);
ctx.drawImage(img, 0, 16, 180, 100);
canvasRef.value.style.backgroundImage = `url(${canvas.toDataURL()})`
};
img.src = ImageBg;
Canvas写入文字做背景水印
将文字写入Canvas然后将Canvas作为背景图
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = fillStyle;
ctx.globalAlpha = opacity;
ctx.font = font
ctx.rotate(Math.PI / 180 * rotate);
ctx.fillText(text, 0, 50);
return canvas
Svg做水印
通过svg样式来控制水印样式,再将svg转换成base64的背景图
const svgStr =
`<svg xmlns="http://www.w3.org/2000/svg" width="180px" height="100px">
<text x="0px" y="30px" dy="16px"
text-anchor="start"
stroke="#000"
stroke-opacity="0.1"
fill="none"
transform="rotate(-20)"
font-weight="100"
font-size="16"> 前端小书童</text>
</svg>`;
return `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svgStr)))}`;
shadowDom水印
使用customElements自定义个一个标签(可以使用其他任意标签,不过注意shadow DOM会使起同级的元素不显示。)
可以像shadow DOM写入style样式和水印节点(可以使用背景或者div形式)
shadow DOM内部实现的样式隔离不用担心写入的style影响页面其他元素样式,这个特性在微前端的实现中也被广泛使用。
class ShadowMark extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
const wrapContainer = document.createElement('div')
const style = document.createElement('style');
style.textContent = `
.wrapContainer {
width: 100%;
height: 100%;
display: flex;
flex-wrap: wrap;
}
.watermark-item {
display: flex;
font-size: 16px;
opacity: .3;
transform: rotate(-20deg);
user-select: none;
white-space: nowrap;
justify-content: center;
align-items: center;
}`;
const waterHeight = 100
const waterWidth = 100
const { clientWidth, clientHeight } = document.querySelector('.shadow-watermark')
const column = Math.ceil(clientWidth / waterWidth)
const rows = Math.ceil(clientHeight / waterHeight)
wrapContainer.setAttribute('class', "wrapContainer")
for (let i = 0; i < column * rows; i++) {
const wrap = document.createElement('div')
wrap.setAttribute('class', 'watermark-item')
wrap.style.width = waterWidth + 'px'
wrap.style.height = waterHeight + 'px'
wrap.textContent = "前端小书童"
wrapContainer.appendChild(wrap)
}
shadowRoot.appendChild(style);
shadowRoot.appendChild(wrapContainer)
}
}
customElements.define('shadow-mark', ShadowMark);
盲水印
canvas画布(canvas.getContext(‘2d’))调用 getImageData 得到一个 ArrayBuffer,用于记录画布每个像素的 rgba 值
r: Red取值范围0~255
g: Green取值范围0~255
b:Blue取值范围0~255
a:Alpha 透明度取值范围0~1,0代表全透明
可以理解为每个像素都是通过红、绿、蓝三个颜色金额透明度来合成颜色
方案一:低透明度方案的暗水印
当把水印内容的透明度 opacity 设置很低时,视觉上基本无法看到水印内容,但是通过修改画布的 rgba 值,可以使水印内容显示出来。
选择固定的一个色值例如R,判断画布R值的奇偶,将其重置为0或者255,低透明的内容就便可以显示出来了。
const decode = (canvas, colorKey, flag, otherColorValue) => {
const ctx = canvas.getContext('2d');
const originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
let data = originalData.data;
for (let i = 0; i < data.length; i++) {
//筛选每个像素点的R值
if (i % 4 == colorKey) {
if (data[i] % 2 == 0) {
//如果色值为偶数
data[i] = flag ? 255 : 0;
} else {
//如果色值为奇数
data[i] = flag ? 0 : 255;
}
} else if (i % 4 == 3) {
//透明度不作处理
continue;
} else {
// 关闭其他色值
if (otherColorValue !== undefined) {
data[i] = otherColorValue
}
}
}
ctx.putImageData(originalData, 0, 0);
}
方案二:将水印内容以像素偏差记录到画布中
用画布和水印后的画布绘制的像素进行ArrayBuffer对比,在存在水印像素的位置(水印画布透明度不为0)修改图片画布的奇偶,这样通过上面指定色值和奇偶去解码时,修改的文本像素就会被显示出来;
const encode = (ctx, textData, color, originalData) => {
for (let i = 0; i < originalData.data.length; i++) {
// 只处理目标色值
if (i % 4 == color) {
// 水印画布透明度为0
if (textData[i + offset] === 0 && (originalData.data[i] % 2 === 1)) {
// 放置越界
if (originalData.data[i] === 255) {
originalData.data[i]--;
} else {
originalData.data[i]++;
}
// 水印画布透明度不为0
} else if (textData[i + offset] !== 0 && (originalData.data[i] % 2 === 0)) {
originalData.data[i]++;
}
}
}
ctx.putImageData(originalData, 0, 0);
}
方案三:数字加密
在图像信号的频域(变换域)中隐藏信息要比在空间域(上面得到的像素颜色的ArrayBuffer)中隐藏信息具有更好的防攻击性。
这部分暗水印的实现,可以直接使用阿里云提供给的api,不过需要图片资源藏到的阿里云的OSS下;
MutationObserver
可以发现上面水印基本都是通过增加节点或者背景图的形式来实现,那用户其实可以通过屏蔽样式或者删除Dom来消除水印,那么我们可以借用MutationObserver来监听下水印dom的变化,来阻止用户以这种形式来消除水印;
以上代码见:https://github.com/wenjuGao/watermark-demo
线上效果:https://watermark-demo.vercel.app/
参考:
https://www.cnblogs.com/88223100/p/Exploring-Web-Watermarking-Technology.html
https://blog.csdn.net/blueblueskyhua/article/details/120346195
https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API