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.

ELab Team
ELab Team
ELab Team
Robust Browser Watermarks: DOM Overlays, Canvas, SVG & Hidden Image Techniques

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:

Canvas compatibility
Canvas compatibility

SVG

SVG compatibility
SVG compatibility
<!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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Image ProcessingCanvasMutationObserverWatermark
ELab Team
Written by

ELab Team

Sharing fresh technical insights

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.