背景

前几天,公司项目中有这么一个需求,说是上传图片给图片加水印的功能,我说那还不简单,因为公司图片存储采用的是七牛云,当然七牛云后台是可以设置图片的水印;

但是我一看需求,好家伙果真不简单,原来不是几个字,而是多个元素叠加在一起的;水印的数据也是动态的,而七牛云后台只能设置一些
静态属性或者图片地址,如果url地址加一些参数的情况,效果也是不可观的

而通过url地址拼接参数的情况:/image/<encodedkodocheme> : 水印的源路径,目前支持 kodo 资源。kodo 资源可由 kodo://<bucketname>/<key> 表示(此时 bucketname 需要与输入源在同一区域),均需要经过 urlsafe_base64_encode。

注意:更换图片水印时,建议更换图片的文件名。

这样的情况目前只支持 kodo的图片资源,如果使用这样的参数,那么我在上传前就要先上传动态数据的水盈图片,之后在上传要上传的图片;

虽然这样是可以做到,但是对于七牛云的存储容量和流量来讲,消耗是很大的; 所以这样的实现方式是不可取的;

水印效果图:

效果

传统的canvas

也可以采用传统的 canvas 实现,但是将上传的问图片也是可以通过下面的方式转为url,然后在通过生成canvas画图功能将上传的图片文件生成
canvas。然后在通过文字的定位定位在图片的左下角;

但是这样的方式就会大量计算文本x, y 在 canvas 中的位置;因为数据是动态的,所以理论上是可以实现的,但是要考虑多行文本的问题; 所以这里不建议采用该方式;

使用 html2canvashtml 元素组合

大致的思路就是:

  • 先将要生成canvas的容器在页面可视区域不可见,这里就是将它定位出去的;
  • 然后上传前,现将图片文件生成地址,赋值给new Image(), 在去监听加载完成; 之后就是将定位出去的元素样式化;
  • 通过html2canvas将跟元素生成带水印的canvas, 接下来就是对canvas 转为文件,从而实现水印图片的上传;

首先,在上传文件前先将图片文件转为对象地址,然后兼容图片的加载完成。

将之前定位出去的元素的图片赋值,设置跟元素的宽度和高度为图片的宽度和高度;

然后在通过html2canvas插件将跟元素生成canvas,在将canvas转化为blob, 接着new Fileblob转成文件;

下面就是大概的思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
this.getDom("#file-input").addEventListener("change", function (e) {
// 这里制考虑单个图片上传的情况
const file = e.target.files[0];

const tempImg = new Image();
tempImg.src = URL.createObjectURL(file);
tempImg.addEventListener("load", function () {
// 地址类似于:src="blob:http://localhost:63342/4302cb32-7eba-4a89-bd4a-66649bce781e"

// 获取元素的跟节点以及渲染的图片
const node = _this.getDom("#node");
const imgDom = _this.getDom("img");

// 设置图片的路径,以及跟元素的宽度和高度
imgDom.setAttribute("src", tempImg.src);
node.style.width = tempImg.width + "px";
node.style.height = tempImg.height + "px";

// 通过 html2canvas 将渲染的元素专程canvas
html2canvas &&
html2canvas(node).then(function (canvas) {
// 在将canvas转成文件, 实现文件的上传
canvas.toBlob(blob => {
// 生成的文件,这个就是你要上传的文件
const newFile = new File([blob], file.name, { type: file.type });
console.log(newFile);

// 预览。将生成水印的canvas在生成图片url
let imgSrc = canvas.toDataURL("image/jpeg", 1);
console.log(imgSrc);
_this.getDom(
".temp-container"
).innerHTML = `<img class="show" style="width: ${tempImg.width}px; height: ${tempImg.height}px" src="${imgSrc}" alt="" />`;
});
});
});
});

svg代码片段

还有一种方式,就是将svg代码片段,而这代码片段就是左下角的水印元素,将它生成 canvas,再将上传图片生成 canvas;然后俩张canvas合并;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// svg 片段
let data = `
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<foreignObject width="100%" height="100%">
<div xmlns="http://www.w3.org/1999/xhtml">
<div style="color: red">这里就是你渲染的元素</div>
</div>
</foreignObject>
</svg>
`;
// 将svg生成 image
let imgSvg = new Image();
let blob = new Blob([data], { type: "image/svg+xml;charset=utf-8" });
imgSvg.src = URL.createObjectURL(blob);

// 上传图片和水印合并
function img2Canvas(img) {
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);

imgSvg.onload = function () {
ctx.drawImage(imgSvg, 20, canvas.height - 100 - 20);
// 摧毁刚刚生产的url
URL.revokeObjectURL(imgSvg.src);
};
imgSvg.src = URL.createObjectURL(blob);

document.body.append(canvas);

return canvas;
}

this.getDom("#file-input2").addEventListener("change", function (e) {
const file = e.target.files[0];
let tempImg = new Image();
tempImg.src = URL.createObjectURL(file);
tempImg.addEventListener("load", () => {
img2Canvas(tempImg).toBlob(blob => {
const newFile = new File([blob], file.name, { type: file.type });
console.log(newFile);
});
});
});