乐趣区

关于javascript:前端实现水印功能

前端水印

前一段时间,我老婆的公司,须要给一份简历加水印,而后沟通了一下,说是服务端来加,服务端搞这个水印的话,光这个接口,就要耗时 500ms,所以说做不了。正愁着不晓得分享啥,就分享个这个好了

为什么要有水印的存在?

  1. 爱护知识产权,避免未经容许被随便盗用。比方淘宝美团的图片,背地都有水印。
  2. 爱护公司机密信息,避免有心之人泄密

通常来说,前后端都能够实现水印的增加。

  • 前端水印实用场景:资源不跟某一个独自的用户绑定,而是一份资源,多个用户查看,须要在每一个用户查看的时候增加用户特有的水印,多用于某些秘密文档或者展现机密信息的页面,水印的目标在于文档外流的时候能够查究到责任人
  • 服务端水印应用场景:资源为某个用户独有,一份原始资源只须要做一次解决,将其存储之后就无需再次解决,水印的目标在于标示资源的归属人

从前端的角度来说,有哪些实现计划

DOM 笼罩

利用 div 来做水印,须要两个要害 css 属性。
use-select:nonepointer-events,不让用户选中我这个水印,以及让用户穿透我这个水印遮罩。
而后利用想要呈现水印区域的宽高以及水印块的宽高,计算出我须要生成多少个水印块,而后铺开。

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,能够看到,咱们的水印呈现了。然而有一个问题是,应用 dom 反复生成的话,还不停的 append 的话,感觉不优雅。而且一下就被人看到了,所以也能够用shadowdom

shadowdom ShadowDom MDN

Web components 的一个重要属性是封装——能够将标记构造、款式和行为暗藏起来,并与页面上的其余代码相隔离,保障不同的局部不会混在一起,可使代码更加洁净、整洁。其中,Shadow DOM 接口是关键所在,它能够将一个暗藏的、独立的 DOM 附加到一个元素上。说白了就是隔离。

能够应用 Element.attachShadow() 办法来将一个 shadow root 附加到任何一个元素上。它承受一个配置对象作为参数,该对象有一个 mode 属性,值能够是 open 或者 closed

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

open 示意能够通过页面内的 JavaScript 办法来获取 Shadow DOM,例如应用 Element.shadowRoot 属性:

let myShadowDom = myCustomElem.shadowRoot;

如果你将一个 Shadow root 附加到一个 Custom element 上,并且将 mode 设置为 closed,那么就不能够从内部获取 Shadow DOM 了, myCustomElem.shadowRoot 将会返回 null。浏览器中的某些内置元素就是如此,例如<video>,蕴含了不可拜访的 Shadow DOM

所以为了简略,咱们用了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)
    }
}

可有看到,这样写,款式没有了,其实就是因为 shadowdom 起到了隔离的作用,微前端里很重要的一个点就是沙箱隔离,其中像 qiankun 这样的框架的 css 隔离就是用了shadowdom

应用内联或者应用非凡的: 伪类也能够解决,这里就先间接内联了。Css part

canvas/svg 背景图

能够看到,不论是应用 dom 还是 shadowdom,都防止不了的进行 for 循环来进行增加元素,还须要计算。仍然不是那么的优雅。所以咱们能够思考用 canvas 输入一个背景图,而后通过 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()}

能够看到,咱们只用了一个标签,以及背景图的形式,来实现了一个水印,然而呢,如果你是一个有心之人,咱们只须要动动手指,关上 F12,把这个标签删了,或者批改它的背景,都能够把水印去掉。那咱们应该怎么办呢?那就须要用到 MutationObserver 了,说到观察者,当初应用的频率也是越来越高了。

MutationObserverMDN

MutationObserver 利用的形式还挺多的

  1. 如咱们上周提到的 guide mask 的解决方案问题。咱们能够通过 mutationObserver 对它要笼罩的父级节点进行监控,并设置一个超时工夫disconnect,在工夫内对它进行改正。我已经有做过一个锚点的性能,也利用到了它来进行改正操作。
  2. 咱们能够通过 MutationObserver 来对真正的可用性能进行监控,通过判断节点的减少趋势,来取得真正能够应用的工夫点。
  3. Vue nexttick的实现原理,利用 MutationObserver 是个 micro task,来进行下一 tick 的告诉。当然是promise.then 不好使的状况下,模仿实现如下

    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. 监控咱们的水印节点是否被变更,咱们也能够针对性的进行复原。
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)
} 

当然,并不是说这样就十拿九稳了,因为咱们还能够通过 disabled javascript 来解决。像有的网站,开启 F12 就会有限循环 debugger,也能够解决。

暗水印

那如果说,就是有这样的人存在,通通都能搞定呢?这时候就须要暗藏水印的呈现了。比方公众点评下面的图片,其实也都是有暗藏版权的水印存在的。如果是商用盗用,都是能被查到的。

暗水印的生成形式有很多,常见的为通过批改RGB 重量值的小量变动、DWT、DCT 和 FFT 等办法。DFT、DCT 和 DWT 的分割和区别。

前端实现次要看 RGB 重量值的小量变动。

咱们都晓得图片都是有一个个像素点形成的,每个像素点都是由 RGB 三种元素形成。当咱们把其中的一个重量批改,人的肉眼是很难看出其中的变动,甚至是像素眼的设计师也很难分辨出。

那么,咱们只有能获取到一张图片上每个像素点上的具体信息,就能够再 RGB 上动动手脚,就能够把咱们想要的信息藏进去。那如何获取像素点的信息呢?
就须要用到 canvas 的 CanvasRenderingContext2D.getImageData() 了,这个办法会返回一个 ImageData对象,其中就蕴含了像素的信息数组。所以咱们应该能够利用这个办法,来做取色器

这个一维数组存储了所有的像素信息,一共有 256 256 4 = 262144 个值。其中 4 个值一组,为什么呢?在浏览器中解析图片,除了 RGB 值外,每组第 4 个值为透明度值,即像素信息理论为大家熟知的 rgba 值。

以咱们想要藏的文字信息为例

    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

把下面代码复制到控制台能够发现,字体的数据根本都是 0,0,0,xx。

那当初咱们有了文字数据和图片数据,咱们就能够设计一个算法。以 R 通道为例子,这个是红色通道,(255,0,0,255) 就代表的是纯红色。

咱们遍历图片数据,查看它的每一个 R 点位,如果这个点位的文字数据是有的,也就是alpha 值不为 0,那咱们就强行把以后图片信息的这个点的值改成奇数,如果这个点位没有数字,就把它改成偶数。

那么最初,这个图片数据里奇数的局部,就是有文字盖着的局部。而偶数局部,就是无关紧要的了。那最初想要找到咱们的指标文案的时候,只须要把奇数局部的值变成 255,把其余通道以及偶数局部的,全都改成 0。文字就呈现了。

// 加密外围办法
 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
      }
}

然而咱们理论想要见到的暗藏水印的模式,必定不是局限在图片上的。咱们心愿的是给咱们整体的文章之类的打上暗藏水印。那这怎么做呢?

其实咱们能够把之前 cavans 的水印,把色彩换成彩色,旋转去掉,透明度升高到 0.005,为啥是这个值,因为0.005 * 255=1.27。根本算是最小透明度了。然而因为咱们是高层级,所以截图的时候,肯定会把这信息蕴含进去。那么我是(0,0,0,1) 叠加到原来的图片上,至多会影响到原图的色彩,也就是说它的 R 通道,起码会动个 1。我编不上来了。我也不晓得为啥。然而的确能够。大家能够一起探讨下。

退出移动版