前端水印
前一段时间,我老婆的公司,须要给一份简历加水印,而后沟通了一下,说是服务端来加,服务端搞这个水印的话,光这个接口,就要耗时 500ms,所以说做不了。正愁着不晓得分享啥,就分享个这个好了
为什么要有水印的存在?
- 爱护知识产权,避免未经容许被随便盗用。比方淘宝美团的图片,背地都有水印。
- 爱护公司机密信息,避免有心之人泄密
通常来说,前后端都能够实现水印的增加。
- 前端水印实用场景:资源不跟某一个独自的用户绑定,而是一份资源,多个用户查看,须要在每一个用户查看的时候增加用户特有的水印,多用于某些秘密文档或者展现机密信息的页面,水印的目标在于文档外流的时候能够查究到责任人
- 服务端水印应用场景:资源为某个用户独有,一份原始资源只须要做一次解决,将其存储之后就无需再次解决,水印的目标在于标示资源的归属人
从前端的角度来说,有哪些实现计划
DOM 笼罩
利用 div 来做水印,须要两个要害 css 属性。use-select:none
和 pointer-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 利用的形式还挺多的
- 如咱们上周提到的
guide mask
的解决方案问题。咱们能够通过mutationObserver
对它要笼罩的父级节点进行监控,并设置一个超时工夫disconnect
,在工夫内对它进行改正。我已经有做过一个锚点的性能,也利用到了它来进行改正操作。 - 咱们能够通过
MutationObserver
来对真正的可用性能进行监控,通过判断节点的减少趋势,来取得真正能够应用的工夫点。 -
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 更新 }
- 监控咱们的水印节点是否被变更,咱们也能够针对性的进行复原。
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。我编不上来了。我也不晓得为啥。然而的确能够。大家能够一起探讨下。