☰
批量加水印还在用在线工具?30 张图 Canvas 本地铺满实测
2026/10/19 12:14:19 网站建设 项目流程

上传、等进度条、下载压缩包,这是在线加水印的标准三步。

我把这三步全砍了。图片不出浏览器,水印用 Canvas 画,导出直接落盘。

30 张图混着 800×600 到 3840×2160 一起跑,四次实测分别是 4.57 秒、4.66 秒、4.89 秒和 4.57 秒。

页面自带的三张示例图更快,0.53 秒就结束了。

在线案例与源码:https://atomgit.com/deli007/demo_park/tree/main/codearts-image-watermark-tool
成品只有一个 index.html,双击就能用,不装任何依赖。

想自己看一遍:git clone仓库后双击 index.html,或者用python -m http.server 8000起个静态服务。

一、为什么值得做:在线加水印的两段等待,本地一段都不用等

搜「图片加水印」,排在前几页的基本都是在线工具。

它们的工作流高度一致:先把图传上去,等服务器处理,再回来下载。

原图动辄好几 MB,几十张一起传,卡在进度条上的时间比处理本身还长。

更让人犹豫的是隐私。合同扫描件、证件复印件、还没上架的商品图,一旦上传就离开了自己的电脑。

本地 Canvas 的好处很直接:图片从来没离开过浏览器,快慢只取决于这台机器的 CPU。

这个页面就是照这个思路做的,单文件 1239 行,没有外链,没有 CDN。

二、准备环境:打开码道 Web,把需求写成一段话

浏览器打开码道 Web 版:https://devcloud.cn-north-4.huaweicloud.com/chat?source=dmzntgwsf&sourcead=dmzntgwwbwz ,登录后在对话框里写需求,就能拿到一个能跑的单文件应用。

码道有三种使用方式:WebUI(浏览器对话)、TUI(终端命令行)和桌面 IDE(IDE 插件)。本文用 WebUI 版演示。

需求我写了 7 条。事后看,最值钱的是第 1 条和第 6 条。

第 1 条把「本地处理、不上传服务器」写进了产品定位,第 6 条要求页面自带三张示例图。

有了示例图,打开页面不用翻硬盘找素材,点一下就能试。

三、需求原文(可以照着复制)

做一个纯前端的「批量图片加水印」网页工具,只要一个 index.html,零依赖,所有处理都在浏览器本地完成、图片不上传服务器。功能要求: 1. 支持拖拽或点击选择多张图片(png/jpg/webp),左侧是批量队列,每张缩略图显示文件名、尺寸和状态; 2. 水印设置面板:类型可切换「文字水印 / 图片水印」;文字水印可设内容、字号、颜色、不透明度、旋转角度、平铺间距;图片水印可上传一张 PNG 作为 logo,并设缩放比例和不透明度; 3. 布局模式两种:九宫格单点位置(九个位置可选)和全图平铺; 4. 中间大画布实时预览选中图片加水印后的效果; 5. 一键导出:按原分辨率批量导出并逐个下载,显示处理总耗时; 6. 顶部提供「载入 3 张示例图片」按钮,用 canvas 直接生成三张不同尺寸的彩色示例图(1200x800、1920x1080、800x800),方便没有图片时直接试用; 7. 输入校验:拖入非图片文件、水印文字为空、字号或间距为 0 或负数时,必须给出明确的错误提示,不要静默失败。 界面要完整好看,中文界面,深色渐变背景,布局清晰。

四、页面长什么样:左队列、中预览、右设置

打开就是三栏。左边是批量队列,中间是实时预览画布,右边是水印设置。

设置面板有两条分叉:水印类型分「文字水印」和「图片水印」,布局模式分「九宫格单点」和「全图平铺」。

平铺模式靠两个 for 循环就铺满了,用的还是同一套参数。

图片水印换成一张 PNG logo,缩放比例 18%。logo 的宽度按原图宽度算,换图不会跑偏。

五、实测:30 张图导出到底要多久

我造了一批不同尺寸的图丢进去,参数统一:56px 文字水印、180px 间距、全图平铺。

批次图片组成导出张数处理总耗时
第一次800×600 / 1200×800 / 1920×1080 / 1000×1000 / 640×480 各 6 张304.57 秒
第二次同上304.66 秒
第三次同上304.89 秒
第四次同上304.57 秒
对照页面自带的示例图30.53 秒

四次之间差了 0.32 秒,这个波动比尺寸差异带来的影响还大,说明瓶颈不在某一种尺寸上。

按 30 张算,平均每张 0.15 秒左右。

导出是串行的,每张之间还留了 60 毫秒让界面刷新。

也就是说这 4.6 秒里有大概 1.8 秒是故意等出来的,真正画水印和编码的时间更短。

这个间隔我留着没删,不然后面几张一进循环页面就锁住,队列状态根本刷新不出来。

六、同一套参数,图越大水印越多

这段我单独量了一次,数的是 canvas 里 fillText 的调用次数。

原图尺寸文字水印个数(56px)图片水印个数(18%)
800×6001212
1920×10803615
3840×216011024

1200×800 那次铺了 16 个,介于第一行和第二行之间。

文字水印这一列涨得很快:800×600 铺 12 个,3840×2160 变成 110 个,差了 9 倍。

图片水印那一列几乎不动,同样放大到 4K,只从 12 个变成 24 个。

原因在参数口径上。文字水印的 56px 是绝对像素,跟图片尺寸没关系,图一大就得铺更多行更多列。

图片水印的宽度写的是W * scale,跟着原图宽度走,所以格子数基本恒定。

想在小图上精确控制水印密度,用文字水印更合适;想让密度在大小图之间保持一致,图片水印更稳。

