First, the background of the problem

In order to prevent information leakage or intellectual property rights infringement, in the Web world, it is very necessary to add watermark processing to pages and pictures. Adding watermark can be divided into two categories according to the environment, front-end browser environment and back-end service environment. Briefly compare the characteristics of these two methods:

Front-end browser watermark:

  • Reduce the pressure on the server, quick response
  • Low security factor, for those who have a certain front-end knowledge, they can skip the watermark and get the source file through various operation
  • Application scenario: The resource is not bound to a single user, but a resource, which can be viewed by multiple users. It is necessary to add a user-specific watermark when each user views it, which is mainly used for some confidential documents or pages displaying confidential information. The purpose of watermark is to trace the responsible person when the document is leaked

Add watermark to back-end server:

  • When large file dense watermark or complex watermark is encountered, the server memory and computation are occupied, and the request time is too long
  • High security, can not get the watermark before the source file
  • Application scenario: A resource is unique to a user. An original resource needs to be processed only once and does not need to be processed again after it is stored. The purpose of watermarking is to identify the owner of the resource

Here we discuss front-end browser environment additions

Second, revenue analysis

Briefly introduce the current mainstream front-end watermark method, other students in the future when used as a reference.

Third, implementation plan

1. Repeated DOM element override implementations

From the beginning, the effect to achieve is “fill the page with low transparency of repeated identity information”, the first idea is to overwrite the page with a div box position:fixed, the box is set to low transparency, and set pointer-events: None; Style to achieve click through, in this box through the JS loop to generate small watermark div, each watermark div to display a watermark content to be displayed, a simple implementation

<! DOCTYPE html><html> 
    <head> 
        <meta charset="utf-8"> 
        <title></title> 
        <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>Confidential content - Confidential content - Confidential content - Confidential content - Confidential content - Confidential content</h2> 
            <br /> 
            <h2>Confidential content - Confidential content - Confidential content - Confidential content - Confidential content - Confidential content</h2> 
            <br /> 
            <h2 onclick="alert(1)">Confidential Content - Confidential content - Confidential content - Confidential content - Confidential content - Confidential content</h2> 
            <br /> 
        </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.'the watermark 123') 
        </script> 
    </body> 
</html>

Copy the code

The page effect is there, but this scenario requires looping through javascript to create multiple DOM elements, which is neither elegant nor performance impactful, so consider not generating so many elements.

2. Canvas displays the background image

The first step is to overlay a fixed positioning box on the page, then create a canvas canvas, draw a watermark area, output this watermark as an image through toDataURL method, set this image as the background image of the box, and pass backgroud-repeat: repeat; Style implementation fills the entire screen effect, simple implementation of the code.

<! DOCTYPE html><html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </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 = 'watermark',
                rotate = '45',
                zIndex = 10000
              } = {}) {
                const args = arguments[0];
                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); }if (typeof module! ='undefined' && module.exports) {  //CMD
                    module.exports = __canvasWM;
                } else if (typeof define == 'function' && define.amd) { // AMD
                    define(function () {
                      return __canvasWM;
                    });
                } else {
                    window.__canvasWM = __canvasWM;
                }
              })();
                
            / / call
            __canvasWM({
              content: 'the watermark 123'
            });
        </script>
    </body>
</html>
Copy the code

3. SVG implementation background diagram

It is similar to the method of canvas to generate background image, except that the method of generating background image is changed to SVG, and canvas is slightly more compatible than SVG. Compatibility comparison:

canvas

svg

<! DOCTYPEhtml>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </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 = 'watermark',
                rotate = '45',
                zIndex = 10000,
                        opacity = 0.3
              } = {}) {
                const args = arguments[0];
                  const svgStr = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${width}">
                  <text x="50%" y="50%" dy="12px"
                    text-anchor="middle"
                    stroke="#000000"
                    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,The ${window.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.style.position ='relative';
                  container.insertBefore(watermarkDiv, container.firstChild);
                }
              if (typeof module! ='undefined' && module.exports) {  //CMD
                module.exports = __canvasWM;
              } else if (typeof define == 'function' && define.amd) { // AMD
                define(function () {
                  return __canvasWM;
                });
              } else {
                window.__canvasWM = __canvasWM;
              }
            })();
        
            / / call
            __canvasWM({
              content: 'the watermark 123'
            });
        </script>
    </body>
