上个月改版,运营丢来一批 1280×720 的界面截图,卡片位只给到 320×180。宽高我照 1/4 重算过,图当天被退回:截图里 12px 的小字糊成一团,竖笔断开。同批顺手试的 1024×576,像素量降到 64%,体积却从 62,744 B 涨到 79,480 B。于是我把这一页真跑起来的代码测了 16 组。
先把三个原因摆出来
- 实测结果画布读到的是
imageSmoothingQuality="low",模板第 220–224 行 0 处重采样赋值,生效的是浏览器默认档。 - 全模板只有第 224 行一处
drawImage,1280×720 到 320×180 是一次成像:320×180 出 12,310 B、160×90 出 5,023 B、12×7 出 215 B,都是一步出的。 - 画布永远等于请求值,
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×180 | 12,310 B | 画布精确,占源 19.6% |
| 640×360 | 28,596 B | 占源 45.6% |
| 1024×576 | 79,480 B | 像素量降到 64%,比源大 26.7% |
| 1280×720 | 62,744 B | 同尺寸往返,校验和同为 6e58cae9 |
图 1:1280×720 源图(62,744 B)四个档位的输出字节,虚线是源体积。
19.6% 只是这张图恰好如此,640×360 占源 45.6% 就不等比:缩放只承诺尺寸,不承诺体积。
放大和变形都在加价
图 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 章只剩测法。与其编一个看着像实测的百分比,不如把缺口摆明处:这篇关于「糊」的主张只到机制层。