雜記 › 技術筆記 › 截圖縮到四分之一,小字糊成一團的三種原因

截圖縮到四分之一,小字糊成一團的三種原因

上個月改版,營運丟來一批 1280×720 的介面截圖,卡片位只給到 320×180。寬高我照 1/4 重算過,圖當天被退回:截圖裡 12px 的小字糊成一團,豎筆斷開。同批順手試的 1024×576,像素量降到 64%,體積卻從 62,744 B 漲到 79,480 B。於是我把這一頁真正跑起來的程式碼測了 16 組。

先把三個原因擺出來

  1. 實測結果畫布讀到的是 imageSmoothingQuality="low",模板第 220–224 行 0 處重取樣賦值,生效的是瀏覽器預設檔。
  2. 全模板只有第 224 行一處 drawImage,1280×720 到 320×180 是一次成像:320×180 出 12,310 B、160×90 出 5,023 B、12×7 出 215 B,都是一步出的。
  3. 畫布永遠等於請求值,doResize 只用第 221/222 行的寬高原值:非等比 800×600 出 69,321 B,是等比 640×360 那檔 28,596 B 的 2.42 倍——先變形,再被預設檔取樣一次。

原因一:這一頁從來沒設過取樣品質

canvas.width=w
canvas.height=h
const ctx=canvas.getContext('2d')
ctx.drawImage(originalImage,0,0,w,h)

這 4 句(第 221 到 224 行)沒有一句碰過 imageSmoothingEnabled 或 imageSmoothingQuality,全文 grep 命中 0 次。縮完 320×180 後直接讀第 223 行返回的上下文,實測就是 true 與「low」;同目錄的 upscale-image.html 第 317–318 行卻顯式寫了 true 和「high」。不是瀏覽器沒給高品質檔,是這一頁沒去要。

對小字為什麼致命:1px 的豎筆在 1/4 縮比下連一個完整像素都佔不滿,能借到的鄰域樣本全由那個預設檔決定。我最初那批卡片圖就是在調整 PNG 尺寸這一頁出的。

原因二:縮多少倍都只有一次取樣

第 224 行是模板裡唯一的繪製語句:沒有第二處 drawImage,沒有減半迴圈,也沒有按倍率拆檔的分支,4 倍、2 倍、8 倍走同一條路。實測表裡每一檔——160×90 出 5,023 B、12×7 出 215 B——都是一次成像的結果。一次核函數取樣,等於把源圖 4×4 鄰域的能量一次性壓進 1 個像素,沒有第二次機會校正筆畫。

得劃一條線:我不能說「分兩步縮會更清晰或更省位元組」,模板裡根本沒有第二步,1280→640→320 的兩步對照本輪沒跑出來,事實檔案第 1.4 節標了未實測。這條只主張:你只有一次取樣機會,那次用的是沒人賦值的預設檔。

原因三:畫布只認輸入框裡那兩個數

第 211/212 行用 parseInt 讀寬高(800.9 截成 800),第 221/222 行原樣賦給畫布,16 組實測裡畫布與解碼尺寸都精確等於請求值,321×181 也輸出 14,297 B,比 320×180 的 12,310 B 大 16.1%。

第 106 行 aspectLocked 初始為 true,第 188–193 行在鎖開時把高度回填成 Math.round(w/aspectRatio),鎖關時第 189 行直接 return,而 doResize 從不讀它——鎖只是輸入框層面的輔助,不是縮放約束:800×600 就是 16:9 的截圖被塞進 4:3 畫布,橫豎各帶一個縮放率,再交給預設檔一步取樣。

縮小不一定省體積

請求尺寸輸出位元組實測備註
320×18012,310 B畫布精確,佔源 19.6%
640×36028,596 B佔源 45.6%
1024×57679,480 B像素量降到 64%,比源大 26.7%
1280×72062,744 B同尺寸往返,校驗和同為 6e58cae9
源 62,744 B 320×180 12,310 B 佔源 19.6% 640×360 28,596 B 佔源 45.6% 1024×576 79,480 B(+26.7%) 像素量降到 64% 1280×720 62,744 B 往返

圖 1:1280×720 源圖(62,744 B)四個檔位的輸出位元組,虛線是源體積。

19.6% 只是這張圖恰好如此,640×360 佔源 45.6% 就不等比:縮放只承諾尺寸,不承諾體積。

放大和變形都在加價

800×600 69,321 B 非等比 640×360 28,596 B 等比 69,321 B 是它的 2.42 倍 1920×1080 210,255 B(3.35 倍) 2560×1440 294,510 B(4.69 倍)

圖 2:非等比請求與放大檔位的真實輸出位元組。

滑桿也管不著體積:第 64 行那個 min=10 max=100 value=90 的 range 只對 image/jpeg 與 image/webp 顯示(第 152–153 行),PNG 下整行隱藏,可第 229 行仍無條件把 parseInt(quality)/100 傳進第 236 行的 toBlob——PNG 下它改變了什麼本輪沒測,所以我不寫「拉動滑桿就能壓體積」。真在意位元組請走PNG 壓縮;滑桿數字一路傳到哪,品質檔那篇拆解另有實測。

順手測的透明 Logo

512×512 的透明 Logo(23,119 B)縮到 64×64 得 2,454 B,alpha 分佈是全透 1,941、半透 303、全不透 1,852,合計 4,096。全透佔 4,096 裡的 47.4%,輸出仍是 image/png,沒鋪白底也沒改投 JPEG;但邊緣新冒出 303 個半透像素,說明 alpha 值本身被重取樣改寫,第 331 行「縮放後 Alpha 透明背景不遺失」被判定口徑過強就是這個道理。

其餘口徑我只敢按原始碼唸:第 133 行上限 file.size > 20 * 1024 * 1024,第 216–218 行只判 w>10000,第 246 行把 shot.png 換成 shot_resized.png。執行期 toast 本輪沒抓到,事實檔案第 7 章整節標了未實測。

三條問答

Q:那 1/4 這一檔到底還能不能用?

尺寸這一頭是準的:畫布精確 320×180,介面文案是「320 × 180 | 12.0 KB」。但「縮完小字還能讀」它給不出保證——沒有第二處 drawImage,也沒有一處 imageSmoothingQuality 賦值,糊是預設狀態。

Q:放大一檔是不是比縮更划算?

210,255 B 與 294,510 B 對應 3.35 倍與 4.69 倍,而源只有 62,744 B。

Q:為什麼不給出灰階級數、邊寬、字幹 IoU 這些清晰度數字?

因為沒跑出來。探針在 512×512 放大到 1024×1024 那一組中斷,兩次分別設 --virtual-time-budget=120000 與 900000 都停在同一處,三項指標連同兩步降取樣對照全是空缺,事實檔案第 3 章只剩測法。與其編一個看起來像實測的百分比,不如把缺口擺在明處:這篇關於「糊」的主張只到機制層。

📎 相關文章

PNG 壓了三遍只減 426 位元組:無損壓縮的真實邊界 2026-10-22 WebP 轉 PNG 後體積漲了幾倍:什麼時候必須轉,怎麼把漲幅壓住 2026-10-22 同樣裁 300×300,頭像為什麼總偏右下角半格 2026-10-22 按每月 1.7 倍預估 18 個月,腳本和表格為什麼給出兩個數 2026-10-22 介面 JSON 貼成 PHP 陣列,哪些值會悄悄變形 2026-10-22