1

Front-end watermark

Why should there be a watermark?

  1. Protect intellectual property from unauthorized misappropriation. For example, the pictures of Taobao Meituan have watermarks on the back.
  2. Protect the company's confidential information and prevent people with intentions from leaking it

Generally speaking, the watermark can be added at the front and back ends.

  • Front-end watermark application scenarios: resources are not bound to a single user, but a resource, which is viewed by multiple users. It is necessary to add a user-specific watermark when each user views it, which is mostly used for some confidential documents or to display confidential information. The purpose of the information page, the purpose of the watermark is to hold the responsible person accountable when the document leaks out.
  • Server-side watermark usage scenario: The resource is unique to a certain user. An original resource only needs to be processed once, and it does not need to be processed again after it is stored. The purpose of the watermark is to indicate the owner of the resource.

From the front-end perspective, what are the implementation options?

DOM overlay

Using div for watermarking requires two key CSS properties.
use-select:none and pointer-events , do not let users select my watermark, and let users penetrate my watermark mask.
Then use the width and height of the watermark area and the width and height of the watermark block to calculate how many watermark blocks I need to generate, and then spread them out.

 initDivWaterMark(userId: string) {
    const waterHeight = 100
    const waterWidth = 100
    const { clientWidth, clientHeight } =
      document.documentElement || document.body
    const column = Math.ceil(clientWidth / waterWidth)
    const rows = Math.ceil(clientHeight / waterHeight)
    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 = userId
      this.box.appendChild(wrap)
    }
}

ok, you can see that our watermark appears. But there is a problem that if you use dom to generate repeatedly, it will not stop append , it doesn't feel elegant. And it was seen at once, so you can also use shadowdom .

shadowdom ShadowDom MDN

An important property of web components is encapsulation - markup structure, style and behavior can be hidden and isolated from other code on the page, ensuring that different parts are not mixed together, making the code cleaner and tidy. Among them, the Shadow DOM interface is the key, it can attach a hidden, independent DOM to an element. In other words, isolation.

A shadow root 2a6975ebc416e85c6d6bf6c7fb063610--- can be attached to any element using the Element.attachShadow() method. It accepts a configuration object as a parameter, which has a mode property, the value can be open or closed :

 let shadow = elementRef.attachShadow({mode: 'open'});
let shadow = elementRef.attachShadow({mode: 'closed'});

open means that the Shadow DOM can be obtained through JavaScript methods in the page, such as using the Element.shadowRoot property:

let myShadowDom = myCustomElem.shadowRoot;

Shadow root Custom element上, mode closed ,那么就不可以从外部获取Shadow DOM , myCustomElem.shadowRoot will return null. This is the case with some built-in elements in browsers, such as <video> , which contains the inaccessible Shadow DOM .

So for simplicity we used closed

 initShadowdomWaterMark(userId: string) {
    const shadowRoot = this.box.attachShadow({ mode: 'closed' })

    const waterHeight = 100
    const waterWidth = 100
    const { clientWidth, clientHeight } =
      document.documentElement || document.body
    const column = Math.ceil(clientWidth / waterWidth)
    const rows = Math.ceil(clientHeight / waterHeight)
    for (let i = 0; i < column * rows; i++) {
      const wrap = document.createElement('div')
      wrap.setAttribute('class', 'watermark-item')
      //   const styleStr = `
      //             color: #f20;
      //             text-align: center;
      //             transform: rotate(-30deg);
      //         `
      //   wrap.setAttribute('style', styleStr)

      //   wrap.setAttribute('part', 'watermark')
      wrap.style.width = waterWidth + 'px'
      wrap.style.height = waterHeight + 'px'
      wrap.textContent = userId
      shadowRoot.appendChild(wrap)
    }
}

As you can see, writing this way, the style is gone. In fact, it is because shadowdom plays the role of isolation. A very important point in the micro front-end is sandbox isolation, which is like qiankun Such a framework css isolation is used shadowdom

Using inline or using a special: pseudo-class can also be solved, here we will inline directly. Css part

canvas/svg background image

It can be seen that whether you use dom or shadowdom, it is inevitable to perform a for loop to add elements, and you need to calculate. Still not so elegant. So we can consider using canvas to output a background image, and then implement it by background-repeat: repeat .

 getCanvasUrl(userId: string) {
    const angle = -30
    const txt = userId
    this.canvas = document.createElement('canvas')
    this.canvas.width = 100
    this.canvas.height = 100
    this.ctx = this.canvas.getContext('2d')!
    this.ctx.clearRect(0, 0, 100, 100)
    this.ctx.fillStyle = '#f20'
    this.ctx.font = `14px`
    this.ctx.rotate((Math.PI / 180) * angle)
    this.ctx.fillText(txt, 0, 50)
    return this.canvas.toDataURL()
}

It can be seen that we only use a label and a background image to implement a watermark, but if you are a conscientious person, we only need to move our finger, open F12, and delete this label, Or modify its background, you can remove the watermark. So what should we do? Then you need to use MutationObserver , and when it comes to observers, the frequency of use is getting higher and higher now.

MutationObserver MDN

