游侠网云服务,免实名免备案服务器 游侠云域名,免实名免备案域名

统一声明:

1.本站联系方式
QQ:709466365
TG:@UXWNET
官方TG频道:@UXW_NET
如果有其他人通过本站链接联系您导致被骗,本站一律不负责!

2.需要付费搭建请联系站长QQ:709466365 TG:@UXWNET
3.免实名域名注册购买- 游侠云域名
4.免实名国外服务器购买- 游侠网云服务
前端JavaScript图片水印生成具体指南:超详细步骤+可复用代码

一、用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);
  • 这里的textAligntextBaseline是关键——我之前没设置这两个属性时,文字老是“飘”在坐标点旁边,比如想放在右下角,结果文字的左边对齐到了canvas.width

  • 20
  • ,导致文字超出画布。设置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张图片,或者在移动端自适应屏幕。我再跟你分享几个亲测有效的进阶技巧。

  • 让水印“听话”:位置与对齐的秘诀
  • 很多人做水印时最头疼的就是“位置调不对”,比如想让文字在图片中心,结果偏上或偏左。其实用textAligntextBaseline就能解决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

  • 文字宽度/2
  • 方便多了。

    再比如想让水印在右上角,只要把textAlign设为righttextBaseline设为top,位置设为canvas.width

  • 20
  • (距右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的透明度,既能起到版权保护作用,又不会挡住商品的细节。

  • 批量处理多图:用Promise.all解决加载问题
  • 如果你的页面上有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";对齐问题,用textAligntextBaseline;旋转问题,用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张商品图,两秒就完成了,比手动点来点去快多了。