上传、等进度条、下载压缩包这是在线加水印的标准三步。我把这三步全砍了。图片不出浏览器水印用 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?sourcedmzntgwsfsourceaddmzntgwwbwz 登录后在对话框里写需求就能拿到一个能跑的单文件应用。码道有三种使用方式WebUI浏览器对话、TUI终端命令行和桌面 IDEIDE 插件。本文用 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×60012121920×108036153840×2160110241200×800 那次铺了 16 个介于第一行和第二行之间。文字水印这一列涨得很快800×600 铺 12 个3840×2160 变成 110 个差了 9 倍。图片水印那一列几乎不动同样放大到 4K只从 12 个变成 24 个。原因在参数口径上。文字水印的 56px 是绝对像素跟图片尺寸没关系图一大就得铺更多行更多列。图片水印的宽度写的是W * scale跟着原图宽度走所以格子数基本恒定。想在小图上精确控制水印密度用文字水印更合适想让密度在大小图之间保持一致图片水印更稳。七、核心那段代码两层循环铺满平铺没有任何花哨算法就是按格子走。conststepXbwMath.max(1,state.spacing);conststepYbhMath.max(1,state.spacing);constsxMath.max(1,state.spacing)/2;constsyMath.max(1,state.spacing)/2;constcolsMath.ceil((W-sx)/stepX)1;constrowsMath.ceil((H-sy)/stepY)1;for(letr0;rrows;r){for(letc0;ccols;c){drawRotatedText(ctx,txt,sxc*stepX,syr*stepY,state.text.angle);}}bw和bh来自ctx.measureText量的是当前字号下这段文字的真实宽度。字号乘 1.2 当行高这一步不加的话上下两行会贴在一起。不透明度不是单独的开关它最后变成 rgba 的第四个参数。functionhexToRgba(hex,a){lethString(hex).replace(#,);if(h.length3)hh.split().map(function(x){returnxx;}).join();constnparseInt(h,16)||0xffffff;returnrgba(((n16)255),((n8)255),(n255),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 / 180px800×600 铺 12 个3840×2160 铺 110 个图片水印平铺18% / 180px800×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 秒说明单次结果不足以当结论。接下来想把字号改成「按原图宽度的百分比」再加一档预设顺便把拖拽定位补上。