七、核心那段代码:两层循环铺满

平铺没有任何花哨算法,就是按格子走。

conststepX=bw+Math.max(1,state.spacing);conststepY=bh+Math.max(1,state.spacing);constsx=Math.max(1,state.spacing)/2;constsy=Math.max(1,state.spacing)/2;constcols=Math.ceil((W-sx)/stepX)+1;constrows=Math.ceil((H-sy)/stepY)+1;for(letr=0;r<rows;r++){for(letc=0;c<cols;c++){drawRotatedText(ctx,txt,sx+c*stepX,sy+r*stepY,state.text.angle);}}

bw和bh来自ctx.measureText,量的是当前字号下这段文字的真实宽度。

字号乘 1.2 当行高,这一步不加的话上下两行会贴在一起。

不透明度不是单独的开关,它最后变成 rgba 的第四个参数。

functionhexToRgba(hex,a){leth=String(hex).replace("#","");if(h.length===3)h=h.split("").map(function(x){returnx+x;}).join("");constn=parseInt(h,16)||0xffffff;return"rgba("+((n>>16)&255)+","+((n>>8)&255)+","+(n&255)+","+a+")";}

#fff这种三位写法先展开成六位,再按位拆出 RGB。

导出链路的顺序是:先drawImage把原图画满整个 canvas,再drawWatermark盖上去,最后canvas.toBlob编码。

canvas 的尺寸直接取原图的自然宽高,所以导出的是原分辨率,不是预览那一版缩小过的图。

输出格式跟着源文件走,JPG 进 JPG 出,PNG 进 PNG 出,JPG 的编码质量固定在 0.92。

八、验证与踩坑:四条校验提示的原文

失败路径我一条一条点过去了,页面给的都是中文提示,不是静默失败。

四条提示的原文我抄下来了:

  • 拖入非图片文件:错误:有 1 个文件不是 PNG/JPG/WEBP 图片,已跳过
  • 水印文字清空:错误:水印内容不能为空,请输入水印文字
  • 字号改成 0:错误:水印字号必须大于 0
  • 平铺间距改成 0:错误:平铺间距必须大于 0

验证方式是把这几个值直接改掉再点导出,然后读页面上的提示节点。

第一次测我差点以为页面没校验。当时点完导出还等了三秒才去读,什么都没读到。

后来翻了代码才发现提示 2.8 秒后自动移除,是我看晚了。

第二个坑更重要。我一开始在 1200×800 的图上调好参数,换成 3840×2160 的图,水印小得几乎看不见。

56px 是写死的像素值,不会跟着图片放大,想铺满 4K 图得把字号手动往上调一大截。

这两个坑都没写进需求,是实际点出来的。

九、实测数据汇总

项目实测值
单文件体积46120 个字符,1239 行
外部依赖1 个 script 标签,0 个 link 标签
资源请求0 条
30 张导出总耗时4.57 / 4.66 / 4.89 秒
3 张示例导出0.53 秒
文字水印平铺(56px / 180px)800×600 铺 12 个,3840×2160 铺 110 个
图片水印平铺(18% / 180px)800×600 铺 12 个,3840×2160 铺 24 个
校验提示4 条,全部为中文原话

「资源请求 0 条」是我用浏览器的性能接口复核的,从打开页面到导完 30 张图,资源列表一直是空数组。

十、本地复现

双击 index.html 就能用,不需要装任何依赖。

想走静态服务:python -m http.server 8000,然后访问 http://localhost:8000 。

想看它有没有偷偷联网,按 F12 打开网络面板再导一批图,列表里不会多出请求。

十一、说点不足

文字水印的字号是绝对像素,不跟着图片尺寸走,这是最不顺手的一处。

导出格式跟着源文件走,没有单独的格式下拉框,也没有质量滑块,JPG 固定 0.92。

水印位置只做了九宫格和全图平铺两种,拖拽定位和自定义坐标都没做。

文字只支持单行,换行、多行排版、描边这些一律没有。

本机内存就是上限。几十张 4K 图一起塞进来浏览器会明显变卡,没有做分批。

没有撤销,也没有缩略图批量预览,改一个参数整批重新算。

水印一旦导出就烧进像素里了,页面上不保留任何可编辑的水印图层,想改只能重新导一遍。

直接下载试玩

这个 Demo 已经完整开源,源代码、本文档和全部截图都在公开仓库里,可以直接下载来玩:

  • Demo Park 仓库:https://atomgit.com/deli007/demo_park
  • 本案例目录:https://atomgit.com/deli007/demo_park/tree/main/codearts-image-watermark-tool

打开上面的链接,进入 codearts-image-watermark-tool 文件夹,把 index.html 下载到本地,用浏览器打开即可运行。

十二、用码道做这个页面的体会

第一条是需求里把「不上传服务器」写成硬约束。

这三个字决定了整个技术选型,交回来的页面从头到尾没出现过一次网络请求。

第二条是让页面自带示例数据。

三张示例图是页面用 canvas 现画的,我调整参数和截验收图都靠它,不用翻硬盘。

第三条是边界条件写清楚。

我把「字号为 0」「间距为 0」「非图片文件」三种情况写进需求,交回来就是四条中文提示。

第四条是绝对值和比例值要分开看。

字号写绝对像素、logo 宽度写百分比,两种口径做出的平铺效果完全不同,这个差异我是量完才写清楚的。

第五条是数据要自己量一遍。

4.57 秒这个数字点了四次才敢写,中间那次 4.89 秒说明单次结果不足以当结论。

接下来想把字号改成「按原图宽度的百分比」再加一档预设,顺便把拖拽定位补上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询