雜記 › 技術筆記 › 把品質調到 100 再壓一次,圖片其實一個位元組都沒動

把品質調到 100 再壓一次,圖片其實一個位元組都沒動

品質拖到最右,吐出來的是原始檔

要給一個卡體積上限的表單交圖,這圖已壓過一輪。想再擠點餘量,最順手的動作是把品質滑到最右——最高畫質,按理不該比 80 檔差。實跑:800×600 的照片輸入 208986 B,輸出 208986 B,節省 0 B。

第 180 行寫著 if (quality >= 1) {,命中後就是 item.blob = item.file;,resultSize 取 item.file.size,一次編碼都不做。那個「無損」按鈕(第 27 行的 setQuality(100))不是最高畫質檔,是原始檔直通。

品質值到編碼器之間只有兩步

數字解析只有一處,第 178 行 parseInt(value) / 100。parseInt 在除法前面,小數尾巴先被砍:99.5 變 99,80abc 變 80。空值和 abc 得 NaN,而 NaN >= 1 是 false,過不了第 180 行那道門,被原樣當品質交給 canvas.toBlob。Chrome 對這類越界值統一按自家預設檔編碼:NaN、-0.05、1.5 三檔都是 214532 B,介於 0.9 檔的 204727 B 與 0.95 檔的 253301 B 之間。

程式碼側後備 0 處:Math.min、Math.max、clamp 全搜不到,[10,100] 全靠 range 控制項扛著。寫值再回讀能看出它替程式碼擋了多少:101→100,-5→10,abc→55,55.5→56,1e2→100。

高質檔為什麼會把體積壓回去

同一張 800×600 的合成照片,逐檔實測:

滑桿編碼器品質輸出位元組對原圖
800.8143525 B省 31.3%
900.9204727 B省 2.0%
990.99333872 B大 59.8%
100不編碼208986 B0 B
80 檔 143525 B 90 檔 204727 B 99 檔 333872 B 越過參考線 100 檔 208986 B 直通 原圖 208986 B

圖 1|合成照片 800×600 的實測位元組;100 檔那根不是編碼結果,是原封搬來的輸入。

更扎眼的是壓狠過的小圖:851 B 的 64×48(0.1 檔產物)在 99 檔出 1284 B,漲 433 B、50.9%。體積回退只寫在 PNG 分支(第 205 行),JPEG 與 WebP 共用的 else 分支 0 處比較大小,變大的那份照樣成為下載物。這組檔位我是在JPG 壓縮這一頁跑的,位元組都能重放。

PNG 這條路先做了一次 JPEG

PNG 分支先有損中轉一趟:第 197 行 adjustedQuality = quality * quality,第 198 行把畫布編成 JPEG,再畫回 canvas 出 PNG。第 214 行的第二次 toBlob 只傳 2 個參數,PNG 編碼器從頭到尾沒收到 quality。

原始 PNG 2597 B 中轉 JPEG 0.64 2070 B PNG 編碼無品質 8531 B 第 205 行回退 2597 B 透明像素 alpha 5013 → 0 透明區亮度 0 → 1,介面只剩「無變化」

圖 2|透明 logo 240×240 的位元組鏈,合成圖 + headless Chrome 實測。

鏈子上 larger_than_input 記 yes,第 205 行在這時把原始位元組搶回來。透明也是中轉那趟沒的:5013 個全透明像素輸出 0 個,透明區平均亮度從 0 變 1,等於填成不透明黑。不帶透明的 PNG 也一樣變大,2114 B → 3340 B,同樣靠回退收場。

丟的是中繼資料,不動的是尺寸

給 JPEG 手工插了 APP1 的樣本,輸入側 marker_FFE1、Make 與 GPS 哨兵全是 yes,重編碼後三個都變 no,位元組從 2299 B 降到 1794 B。Orientation=6 那張固有 64×48,解碼給頁面的已是 48×64,轉正發生在範本之前。輸出統一由 Chrome 寫入 JFIF:units=0、xd/yd 為 1×1,原圖的 300dpi 這類數值不再存在(真機原圖未測)。

尺寸側很穩:兩處 drawImage 全是三參形式,0 處縮放或重取樣,範本裡 resize、scale 命中 0 處,800×600 進 800×600 出,PNG 雙編碼後仍然 240×240。要改解析度得換條路,這篇調尺寸的記錄 對得上那種需求。

「節省」在兩個地方不是同一個算法

單條判據是 originSize > resultSize,嚴格大於才寫 "-63.9 KB",相等和變大一起落進「無變化」,差值從不露面。批次是另一套:第 252 行 saved = totalOrigin - totalResult,第 258 行的位元組套了 Math.abs(saved),負節省被寫成正數,百分比那個 pct 卻沒取絕對值,負號留著,於是同一列讀出「節省: 433 B (-50.9%)」。分母也更寬:第 250 行 Σorigin 算進所有檔案,第 251 行用 originSize 頂替沒壓縮的那些,百分比天然低於直覺值。

批次的門檻只有兩道,下載不打包

檔案數上限 0 處:沒有 MAX、沒有 length 截斷。門檻只有第 77 行的單張 20MB(嚴格大於才拒,正好 20971520 B 放行)與第 73 行的 MIME 白名單。那條正規表達式沒有錨點:image/jpg、gif、heic、大寫 IMAGE/PNG 都被拒,data/image/pngx 反倒能過,被原樣當 mime 交給編碼器。

「全部壓縮」是第 240 行 for...of 加 await 的串行,0 併發、0 進度條。批次下載是 N 次獨立觸發,第 280 行 setTimeout(..., i * 300) 把節拍硬編碼成 300ms,範本裡 zip、JSZip 命中 0 處,也就沒有打包。檔名只走第 268 行的正規表達式,name 變 name_compressed.ext;第 87 行 Date.now() 加 4 位隨機的 id 只做 DOM 鍵、不進下載名,於是無副檔名的 noext 原樣照抄、兩個同名 noext 分不出彼此,a.tar.gz 只動最後一段。

Q:WebP 是不是比 JPEG 小很多?

同 0.8 檔兩列輸出是 JPEG 143525 B、WebP 140356 B,差 2.2%。但兩條輸入不同源(208986 B 與 151210 B),位元組又來自同步 toDataURL,所以 2.2% 只看量級,宣傳的 25-35% 在這組樣本裡確認不了。WebP 與 JPEG 共用同一個 else 分支、無專屬參數,透明倒是留住了,5013 → 5013。

Q:那「無損」按鈕到底該不該按?

只在根本不想重編碼的時候按,它給的就是原始檔。想減體積的話,99 與 100 這兩檔一個把 208986 B 頂到 333872 B、一個位元組不動,都算不上「近乎無損」。程式碼裡 101、150 同樣直通,只是滑桿鉗到 100,讓你碰不到。

Q:什麼時候不該用這一頁?

要留 EXIF、GPS 或相機資訊的,要留 PNG 透明背景的,還有要改解析度的——這三件它都不做,這一族四個入口共用同一個範本,換入口不換結果;通用取捨見這篇壓縮格式怎麼選。

📎 相關文章

PNG 壓了三遍只減 426 位元組:無損壓縮的真實邊界 2026-10-22 WebP 轉 PNG 後體積漲了幾倍:什麼時候必須轉,怎麼把漲幅壓住 2026-10-22 截圖縮到四分之一,小字糊成一團的三種原因 2026-10-22 同樣裁 300×300,頭像為什麼總偏右下角半格 2026-10-22 按每月 1.7 倍預估 18 個月,腳本和表格為什麼給出兩個數 2026-10-22