There are quite a few ways to apply MutationObserver

  1. Solution problem as we mentioned last week guide mask . We can monitor the parent node it wants to cover by mutationObserver and set a timeout disconnect to correct it in time. I once made an anchor point function and used it for correction operations.
  2. We can monitor the real usable performance through MutationObserver , and obtain the real usable time point by judging the increasing trend of nodes.
  3. The realization principle of ---49efb575bb3bed59132db8fd932e2a81 Vue nexttick , using MutationObserver is micro task to notify the next tick. Of course it is promise.then in the case of bad use, the simulation is implemented as follows

     function myNextTick(func) {
         var textNode = document.createTextNode('0'); //新建文本节点
         var callback = (mutationsList, observer) => {
             func.call(this);
         };
         var observer = new MutationObserver(callback);
         observer.observe(textNode, { characterData: true });
         textNode.data = '1'; //修改文本信息,触发dom更新
     }
  4. To monitor whether our watermark node has been changed, we can also restore it in a targeted manner.
 initObserver() {
    // 观察器的配置
    const config = { attributes: true, childList: true, subtree: true }
    // 当观察到变动时执行的回调函数
    const callback: MutationCallback = (mutationsList, observer) => {
      for (const mutation of mutationsList) {
        mutation.removedNodes.forEach((item) => {
          if (item === this.box) {
            this.warnningTargetChanged = true
            // 省事,直接添加在body上了
            document.body.appendChild(this.box)
          }
        })
      }
      if (this.warnningTargetChanged) {
        this.warnningTargetChanged = false
        console.log(`用户${this.userId}的水印变动了,可能涉嫌违规操作!!!`)
      }
    }
    // 监听元素
    const targetNode = document.body
    // 创建一个观察器实例并传入回调函数
    const observer = new MutationObserver(callback)
    // 以上述配置开始观察目标节点
    observer.observe(targetNode, config)
}

Of course, it's not that this is foolproof, because we can also solve it by disabled javascript. Like some websites, opening F12 will loop the debugger infinitely, which can also be solved.

dark watermark

Then what if it is said that there are such people who can handle everything? At this time, you need to hide the appearance of the watermark. For example, the pictures on Dianping also have watermarks with hidden copyrights. If it is commercial theft, it can be found.

There are many ways to generate dark watermarks, the common ones are through modification RGB 分量值的小量变动 , DWT, DCT and FFT. The connection and difference between DFT, DCT and DWT .

The front-end implementation mainly looks at small changes in RGB component values.

We all know that pictures are composed of pixels, and each pixel is composed of three elements of RGB. When we modify one of the components, it is difficult for the human eye to see the change, and it is difficult for even pixel-eyed designers to distinguish.

Then, as long as we can get the specific information on each pixel on a picture, we can move our hands and feet on RGB to hide the information we want. So how to get the information of the pixel point?
You need to use canvas CanvasRenderingContext2D.getImageData() , this method will return a ImageData object, which contains the pixel information array. So we should be able to use this method to make a color picker

This one-dimensional array stores all the pixel information, a total of 256 256 4 = 262144 values. 4 of them are a group, why? Parsing the image in the browser, in addition to the RGB value, the fourth value of each group is the transparency value, that is, the pixel information is actually the well-known rgba value.

pic

Take the text information we want to hide as an example

 const txt = '测试点'
    this.canvas = document.createElement('canvas')
    this.canvas.width = 10
    this.canvas.height = 10
    this.ctx = this.canvas.getContext('2d')
    this.ctx.clearRect(0, 0, 10, 10)
    this.ctx.font = `14px`

    this.ctx.fillText(txt, 0, 0)
    const textData = this.ctx.getImageData(0, 0, 10, 10).data

Copy the above code to the console and you can find that the font data is basically 0,0,0,xx.

Now that we have text data and image data, we can design an algorithm. Taking the R channel as an example, this is the red channel, (255, 0, 0, 255) which represents pure red.

We traverse the image data and check each R point of it. If the text data of this point exists, that is, the value of alpha is not 0, then we will force this point of the current image information Change the value to odd, and if there is no number at this point, change it to even.

Then finally, the odd-numbered part of the image data is the part covered with text. The even-numbered part is irrelevant. When we finally want to find our target copy, we only need to change the value of the odd-numbered part to 255, and change all the other channels and the even-numbered part to 0. The text appeared.

 // 加密核心方法
 for (let i = 0; i < oData.length; i++) {
      if (i % 4 === bit) {
        // 如果文字在这里没有数据,并且图片R通道是奇数,那我们把它改成偶数。
        if (newData[i + offset] === 0 && oData[i] % 2 === 1) {
          // 没有信息的像素,该通道最低位置0,但不要越界
          if (oData[i] === 255) {
            oData[i]--
          } else {
            oData[i]++
          }
          // 如果文字在这里是有数据的,并且图片R通道是偶数,那我们把它改成奇数。
        } else if (newData[i + offset] !== 0 && oData[i] % 2 === 0) {
          oData[i]++
        }
        // 也就是说,如果是奇数,说明一定是有文字压在上面的
      }
}

// 解密核心方法
for (let i = 0; i < data.length; i++) {
      // R通道
      if (i % 4 === 0) {
        // 目标分量,把偶数的关闭。因为文字没有数据在这里。
        if (data[i] % 2 === 0) {
          data[i] = 0
        } else {
          data[i] = 255
          data[i + 3] = 255
        }
      } else if (i % 4 === 3) {
        continue
      } else {
        data[i] = 0
      }
}

But the form of the hidden watermark we actually want to see is definitely not limited to the picture. What we want is to put a hidden watermark on our overall articles and the like. So how to do this?

In fact, we can change the watermark of the previous cavans , change the color to black, remove the rotation, and reduce the transparency to 0.005 , why is this value, because 0.005 * 255=1.27 . It's basically the least transparent. But because we are at a high level, this information must be included when taking screenshots. Then I am (0,0,0,1) superimposed on the original image, which will at least affect the color of the original image, that is to say, its R channel will move at least 1. I can't make it up. I don't know why either. But it does. We can discuss it together.


jansen
130 声望16 粉丝

学习不能止步,学习就是兴趣!终生学习是目标