雜記 › 生活隨筆 › 淺底白字浮水印設到 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 28 天還是 29 天:1 月 31 日加一個月要自己換算 2026-10-28 字帖每頁放幾個字:預設 A4 是 20 個,不是 100÷4=25 2026-10-27 同一天既是週末又被登記成補班,工作日計算器聽誰的 2026-10-27 倒數計時裡的 86400 秒一天,碰上夏令時間會少給你 1 天 2026-10-27