前端水印生成方案
先看下效果:
水印效果
思路1:
使用canvas
进行生成图片,然后动态生成div
填充整个背景,将生成的图片用与 background-image
属性上,进行页面填充.
问题:
如果在开发者工具里取消了这个属性,则该水印效果就会消失,所以需要一种方法监视该div,不让其修改,或修改了之后又进行恢复,此处就用到了用来监视 DOM
变动的Mutation Observer
API .DOM 的任何变动,比如节点的增减、属性的变动、文本内容的变动,这个 API 都可以得到通知。参考MDN:MutationObserver
MutationObserver
只能监测到诸如属性改变、增删子结点等,对于自己本身被删除,是没有办法的,那么可以通过监测父结点来达到要求。
代码:
/**
* 网页加水印
*
* @export
* @param {*} [{
* container = document.body,
* width = '400px',
* height = '300px',
* textAlign = 'center',
* textBaseline = 'middle',
* font = "20px Microsoft Yahei",
* fillStyle = 'rgba(230, 230, 230, 0.8)',
* content = '保密水印',
* rotate = '10',
* zIndex = -1000
* }={}]
* @returns
*/
export function __waterDocument({
container = document.body,
width = '200px',
height = '150px',
textAlign = 'center',
textBaseline = 'middle',
font = "20px Microsoft Yahei",
fillStyle = 'rgba(230, 230, 230, 0.8)',
content = '保密水印',
rotate = '10',
zIndex = -1000
} = {}) {
const args = arguments[0];
const canvas = document.createElement('canvas');
canvas.setAttribute('width', width);
canvas.setAttribute('height', height);
const ctx = canvas.getContext("2d");
if (ctx === null) {
console.error("this browser is not support canvas.");
return;
}
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:absolute;
top:0;
left: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');
// 只在__wm元素变动才重新调用 __canvasWM
if ((__wm && __wm.getAttribute('style') !== styleStr) || !__wm) {
// 避免一直触发
mo.disconnect();
mo = null;
__canvasWM(JSON.parse(JSON.stringify(args)));
}
});
mo.observe(container, {
attributes: true,
subtree: true,
childList: true
})
}
}
思路2:
如果浏览器对canvas
不支持则可以使用svg
,svg浏览器支持面比canvas要大一些
可以使用https://caniuse.com/进行查看
实现原理与canvas几乎一致,只是图片替换成了svg
/**
*网页加水印 svg 方式
*
* @export
* @param {*} [{
* container = document.body,
* content = '请勿外传',
* width = '300px',
* height = '200px',
* opacity = '0.2',
* fontSize = '20px',
* zIndex = 1000
* }={}]
*/
export function __waterDocumentSvg({
container = document.body,
content = '请勿外传',
width = '300px',
height = '200px',
opacity = '0.2',
fontSize = '20px',
zIndex = 1000
} = {}) {
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: ${fontSize};">
${content}
</text>
</svg>`;
const base64Url = `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svgStr)))}`;
const __wm = document.querySelector('.__wm');
const watermarkDiv = __wm || document.createElement("div");
const styleStr = `
position:absolute;
top:0;
left: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');
// 只在__wm元素变动才重新调用 __canvasWM
if ((__wm && __wm.getAttribute('style') !== styleStr) || !__wm) {
// 避免一直触发
mo.disconnect();
mo = null;
__waterDocumentSvg(JSON.parse(JSON.stringify(args)));
}
});
mo.observe(container, {
attributes: true,
subtree: true,
childList: true
})
}
}
通过NodeJS生成水印
身为现代前端开发者,Node.JS也是需要掌握的。我们同样可以通过NodeJS来生成网页水印,前端发一个请求,参数带上水印内容,后台返回图片内容。
具体实现(Koa2环境):
- 安装gm以及相关环境,详情看gm文档
-
ctx.type = 'image/png';
设置响应为图片类型 - 生成图片过程是异步的,所以需要包装一层Promise,这样才能为通过 async/await 方式为 ctx.body 赋值
const fs = require('fs')
const gm = require('gm');
const imageMagick = gm.subClass({
imageMagick: true
});
const router = require('koa-router')();
router.get('/wm', async (ctx, next) => {
const {
text
} = ctx.query;
ctx.type = 'image/png';
ctx.status = 200;
ctx.body = await ((() => {
return new Promise((resolve, reject) => {
imageMagick(200, 100, "rgba(255,255,255,0)")
.fontSize(40)
.drawText(10, 50, text)
.write(require('path').join(__dirname, `./${text}.png`), function (err) {
if (err) {
reject(err);
} else {
resolve(fs.readFileSync(require('path').join(__dirname, `./${text}.png`)))
}
});
})
})());
});
图片水印生成解决方案
通过canvas给图片加水印
思路:读取图片,在图片加载完之后用canvas填充图片,并填充加密文字,然后将图片转成base64url传值给回调函数
/**
*图片加水印
*
* @export
* @param {*} [{
* url = '',
* textAlign = 'center',
* textBaseline = 'middle',
* font = "20px Microsoft Yahei",
* fillStyle = 'rgba(184, 184, 184, 0.8)',
* content = '请勿外传',
* cb = null,
* textX = 100,
* textY = 30
* }={}]
*/
export function __picWM({
url = '',
textAlign = 'center',
textBaseline = 'middle',
font = "20px Microsoft Yahei",
fillStyle = 'rgba(184, 184, 184, 0.8)',
content = '请勿外传',
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);
const base64Url = canvas.toDataURL();
cb && cb(base64Url);
}
}
调用:
__picWM({
url: 'https://www.baidu.com/img/bd_logo1.png?where=super',
content: '加密图片',
cb: (base64Url) => {
document.getElementById('s_lg_img').src = base64Url
},
})
效果:
加密图片
使用加密后的水印内容
前端生成的水印也可以,别人也可以用同样的方式生成,可能会有“嫁祸于人”(可能这是多虑的),我们还是要有更安全的解决方法。水印内容可以包含多种编码后的信息,包括用户名、用户ID、时间等。比如我们只是想保存用户唯一的用户ID,需要把用户ID传入下面的md5方法,就可以生成唯一标识。编码后的信息是不可逆的,但可以通过全局遍历所有用户的方式进行追溯。这样就可以防止水印造假也可以追溯真正水印的信息。
// MD5加密库 utility
const utils = require('utility')
// 加盐MD5
exports.md5 = function (content) {
const salt = 'microzz_asd!@#IdSDAS~~';
return utils.md5(utils.md5(content + salt));
}
参考:前端水印生成方案 --QQ音乐前端团队