批量加水印还在用在线工具?30 张图 Canvas 本地铺满实测

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

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

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

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

在线案例与源码:atomgit.com/deli007/dem... 成品只有一个 index.html,双击就能用,不装任何依赖。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

text 复制代码
做一个纯前端的「批量图片加水印」网页工具,只要一个 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 张 30 4.57 秒
第二次 同上 30 4.66 秒
第三次 同上 30 4.89 秒
第四次 同上 30 4.57 秒
对照 页面自带的示例图 3 0.53 秒

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

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

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

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

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

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

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

原图尺寸 文字水印个数(56px) 图片水印个数(18%)
800×600 12 12
1920×1080 36 15
3840×2160 110 24

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

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

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

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

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

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

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

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

js 复制代码
const stepX = bw + Math.max(1, state.spacing);
const stepY = bh + Math.max(1, state.spacing);
const sx = Math.max(1, state.spacing) / 2;
const sy = Math.max(1, state.spacing) / 2;
const cols = Math.ceil((W - sx) / stepX) + 1;
const rows = Math.ceil((H - sy) / stepY) + 1;
for (let r = 0; r < rows; r++) {
  for (let c = 0; c < cols; c++) {
    drawRotatedText(ctx, txt, sx + c * stepX, sy + r * stepY, state.text.angle);
  }
}

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

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

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

js 复制代码
function hexToRgba(hex, a) {
  let h = String(hex).replace("#", "");
  if (h.length === 3) h = h.split("").map(function (x) { return x + x; }).join("");
  const n = 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 已经完整开源,源代码、本文档和全部截图都在公开仓库里,可以直接下载来玩:

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

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

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

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

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

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

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

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

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

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

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

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

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

相关推荐
鸿蒙开发1 小时前
扫描 PDF 看得见却搜不到?用 Tesseract.js + PDF.js 生成可搜索 PDF
前端·react.js
小万7421 小时前
ECC(Everything Claude Code)深度解析:把编程 Agent 从“聊天写代码”拉进工程现场的 Harness 系统
前端
用户2145549310151 小时前
VeapAI 自然语言操作实战|第 02 篇:语义层——把系统讲给模型听的四个构件
前端·后端
定栓2 小时前
Webpack迁到Vite第七节(一):全家桶移除 — 配置文件与构建核心
前端·webpack·node.js
码艺-Alimjan2 小时前
维吾尔语 RTL 排版的隐形利器:`\u200F`
前端·javascript
alwaysrun2 小时前
Web之wasm-pack详解
前端·rust·web·wasm
广州华水科技2 小时前
如何通过单北斗GNSS实现精准的变形监测?
前端
云和数据.ChenGuang3 小时前
print(f“a+b=“,result) print(f“a+b={result}“)区别
java·前端·人工智能·python·深度学习
速易达网络3 小时前
基于 Vue3 + Flask + ECharts 构建政务 OA 系统门户
前端·echarts·政务