质量拖到最右,吐出来的是原文件
要给一个卡体积上限的表单交图,这图已压过一轮。想再挤点余量,最顺手的动作是把质量滑到最右——最高画质,按理不该比 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 的合成照片,逐档实测:
| 滑块 | 编码器质量 | 输出字节 | 对原图 |
|---|---|---|---|
| 80 | 0.8 | 143525 B | 省 31.3% |
| 90 | 0.9 | 204727 B | 省 2.0% |
| 99 | 0.99 | 333872 B | 大 59.8% |
| 100 | 不编码 | 208986 B | 0 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。
图 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 透明背景的,还有要改分辨率的——这三件它都不做,这一族四个入口共用同一个模板,换入口不换结果;通用取舍见这篇压缩使用指南。