Robust Browser Watermarks: DOM Overlays, Canvas, SVG & Hidden Image Techniques
This article explores why adding watermarks to web pages is essential for protecting confidential content, compares front‑end and back‑end watermarking approaches, and provides step‑by‑step implementations using DOM overlays, canvas, SVG, image processing, and MutationObserver to keep watermarks resilient against tampering.
Problem Background
To prevent information leakage or intellectual‑property theft, adding watermarks to web pages and images is necessary. Watermarks can be applied in two environments: front‑end (browser) and back‑end (server). The front‑end approach reduces server load and reacts quickly, but its security is lower because knowledgeable users can bypass it; it is suitable for resources viewed by many users where each view needs a user‑specific watermark. The back‑end approach offers higher security and is ideal when a resource belongs to a single user and only needs one processing step.
Benefit Analysis
A brief overview of mainstream front‑end watermarking methods is provided for future reference.
Implementation方案
1. Duplicate DOM Element Overlay
The idea is to cover the page with a position:fixed div of low opacity, set pointer-events:none for click‑through, and generate many small watermark divs inside it via JavaScript.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
#watermark-box {
position: fixed;
top: 0; bottom: 0; left: 0; right: 0;
font-size: 24px;
font-weight: 700;
display: flex;
flex-wrap: wrap;
overflow: hidden;
user-select: none;
pointer-events: none;
opacity: 0.1;
z-index: 999;
}
.watermark { text-align: center; }
</style>
</head>
<body>
<div>
<h2>机密内容- …</h2>
<h2>机密内容- …</h2>
<h2 onclick="alert(1)">机密内容- …</h2>
</div>
<div id="watermark-box"></div>
<script>
function doWaterMark(width, height, content) {
let box = document.getElementById("watermark-box");
let boxWidth = box.clientWidth, boxHeight = box.clientHeight;
for (let i = 0; i < Math.floor(boxHeight / height); i++) {
for (let j = 0; j < Math.floor(boxWidth / width); j++) {
let next = document.createElement("div");
next.setAttribute("class", "watermark");
next.style.width = width + 'px';
next.style.height = height + 'px';
next.innerText = content;
box.appendChild(next);
}
}
}
window.onload = doWaterMark(300, 100, '水印123');
</script>
</body>
</html>This method works but creates many DOM nodes, affecting performance.
2. Canvas Output Background Image
Place a fixed‑position box, draw a watermark on a canvas, convert it to a data URL, and set it as the box’s repeating background.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="info" onclick="alert(1)">123</div>
<script>
(function(){
function __canvasWM({container=document.body,width='300px',height='200px',textAlign='center',textBaseline='middle',font='20px Microsoft Yahei',fillStyle='rgba(184,184,184,0.6)',content='水印',rotate='45',zIndex=10000} = {}){
const canvas = document.createElement('canvas');
canvas.setAttribute('width', width);
canvas.setAttribute('height', height);
const ctx = canvas.getContext('2d');
ctx.textAlign = textAlign;
ctx.textBaseline = textBaseline;
ctx.font = font;
ctx.fillStyle = fillStyle;
ctx.rotate(Math.PI/180*rotate);
ctx.fillText(content, parseFloat(width)/2, parseFloat(height)/2);
const base64Url = canvas.toDataURL();
const __wm = document.querySelector('.__wm');
const watermarkDiv = __wm || document.createElement('div');
const styleStr = `position:fixed;top:0;left:0;bottom:0;right:0;width:100%;height:100%;z-index:${zIndex};pointer-events:none;background-repeat:repeat;background-image:url('${base64Url}')`;
watermarkDiv.setAttribute('style', styleStr);
watermarkDiv.classList.add('__wm');
if (!__wm) { container.insertBefore(watermarkDiv, container.firstChild); }
}
__canvasWM({content:'水印123'});
})();
</script>
</body>
</html>3. SVG Background Image
Similar to the canvas method but generates an SVG data URL. SVG has slightly poorer compatibility.
Compatibility comparison:
SVG
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="info" onclick="alert(1)">123</div>
<script>
(function(){
function __canvasWM({container=document.body,width='300px',height='200px',textAlign='center',textBaseline='middle',font='20px Microsoft Yahei',fillStyle='rgba(184,184,184,0.6)',content='水印',rotate='45',zIndex=10000,opacity=0.3} = {}){
const svgStr = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
<text x="50%" y="50%" dy="12px" text-anchor="middle" stroke="#000" stroke-width="1" stroke-opacity="${opacity}" fill="none" transform="rotate(-45,120,120)" style="font-size:${font};">${content}</text>
</svg>`;
const base64Url = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svgStr)))}`;
const __wm = document.querySelector('.__wm');
const watermarkDiv = __wm || document.createElement('div');
const styleStr = `position:fixed;top:0;left:0;bottom:0;right:0;width:100%;height:100%;z-index:${zIndex};pointer-events:none;background-repeat:repeat;background-image:url('${base64Url}')`;
watermarkDiv.setAttribute('style', styleStr);
watermarkDiv.classList.add('__wm');
if (!__wm) { container.insertBefore(watermarkDiv, container.firstChild); }
}
__canvasWM({content:'水印123'});
})();
</script>
</body>
</html>4. Image Watermark
Load an image, draw it onto a canvas, render watermark text, then replace the original image source with the canvas data URL.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="info" onclick="alert(1)"><img /></div>
<script>
(function(){
function __picWM({url='',textAlign='center',textBaseline='middle',font='20px Microsoft Yahei',fillStyle='rgba(184,184,184,0.8)',content='水印',cb=null,textX=100,textY=30} = {}){
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.textAlign = textAlign;
ctx.textBaseline = textBaseline;
ctx.font = font;
ctx.fillStyle = fillStyle;
ctx.fillText(content, img.width - textX, img.height - textY);
const base64Url = canvas.toDataURL();
cb && cb(base64Url);
};
}
__picWM({url:'./a.png',content:'水印水印',cb:(base64Url)=>{document.querySelector('img').src = base64Url}});
})();
</script>
</body>
</html>5. Extension: Hidden Image Watermark (Steganography)
Instead of visible marks, modify pixel RGB values slightly so changes are invisible to the eye. By encoding watermark bits into the parity of a chosen channel (e.g., G), the image appears unchanged but can be decoded later.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<canvas id="canvasText" width="256" height="256"></canvas>
<canvas id="canvas" width="256" height="256"></canvas>
<script>
var ctx = document.getElementById('canvas').getContext('2d');
var ctxText = document.getElementById('canvasText').getContext('2d');
ctxText.font = '30px Microsoft Yahei';
ctxText.fillText('水印', 60, 130);
var textData = ctxText.getImageData(0,0,ctxText.canvas.width,ctxText.canvas.height).data;
var img = new Image();
var originalData;
img.onload = function(){
ctx.drawImage(img,0,0);
originalData = ctx.getImageData(0,0,ctx.canvas.width,ctx.canvas.height);
mergeData(textData,'G');
};
img.src = './aa.jpeg';
function mergeData(newData,color){
var oData = originalData.data;
var bit, offset;
switch(color){
case 'R': bit=0; offset=3; break;
case 'G': bit=1; offset=2; break;
case 'B': bit=2; offset=1; break;
}
for(var i=0;i<oData.length;i++){
if(i%4===bit){
if(newData[i+offset]===0 && (oData[i]%2===1)){
oData[i] = (oData[i]===255)?254: oData[i]+1;
} else if(newData[i+offset]!==0 && (oData[i]%2===0)){
oData[i] = (oData[i]===255)?254: oData[i]+1;
}
}
}
ctx.putImageData(originalData,0,0);
}
</script>
</body>
</html>Robustness with MutationObserver
Because a malicious user can delete the watermark element via DevTools, a timer or a MutationObserver can monitor changes and re‑apply the watermark when the element is altered or removed.
const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
if (MutationObserver) {
let mo = new MutationObserver(function(){
const __wm = document.querySelector('.__wm');
if ((!__wm) || (__wm.getAttribute('style') !== styleStr)) {
mo.disconnect();
mo = null;
__canvasWM(JSON.parse(JSON.stringify(args)));
}
});
mo.observe(container, {attributes:true, subtree:true, childList:true});
}References
Blind watermark and image steganography: https://juejin.cn/post/6917934964202242061
Undisclosed secrets – front‑end image steganography: http://www.alloyteam.com/2016/03/image-steganography/
Front‑end watermark solutions (page + image): https://juejin.cn/post/6844903645155164174
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
