Front-end watermark
Why should there be a watermark?
- Protect intellectual property from unauthorized misappropriation. For example, the pictures of Taobao Meituan have watermarks on the back.
- 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
- Solution problem as we mentioned last week
guide mask
. We can monitor the parent node it wants to cover bymutationObserver
and set a timeoutdisconnect
to correct it in time. I once made an anchor point function and used it for correction operations. - We can monitor the real usable performance through
MutationObserver
, and obtain the real usable time point by judging the increasing trend of nodes. The realization principle of ---49efb575bb3bed59132db8fd932e2a81
Vue nexttick
, usingMutationObserver
ismicro task
to notify the next tick. Of course it ispromise.then
in the case of bad use, the simulation is implemented as followsfunction 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更新 }
- 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.
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.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。