</html>

Copy the code

Above three methods, however, there is a common problem, because is the front-end generated dom elements on the cover page, for some front knowledge can be found in the developer tools watermark in the element, the element the delete to delete the page of the purpose of the watermark, aiming at this problem, I thought of a stupid way: Set a timer every few seconds to check if our watermark element is still there and has been modified. If it has changed, we will override the watermark again. Another solution is seen online: use MutationObserver

A MutationObserver is a change observer, which is literally meant to observe node changes. The Mutation Observer API is used to monitor changes to the DOM and is notified of any changes to the DOM, such as child nodes, attributes, or text content.

However, MutationObserver can only monitor changes such as property changes, child node changes, and so on. There is no way to listen to the deletion of the MutationObserver itself, which can be achieved by monitoring the parent node. Monitor code implementation:

const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
if (MutationObserver) {
  let mo = new MutationObserver(function () {
    const __wm = document.querySelector('.__wm');
    // Call __canvasWM again only if the __wm element changes
    if ((__wm && __wm.getAttribute('style')! == styleStr) || ! __wm) {// Avoid triggering all the time
      mo.disconnect();
      mo = null;
    __canvasWM(JSON.parse(JSON.stringify(args))); }}); mo.observe(container, {attributes: true.subtree: true.childList: true}}})Copy the code

The overall code

<! DOCTYPE html><html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </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 = 'watermark',
                rotate = '45',
                zIndex = 10000
              } = {}) {
                const args = arguments[0];
                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.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');
                    // Call __canvasWM again only if the __wm element changes
                    if ((__wm && __wm.getAttribute('style')! == styleStr) || ! __wm) {// Avoid triggering all the time
                      mo.disconnect();
                      mo = null;
                    __canvasWM(JSON.parse(JSON.stringify(args))); }}); mo.observe(container, {attributes: true.subtree: true.childList: true}}})if (typeof module! ='undefined' && module.exports) {  //CMD
                module.exports = __canvasWM;
              } else if (typeof define == 'function' && define.amd) { // AMD
                define(function () {
                  return __canvasWM;
                });
              } else {
                window.__canvasWM = __canvasWM;
              }
            })();
        
            / / call
            __canvasWM({
              content: 'the watermark 123'
            });
        </script>
    </body>
</html>



Copy the code

Of course, after setting the MutationObserver, it is only relatively safe, we can still disable JS through the console to skip our listening, in general, in the front page simply watermark can always be skipped by some operation, prevent the man not to prevent the little people, prevent the outsider not to prevent the expert

4. Watermark pictures

Sometimes we need to add a watermark to the image to indicate the attribution or other information. The realization idea of adding a watermark to the image is that the image is drawn to the canvas after it is loaded successfully, and then the watermark is drawn in the canvas. After the completion, base64 is obtained through the Cancanvas.todataURL () method to replace the original image path

Code implementation:

<! DOCTYPE html><html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </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 = 'watermark',
           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);

                 constbase64Url = canvas.toDataURL(); cb && cb(base64Url); }}if (typeof module! ='undefined' && module.exports) {  //CMD
           module.exports = __picWM;
         } else if (typeof define == 'function' && define.amd) { // AMD
           define(function () {
                 return __picWM;
           });
         } else {
           window.__picWM = __picWM;
         }
             
       })();

       / / call
       __picWM({
           url: './a.png'.content: 'Watermark watermark'.cb: (base64Url) = > {
                 document.querySelector('img').src = base64Url
           },
       });

    </script>
    </body>
</html>

Copy the code

5. Expansion: Hidden watermarking of images

