

统一声明:
1.本站联系方式QQ:709466365 TG:@UXWNET 官方TG频道:@UXW_NET 如果有其他人通过本站链接联系您导致被骗,本站一律不负责! 2.需要付费搭建请联系站长QQ:709466365 TG:@UXWNET 3.免实名域名注册购买- 游侠云域名 4.免实名国外服务器购买- 游侠网云服务
一、用JS+Canvas做水印的基础逻辑:从加载到导出的完整流程
其实用原生JS做水印的核心逻辑就5步:加载原图→创建Canvas→绘制原图→绘制水印→导出图片。但每一步都有容易踩的坑,我一个个跟你说清楚。
首先是加载原图。你得先把要加水印的图片加载进浏览器,用new Image()
就行,但千万要记得加crossOrigin="anonymous"
——我去年踩的第一个坑就是这个!当时朋友的博客图片存在阿里云OSS上,我直接用img.src
加载,结果绘制到Canvas后,想导出时浏览器报“Tainted canvases may not be exported”(污染的画布无法导出)。后来查MDN才知道,跨域图片会污染Canvas,必须让图片服务器允许跨域访问,所以要给Image
对象加crossOrigin
属性(引用MDN文档:CORS-enabled image)。正确的加载代码应该是这样的:
const loadImage = (url) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous'; // 关键!处理跨域
img.src = url;
img.onload = () => resolve(img);
img.onerror = (err) => reject(err);
});
};
用Promise封装一下,这样后面可以用await
确保图片加载完成,避免“图片没加载完就绘制”的问题——我之前没封装的时候,经常遇到Canvas画出来是空白的,就是因为图片还没加载好。
接下来是创建Canvas。Canvas的宽高要和原图一致,不然会拉伸图片。比如原图是1000×800像素,Canvas也要设置成同样的尺寸:
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d'); // 获取2D绘图上下文
// 设置Canvas宽高和原图一致
canvas.width = img.width;
canvas.height = img.height;
这里要注意:getContext('2d')
是获取Canvas的2D绘图环境,所有绘制操作(比如画图片、写文字)都要通过这个上下文对象来做。
第三步是绘制原图到Canvas。用ctx.drawImage()
方法把加载好的原图画到Canvas上,参数是(图片对象, 起始X坐标, 起始Y坐标, 宽度, 高度)
——因为要铺满整个Canvas,所以起始坐标设为0,宽高用Canvas的宽高就行:
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
这一步很简单,但要确保图片已经加载完成——所以前面用Promise封装加载逻辑很重要。
第四步是绘制水印。不管是文字还是图片水印,本质都是在原图上再画一层。先讲文字水印:比如要加“美食博客版权所有”的文字,需要设置字体、颜色、位置。我朋友的博客一开始要加灰色半透明文字,我是这么写的:
// 设置文字样式
ctx.font = '24px "微软雅黑"'; // 字体和大小
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'; // 半透明黑色
ctx.textAlign = 'right'; // 文字右对齐
ctx.textBaseline = 'bottom'; // 文字底部对齐
// 绘制文字:位置在右下角,距右边20px、底边20px
ctx.fillText('美食博客版权所有', canvas.width
20, canvas.height
20);
这里的textAlign
和textBaseline
是关键——我之前没设置这两个属性时,文字老是“飘”在坐标点旁边,比如想放在右下角,结果文字的左边对齐到了canvas.width
,导致文字超出画布。设置textAlign='right'
后,文字的右侧会对齐到指定的X坐标,textBaseline='bottom'
则让文字的底部对齐到指定的Y坐标,刚好贴在图片右下角,不会挡住内容。
如果是图片水印(比如品牌LOGO),逻辑和绘制原图差不多:先加载LOGO图片,再画到Canvas上。比如朋友后来想加博客的LOGO作为水印,我是这么写的:
// 加载LOGO图片(同样要处理跨域)
const logoImg = new Image();
logoImg.crossOrigin = 'anonymous';
logoImg.src = 'https://your-domain.com/logo.png';
logoImg.onload = () => {
// 设置LOGO大小(比如原图宽400px,缩成100px)
const logoWidth = 100;
const logoHeight = logoImg.height (logoWidth / logoImg.width); // 保持比例
// 绘制LOGO到左下角,距左边30px、底边30px
ctx.drawImage(logoImg, 30, canvas.height
30
logoHeight, logoWidth, logoHeight);
};
这里要注意保持LOGO的比例——直接写死宽高会让LOGO变形,所以用“宽高比=原图宽高比”来计算高度,比如原图宽400、高200,缩成宽100,高度就是50,这样LOGO不会拉伸。
最后一步是导出带水印的图片。用canvas.toDataURL('image/png')
把Canvas内容转成base64字符串,然后创建一个a
标签下载就行:
// 导出为PNG格式的base64字符串
const dataURL = canvas.toDataURL('image/png');
// 创建下载链接
const downloadLink = document.createElement('a');
downloadLink.href = dataURL;
downloadLink.download = 'watermarked-image.png'; // 下载文件名
downloadLink.click(); // 触发下载
到这一步,一张带水印的图片就生成了!我朋友的博客用这个方法后,所有用户上传的菜品图都会自动加版权水印,再也没遇到过盗图的问题。
二、进阶技巧:让水印更灵活的5个实用方法
基础流程会了,但实际项目中你可能会遇到更复杂的需求——比如要让水印旋转45度,或者批量处理10张图片,或者在移动端自适应屏幕。我再跟你分享几个亲测有效的进阶技巧。
很多人做水印时最头疼的就是“位置调不对”,比如想让文字在图片中心,结果偏上或偏左。其实用textAlign
和textBaseline
就能解决90%的对齐问题——我做活动海报生成工具时,用户要自定义水印位置,靠这两个属性省了很多事。
比如你想让文字在图片正中心,可以这么写:
ctx.font = '36px "思源黑体"';
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
ctx.textAlign = 'center'; // 水平居中
ctx.textBaseline = 'middle'; // 垂直居中
ctx.fillText('活动专属', canvas.width / 2, canvas.height / 2);
这样文字的中心点会刚好对齐到(canvas.width/2, canvas.height/2)
,比你自己算x = canvas.width/2
方便多了。
再比如想让水印在右上角,只要把textAlign
设为right
,textBaseline
设为top
,位置设为canvas.width
(距右20px)、20
(距上20px)就行:
ctx.textAlign = 'right';
ctx.textBaseline = 'top';
ctx.fillText('版权所有', canvas.width
20, 20);
我把常用的对齐组合整理成了表格,你可以直接用:
目标位置 | textAlign | textBaseline | 示例坐标 |
---|---|---|---|
正中心 | center | middle | (w/2, h/2) |
右下角 | right | bottom | (w-20, h-20) |
左上角 | left | top | (20, 20) |
有时候你需要水印更“低调”或者更有设计感,比如活动海报的水印要旋转45度,或者电商商品图的水印要半透明。这时候要用到ctx.rotate()
和rgba
颜色。
比如想让文字水印旋转45度,步骤是:保存上下文状态→移动原点→旋转→绘制文字→恢复状态。我帮朋友做活动海报时,就是这么写的:
ctx.save(); // 保存当前上下文状态(比如旋转角度、原点位置)
// 移动原点到图片中心(因为旋转是绕原点转的)
ctx.translate(canvas.width / 2, canvas.height / 2);
// 旋转45度(Math.PI/4等于45度,因为JS用弧度制)
ctx.rotate(Math.PI / 4);
// 设置文字样式
ctx.font = '36px "思源黑体"';
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 绘制文字(此时原点在中心,所以坐标是0,0)
ctx.fillText('活动专属', 0, 0);
ctx.restore(); // 恢复之前的上下文状态
这里要注意save()
和restore()
必须配对使用——不然旋转会影响后面的绘制操作,比如你再画其他元素时,会跟着旋转45度。
透明度的调整更简单,直接用rgba
颜色就行——比如rgba(0,0,0,0.2)
就是20%透明度的黑色,rgba(255,255,255,0.5)
是50%透明度的白色。我做电商商品图时,一般用0.3-0.4的透明度,既能起到版权保护作用,又不会挡住商品的细节。
如果你的页面上有10张图片要加水印,总不能一张张手动处理吧?这时候可以用Promise.all
批量加载图片,再循环处理。比如页面上有多个class="need-watermark"
的img标签:
// 选中所有需要加水印的图片
const imgs = document.querySelectorAll('.need-watermark');
// 批量加载图片(处理跨域)
const loadImagePromises = Array.from(imgs).map(img => {
return new Promise((resolve) => {
const newImg = new Image();
newImg.crossOrigin = 'anonymous';
newImg.src = img.src;
newImg.onload = () => resolve(newImg);
});
});
// 所有图片加载完成后,批量加水印
Promise.all(loadImagePromises).then(loadedImgs => {
loadedImgs.forEach((img, index) => {
// 创建Canvas
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
// 绘制原图
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// 绘制水印(比如统一加“电商专属”文字)
ctx.font = '20px "微软雅黑"';
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.textAlign = 'right';
ctx.textBaseline = 'bottom';
ctx.fillText('电商专属', canvas.width
20, canvas.height
20);
// 替换原图的src(让页面显示带水印的图片)
imgs[index].src = canvas.toDataURL('image/png');
});
});
这个方法我在电商项目中用过,批量处理20张商品图也就用了2秒,比手动处理快多了。
如果你的页面要适配移动端,比如用户在手机上上传图片,水印要跟着屏幕大小调整——这时候要用到devicePixelRatio
(设备像素比),避免水印在Retina屏上模糊。
比如在移动端,Canvas的宽高要乘以devicePixelRatio
,然后用CSS把Canvas缩放到100%宽度:
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 获取设备像素比(Retina屏是2,普通屏是1)
const dpr = window.devicePixelRatio || 1;
// 设置Canvas宽高(乘以dpr,避免模糊)
canvas.width = img.width dpr;
canvas.height = img.height * dpr;
// 缩放Canvas(用CSS缩回到原来的大小)
canvas.style.width = ${img.width}px
;
canvas.style.height = ${img.height}px
;
// 缩放绘图上下文(不然绘制的内容会变小)
ctx.scale(dpr, dpr);
// 后面的绘制逻辑和之前一样
ctx.drawImage(img, 0, 0, img.width, img.height);
ctx.fillText('移动端专属', img.width
20, img.height
20);
我做移动端海报工具时,一开始没加devicePixelRatio
,结果生成的水印在iPhone上模糊得看不清,加了之后立刻变清晰了——这个小技巧很有用, 你记下来。
这些方法我自己用了大半年,从美食博客到电商项目都试过,亲测有效。你要是按这些步骤做了,不管是文字水印还是图片水印,应该都能少踩很多坑。比如跨域问题,记得加crossOrigin="anonymous"
;对齐问题,用textAlign
和textBaseline
;旋转问题,用save()
和restore()
。如果试的时候遇到问题,比如跨域还是解决不了,或者水印位置调不好,欢迎在评论区留言,我帮你看看!
为什么用Canvas生成水印后,导出图片会报“污染的画布无法导出”的错误?
这个问题九成是跨域搞的鬼——如果你的原图存在其他域名(比如阿里云OSS、CDN),直接用img.src加载会让Canvas“染脏”,浏览器怕你盗用图片,就不让导出了。我去年帮朋友的美食博客做水印时也踩过这坑,后来查MDN才搞懂,得给Image对象加个crossOrigin=”anonymous”属性,同时让图片服务器允许跨域访问(比如OSS后台开CORS规则),这样加载的图片才不会“污染”Canvas,导出就正常了。
创建Canvas时,为什么要和原图宽高保持一致?
要是Canvas宽高和原图不一样,绘制的原图会拉伸变形——比如原图是1000×800,Canvas设成500×400,图片就会被挤成小方块,水印位置也会跟着歪到姥姥家。我之前做项目时没注意这点,结果生成的水印要么贴在图片外面,要么挡住菜品的关键细节,后来把Canvas宽高和原图对齐,立马就好了。
想让水印文字刚好在图片右下角,为什么调坐标总不对?
别光调x、y坐标,得用textAlign和textBaseline这两个“对齐神器”——比如要放右下角,把textAlign设为right(文字右边缘对齐坐标点),textBaseline设为bottom(文字下边缘对齐坐标点),然后坐标填“图片宽度-20”(距右边20px)和“图片高度-20”(距底边20px),文字就会乖乖贴在右下角。我帮朋友调水印位置时,用这方法一分钟搞定,之前算坐标算半小时还歪。
想让水印旋转45度,为什么旋转后位置总跑偏?
旋转水印得先“移原点”——Canvas默认绕左上角旋转,直接转肯定会把文字转到画布外面。正确步骤是:先save()保存当前状态,再用translate把原点移到图片中心(比如canvas.width/2、canvas.height/2),然后rotate(Math.PI/4)(45度的弧度值),画完文字再restore()恢复状态。我做活动海报时就是这么干的,旋转后的水印刚好在中心,不会跑出去。
有很多张图片要加水印,手动处理太麻烦,有没有批量方法?
用Promise.all批量加载图片就行——比如选中页面上所有带need-watermark类的图片,用Promise.all一次性加载完(记得加crossOrigin),然后循环创建Canvas、画原图、画水印,最后把生成的图片地址替换回原img标签的src。我在电商项目里批量处理20张商品图,两秒就完成了,比手动点来点去快多了。
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需!
7. 如遇到加密压缩包,请使用WINRAR解压,如遇到无法解压的请联系管理员!
8. 精力有限,不少源码未能详细测试(解密),不能分辨部分源码是病毒还是误报,所以没有进行任何修改,大家使用前请进行甄别!
站长QQ:709466365 站长邮箱:709466365@qq.com