无论是保护原创图片版权,还是给企业内部系统加上防泄露标识,水印都是前端开发中经常被提出的需求。JavaScript凭借canvas强大的图形绘制能力,可以在浏览器端完成水印的添加与导出,无需依赖服务器处理。本文将介绍几种主流的实现思路,包括纯canvas绘制、上传前处理、第三方库辅助以及DOM叠加方案,并分析它们各自的适用场景。

方案一:使用Canvas绘制文字水印
这是最基础也是最常用的方案。核心思路是:把原始图片绘制到canvas上,再用canvas的绘图API在指定位置画出水印文字,最后通过toDataURL或toBlob导出带水印的新图片。
整个流程分为三步。第一步加载图片,可以是从<input type="file">读取的本地文件,也可以是网络图片(注意跨域问题,网络图片的服务器需要返回正确的CORS头,否则导出时会报错)。第二步绘制水印,利用fillText方法写文字,配合rotate方法可以让文字倾斜,配合循环可以铺满整张图片。第三步导出结果,toDataURL('image/jpeg', 0.9)的第二个参数控制压缩质量,对大图来说适当降低质量可以显著减小文件体积。
function addTextWatermark(imgSrc, text) {
const img = new Image();
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.font = '24px sans-serif';
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 平铺水印,每200像素一个,倾斜30度
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(-30 * Math.PI / 180);
for (let y = -canvas.height; y < canvas.height; y += 120) {
for (let x = -canvas.width; x < canvas.width; x += 240) {
ctx.fillText(text, x, y);
}
}
ctx.restore();
// 导出为图片
const result = canvas.toDataURL('image/jpeg', 0.9);
const output = document.createElement('img');
output.src = result;
document.body.appendChild(output);
};
img.src = imgSrc;
}
addTextWatermark('https://picsum.photos/600/400', 'ipipp.com');这种方案的优点是完全自主可控,水印样式、密度、角度都可以自由调整,且不依赖任何第三方库。缺点是代码量相对较多,如果需要更复杂的效果(比如水印拖拽、缩放、实时预览),自己实现的成本会快速上升。
方案二:用户上传前添加水印
在许多业务场景中,水印应该在图片上传之前就处理好,这样服务器存储的文件天然带水印,安全性更高。这需要结合File API读取用户选择的文件,处理后通过FormData重新上传。
具体做法是监听文件选择控件的change事件,用FileReader把文件读成dataURL,再交给canvas处理。处理完成后调用canvas.toBlob生成二进制对象,包装成FormData提交给后端接口。相比dataURL,toBlob生成的Blob体积更小,上传效率更高,推荐优先使用。
document.querySelector('#fileInput').addEventListener('change', function (e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function (ev) {
const img = new Image();
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.font = '16px sans-serif';
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.textAlign = 'right';
ctx.fillText('版权归 ipipp.com 所有', canvas.width - 10, canvas.height - 10);
// 转为Blob并上传
canvas.toBlob(function (blob) {
const formData = new FormData();
formData.append('image', blob, 'watermarked.jpg');
fetch('/api/upload', { method: 'POST', body: formData });
}, 'image/jpeg', 0.9);
};
img.src = ev.target.result;
};
reader.readAsDataURL(file);
});需要提醒的是,前端加水印只能防止普通用户拿到原图,技术用户依然可以在请求发出前篡改JS代码绕过处理。因此对安全性要求高的系统,前端水印应作为辅助手段,与后端水印策略配合使用,形成双重保障。
方案三:使用Fabric.js实现交互式水印
如果产品需要让用户自己调整水印位置、大小和透明度,纯手写canvas会非常繁琐。此时可以借助Fabric.js这样的canvas库,它提供了对象模型,每个元素都可以独立选中、拖拽、缩放,非常适合做水印编辑器。
在Fabric.js中,图片作为背景层,文字水印作为Text对象叠加在上面,天然支持交互操作。用户调整满意后,调用canvas.toDataURL即可导出最终效果。相比原生canvas的命令式绘制,这种对象式操作的开发效率要高得多。
// 需先引入 fabric.js
const canvas = new fabric.Canvas('editor');
fabric.Image.fromURL('https://picsum.photos/600/400', function (img) {
// 将图片设置为背景并缩放到画布大小
const scale = Math.min(canvas.width / img.width, canvas.height / img.height);
img.set({ selectable: false, scaleX: scale, scaleY: scale });
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
});
const watermark = new fabric.Text('ipipp.com', {
left: 50,
top: 50,
fontSize: 28,
fill: 'rgba(255,255,255,0.6)',
angle: -15
});
canvas.add(watermark);
// 用户调整完成后导出
// document.getElementById('export').onclick = function () {
// window.open(canvas.toDataURL({ format: 'jpeg', quality: 0.9 }));
// };这个方案的代价是引入了额外的库(Fabric.js压缩后约90KB),对于只需要固定位置水印的项目来说没有必要。但在素材编辑、在线设计类工具中,它的交互能力带来的收益远大于体积成本。
方案四:DOM叠加实现展示型水印
还有一种场景是图片本身不需要修改,只需要在页面上展示时看起来有水印,比如企业内网截图防护、付费预览图等。这种情况用DOM元素叠加会简单得多:在一个容器内放置图片,再用绝对定位的透明水印层盖在上面。
水印层可以用一个小的div配合CSS的background-repeat平铺生成,代码量极少。为防止用户通过删除DOM元素去除水印,可以配合MutationObserver监听水印节点的变化,一旦被删除立即重新插入,同时禁止右键和拖拽事件。当然,任何纯前端防护都无法对抗截图和录屏,这类方案的目标是提高盗用成本而非绝对禁止。
<div class="img-wrap" style="position:relative; display:inline-block;">
<img src="photo.jpg" style="display:block;" />
<div class="watermark-layer"
style="position:absolute; inset:0; pointer-events:none;
background-image:url('mark.png');
background-repeat:repeat; opacity:0.3;"></div>
</div>
<script>
// 防止水印被开发者工具删除
const layer = document.querySelector('.watermark-layer');
new MutationObserver(function () {
if (!document.body.contains(layer)) {
document.body.appendChild(layer);
}
}).observe(document.body, { childList: true, subtree: true });
</script>方案对比与选型建议
四种方案各有侧重,可以从三个维度来选择。第一个维度是水印是否需要真正写入图片文件:需要导出或上传带水印文件时,选方案一或方案二;仅展示时选方案四。第二个维度是是否需要用户交互:需要拖拽、缩放水印的编辑类场景选方案三;固定样式的自动化处理选方案一。第三个维度是安全性要求:高安全场景务必叠加后端处理,前端方案只能作为第一道防线。
性能方面也要注意,canvas处理大图片(例如4000像素以上的照片)时可能造成短暂卡顿,建议把处理逻辑放到requestIdleCallback或Web Worker允许的环节中异步执行,并在处理期间给用户明确的进度提示。另外导出时优先选择toBlob而非toDataURL,可以避免生成巨大的base64字符串占用内存。
总结来说,纯canvas方案适合大多数固定水印需求,Fabric.js适合交互式编辑,DOM叠加适合轻量展示防护,上传前处理适合与后端配合构建完整防线。理解每种方案的边界,按需组合使用,才能在开发效率和防护效果之间取得平衡。
JavaScript水印canvas图片处理图片水印修改时间:2026-09-01 23:54:42