杂记 › 生活随笔 › 浅色底上的白字水印设成 5% 等于没加:导出 4782 字节与底图一字节不差

浅色底上的白字水印设成 5% 等于没加:导出 4782 字节与底图一字节不差

5% 那一档不是"淡到看不见",是 touched=0

浅底图加白字水印是设计稿里最常见的组合。把透明度滑杆推到 5%,导出得到的 PNG 是 4782 字节;同一张尺寸、没有水印的底图导出来也是 4782 字节,一字节不差。度量脚本记的是 touched=0,画面里没有任何像素被改动。6% 那一档同样是 touched=0。

把 96 档透明度全扫一遍,浅底上真正落下来的整数只有 15 个不同取值,其余档位互相塌缩。

混合这件事不是模板做的

读代码找混合函数会失望:整份模板只有 293 行与 461 行两处 ctx.globalAlpha = opacity,没有 getImageData,也没有手写的 dst*(1-a)+src*a。逐像素的合算是浏览器合成器完成的,模板只把 alpha 交出去。

96 档全扫共 1152 个实测点,拿理想的 round() 去比,偏差 0 命中 828 次,占 71.875%;偏差 -1 命中 240 次,+1 命中 80 次,绝对值最大是 1 level。

同一个 alpha 值走 fillText 与走 drawImage,96/96 个平台值逐一相等,文字路径和 Logo 路径没有第二套规则。#ffd700 这类彩色 Logo 的三个通道各自独立落值,B 通道跨度只有 18。

对比度是量出来的

白字水印压在深底(RGB 20)与浅底(RGB 240)上,差别不是看得出看不出,而是 WCAG 比值能爬多高。要复核这些档位,可以在图片加水印页上传一张纯色底图,比对最低两档的导出字节数。

实测项暗侧 RGB 20亮侧 RGB 240
扫描首档 WCAG 对比度1.11771.0000(5% 档,touched=0)
80% 档 WCAG 对比度11.94411.1108
5% 与 6% 档被改动像素—0
导出 PNG 字节数—4782,与底图相同
96 档塌缩出的不同整数—15

同一条水印跨过深浅硬分界时最说明问题:80% 档在暗侧拿到 11.9441,在亮侧只有 1.1108。同一个 alpha、同一次调用,两侧差出一个数量级。

暗侧首档 暗侧 80% 亮侧首档 亮侧 80% 1.1177 11.9441 1.0000 1.1108 WCAG 对比度 白字,96 档两端

图 1:同一条白字水印在深浅两侧的实测对比度。

平铺的"间距"不是净空

平铺步长是 sx = measureText 宽 + gapx、sy = 字号 + gapy。字号 48 时盒子高 48,而 Watermark 的墨迹高只有 35,于是滑杆上 120 px 的名义垂直间距,实际净空是 133 px。

gap 拉到 400(滑杆的合法值)时覆盖率是 0.0000%。不是步长算错,而是循环起点被硬编码成 -wW 与 -wH:起点和步长一起变大,第一格直接落在画面之外。

旋转只转图元、不转步长。字号 48、gap 40 这组参数下,rot0 有 254 条空行;rot15、30、45、135 在同样的 gap40 已经是 0 空行 0 空列。覆盖率从 16.1331% 涨到 17.9763%(45°)与 19.4256%(135°)。

能不能反解出原图:不能,但能量出距离

只看被水印完全压住的 2963 个像素。单帧反解的误差放大系数正好是 1/(1-a):5% 档平均误差 0.270,80% 档 1.310,最大 5。

想靠后处理把水印洗掉,三步全部帮倒忙。拿 80% 档那一帧量平均逐通道误差:

  • 原始帧 78.461
  • 对比度拉伸后 79.839
  • 反相后 109.599
  • 阈值化后 111.953
  • 6 档最小二乘联合 0.467,最大误差从 153 降到 1

只有多档联合把误差压了下去,但那是 6 次不同 alpha 的取样拟合,不是还原原图。

另一个结果:6 帧逐通道取 min 做包络,平均误差 4.480,与单独那一帧 5% 档的 4.480 完全相同。包络没有外推能力,只等于最低档那一次观测。

原始 拉伸 反相 阈值化 6 档联合 78.461 79.839 109.599 111.953 0.467 平均逐通道误差 80% 档那一帧

图 2:三步后处理都比原始帧更糟,只有 6 档联合把误差压低。

导出这条路上没有闸门

导出只有一行 toBlob(..., 'image/png'),quality 实参是 undefined,文件名形如 watermarked_400x400.png,工具里没有任何 JPEG 分支。file.size、img.width、尺寸 clamp 在模板里一次都没出现,所以 4000×4000 的底图照样一路导出 1580930 字节的 PNG。

坏图也没人拦。207 至 215 行只绑了 onload,没有绑 onerror;72 字节的 corrupt.png 过了 MIME 门之后静默失败,实测 4000 ms 内 baseImage 恒为 null。

MIME 门是一次不锚定的子串搜索,204 行与 232 行都是 file.type.match(/image\//)。同一份 PNG 字节,声明成 x-image/png 与声明成 image/png,得到完全相同的 400x400 / 4659 像素 / 10676 字节;写成 application/image+x 反而被拒。

"批量"、拖角、缩放滑杆,三个都不成立

文件控件没有 multiple,onchange 与 drop 都只取 files[0],一次塞 3 个文件只加载第一张。

Logo 模式拖右下角,wmScale 从 1 变到 1.4951,可导出的 Logo 框仍然是 60x60@170,170,仍然是 3600 像素、114858 字节。文字模式更干脆:5、30、100 三档"水印缩放"滑杆导出的图逐字节相同,都是 4659 像素、121019 字节,而滑杆读数照写 60%——wmScale 在整份模板里没有任何回写。

这些数字之外的事

本篇只报像素层面的实测档位,不讲水印摆在哪、密度怎么权衡,那些是防盗图那篇旧文的范围;这里也不给任何"合适透明度":浅底 5% 与 6% 都是 touched=0,深底同一档却能量出 1.1177,差别由底色决定,不由某个通用数值决定。

Q:5% 和 6% 到底差在哪?

浅底 RGB 240 上两档都是 touched=0,导出 4782 字节、与底图一致,差别只在滑杆读数。

Q:加了水印的图能被算回原图吗?

不能。单帧反解的放大系数是 1/(1-a),拉伸 79.839、反相 109.599、阈值化 111.953 三步都比原始 78.461 更糟;6 档最小二乘压到 0.467,那是多次取样的拟合。

Q:导出为什么没有体积与坏图限制?

只有 image/png 这一条编码路径,quality 是 undefined,也没有 file.size 上限;<img> 没绑 onerror,坏图过 MIME 门后静默失败。

📎 相关文章

横图裁掉 25%、竖图裁掉 43.75%:7 个拼图模板逐格算一笔几何账 2026-10-28 1 月 31 日加一个月落在哪天?这页只给天数,28 天与 29 天差 86400 秒 2026-10-28 字帖每页放几个字:默认 A4 是 20 个,不是 100÷4=25 2026-10-27 同一天既是周末又被登记成补班,工作日计算器听谁的 2026-10-27 倒计时里的 86400 秒一天,碰上夏令时会少给你 1 天 2026-10-27