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.1177 | 1.0000(5% 檔,touched=0) |
| 80% 檔 WCAG 對比度 | 11.9441 | 1.1108 |
| 5% 與 6% 檔被改動像素 | — | 0 |
| 匯出 PNG 位元組數 | — | 4782,與底圖相同 |
| 96 檔塌縮出的不同整數 | — | 15 |
同一條浮水印跨過深淺硬分界時最說明問題:80% 檔在暗側拿到 11.9441,在亮側只有 1.1108。同一個 alpha、同一次呼叫,兩側差出一個數量級。
圖 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 完全相同。包絡沒有外推能力,只等於最低檔那一次觀測。
圖 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 閘門後靜默失敗。