For image resources, explicit watermark will destroy the integrity of the image. In some cases, we want to retain the original image style, then we can add hidden watermark.

Simple implementation idea is: the image of the pixel information stored in the RGB color value, for the RGB component value of a small amount of changes, is invisible to the naked eye, will not affect the recognition of the picture, we can make small changes to the IMAGE of the RGB with a special rule.

The pixel data of the image can be obtained through Canva.geTimageData (). First, the watermark is drawn on the canvas to obtain its pixel data, and then the pixel data of the original image is obtained through the canvas. Select one of R, G and B such as G to traverse the pixels of the original image. Will have the information of the pixel corresponding watermark pixel into an odd number of G, there is no information of pixels are converted to corresponding watermark pixel even, after processing into base64 and replace the page, the invisible watermarking is then ok, normally look at the picture is no watermark, but after corresponding rules (example above the corresponding decryption rule is: Traverse the corresponding G in the pixel data of the image, and set its rgba to 0,255,0 in the case of odd numbers, and set it to 0,0,0 in the case of even numbers). After decrypting, the watermark can be seen.

In this way, when the user obtains the picture by taking screenshots, converting the format after saving the picture, etc., the color value of the picture may change, which will affect the watermarking effect. Add watermark code to achieve:

<! DOCTYPE html><html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </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');
            
            var textData;
            ctxText.font = '30px Microsoft Yahei';
            ctxText.fillText('watermark'.60.130);
            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);
                // Obtain the canvas pixel information of the specified area
                originalData = ctx.getImageData(0.0, ctx.canvas.width, ctx.canvas.height);
                console.log(originalData);
                    mergeData(textData,'G')
                    console.log(document.getElementById('canvas').toDataURL())
            };
            img.src = './aa.jpeg';
            
            var mergeData = function(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){
                        // Only the target channel is processed
                        if(newData[i + offset] === 0 && (oData[i] % 2= = =1)) {// Set the channel value for pixels without watermark information to an even number
                            if(oData[i] === 255){
                                oData[i]--;
                            } else{ oData[i]++; }}else if(newData[i + offset] ! = =0 && (oData[i] % 2= = =0)) {// Set the value of the channel corresponding to the pixel with watermark information to an odd number
                            if(oData[i] === 255){
                                oData[i]--;
                            } else {
                                oData[i]++;
                            }
                        }
                    }
                }
                ctx.putImageData(originalData, 0.0);
            }
            
        </script>
    </body>
</html>

Copy the code

Display watermark code implementation:

<! DOCTYPEhtml>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>
        <canvas id="canvas" width="256" height="256"></canvas>
        
        <script>
            var ctx = document.getElementById('canvas').getContext('2d');
            var img = new Image();
            var originalData;
            img.onload = function() {
                ctx.drawImage(img, 0.0);
                // Obtain the canvas pixel information of the specified area
                originalData = ctx.getImageData(0.0, ctx.canvas.width, ctx.canvas.height);
                console.log(originalData);
                    processData(originalData)
            };
            img.src = './a.jpg';
            
            var processData = function(originalData){
                var data = originalData.data;
                for(var i = 0; i < data.length; i++){
                    if(i % 4= =1) {if(data[i] % 2= = =0){
                            data[i] = 0;
                        } else {
                            data[i] = 255; }}else if(i % 4= = =3) {// The alpha channel is not processed
                        continue;
                    } else {
                        // Close the other components, do not close does not affect the answer, even more beautiful
                        data[i] = 0; }}// Draw the result to the canvas
                ctx.putImageData(originalData, 0.0);
            }
                
        </script>
    </body>
</html>


Copy the code

This is an easy way to do it, and if you want to learn more about it, see juejin.cn/post/691793…

4. Reference documents

1. Blind watermarking and steganography: juejin.cn/post/691793…

2. Can’t say the secret – front can play image steganography: www.alloyteam.com/2016/03/ima…

3. Front-end watermarking scheme (web page watermarking + picture watermarking) : juejin.cn/post/684490…