Skip to content

背景

WARNING

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

但是我一看需求,好家伙果真不简单,原来不是几个字,而是多个元素叠加在一起的;水印的数据也是动态的,而七牛云后台只能设置一些 静态属性或者图片地址,如果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转成文件;

下面就是大概的思路

js
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合并;

js
// 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);
		});
	});
});

wangxiaoze | MIT License.