把一个只有描边的 logo 转成 JPG 发给合作方,对方回了一句「背景怎么是白的」。源图那块区域确实是透明的,问题出在转 JPG 这一步:它不接受「留着透明」,而是挑一种底色把透明像素填满。更别扭的是同一张图只换底色能差出几百字节,质量滑杆拉到 90 还是 10,对 PNG 出口一个字节都不影响。
白底不是编码器加的,是画布上先铺了一层底色
起点不在压缩,在画布。模板第 230 到 232 行先用底色把整张画布 fillRect 涂满,再把原图 drawImage 压上去,alpha=0 的点没有机会留空,落成的就是底色本身。采样图里这些点记的 RGB 是 (255,0,0):填白底读回 (255,255,255)、填 #ff0080 读回 (255,0,128),24 个采样点的输出 alpha 一律是 255,逐字节精确。
勾选框在切到「转为 JPG」那一刻变成 checked=false 且 disabled=true,提示换成「JPG 不支持透明,透明区域自动填充下方底色」;点了那个灰掉的框,读回来仍是 checked=false,shouldFillBg() 恒为 true。切到「转为 PNG」时底色选择器又 disabled=true,三个出口里它真正能动的只有 WebP 且不保留透明那一种。
只换底色就出 4 个体积,转 JPG 也未必变小
填色同时改体积。那张条带图的源是 2576 B,在 图片格式转换 里把质量停在 90 不动、只换底色走 JPG 出口,拿到的是 4 个互不相同的字节数。
| 填充底色 | JPG 导出体积 |
|---|---|
#ffffff | 5023 B |
#7f7f7f | 4840 B |
#000000 | 4769 B |
#ff0080 | 4196 B |
方向也是反的。质量 90 下 6 张真实 PNG 走 JPG,5 张变大——2083 B 到 3160 B、5445 B 到 8586 B;唯一变小的那张从 899899 B 到 216635 B,也只到 4.15 倍。16 张一批的汇总条自己写着「合计 32.5 KB → 49.4 KB(体积增加 52%)」。
图 1 三张 PNG 的源体积与质量 90 下的 JPG 转出体积,横条按 8586 B 等比缩放。
91 个档位在 PNG 面前一个都不参与编码
滑杆是 min=10、max=100,读回来是 step="1",可取的整数档位从 10 到 100 共 91 个,把 value 改成 5 会被浏览器夹回 10。选中「转为 PNG」时质量那一行 display=none,第 235 行把传给 toBlob 的 quality 换成 undefined——纯色图 10 / 55 / 90 / 100 四档全是 10606 B、照片四档全是 26190 B。
图 2 照片那张(源 5445 B)从 10 到 100 全枚举的 JPG 字节曲线,虚线是 PNG 出口恒定值。
JPG 那侧才有曲线:91 档全枚举得到 91 个互不相同的字节数,10 档 2040 B、90 档 8586 B、100 档 67341 B,单档最大跳变落在 99 到 100 之间,多出 38192 B。纯色图上 10 到 60 档全是 3159 B、70 到 90 档全是 3160 B,91 个位置只出 3 个体积;WebP 也不单调,10 档 1392 B 比 90 档 1294 B 大,100 档的 520 B 反而最小。
问它要 HEIC,它默默还你一张 PNG
目标格式只有 3 个按钮,data-mime 是 image/png、image/jpeg、image/webp,第 65 行的后缀映射也只有 png、jpg、webp 三项。越过界面把 targetMime 设成映射外的值,canvas.toBlob 不报错也不提示:问 image/heic、image/avif、image/tiff 时全部静默回退成 image/png,同一张纯色图都是 10606 B、文件头 89 50 4e 47,只有 image/webp 真出 1294 B 的 RIFF。MIME 落在映射外时导出名直接是 solid_640.undefined,而界面第 9 行的支持列表也只写 PNG / JPG / WebP / BMP / GIF / SVG,不含 HEIC。
入口只看声明不看内容,唯一的判断是第 167 行的 file.type.startsWith('image/'):声明 image/heic、内容是那张 2083 B PNG 的照收,640×640 读入;type 为空字符串的合法 PNG 反被跳掉,只留一条 toast「已跳过非图片文件:no_type」。模板里 MAX、maxSize、limit 与对 blob.size 的比较全部 0 命中,没有体积上限也没有张数上限。
两张图撞成同一个名字,包里就只剩一张
导出名只替换最后一个点之后的部分:archive.tar.gz 变 archive.tar.jpg,连尖括号和引号都原样留着——<b>&"x".png 转出后是 <b>&"x".jpg。叠上那 3 项后缀,同批里的 same.png 和 same.jpg 都导出成 same.jpg,于是 zip.file() 被调用 2 次而最终条目只有 1 个:后写入的 8948 B 覆盖先写入的 3160 B,整包 9062 B,两张成员图合计本该 12108 B,被吃掉的那张没有提示。只有 1 张时不打 ZIP,第 274 行 typeof JSZip 取不到依赖时也退化成只下第 1 张。
尺寸听解码器的:9 张输入的转出宽高与 naturalWidth、naturalHeight 逐个相同,代码里没有缩放路径,真要改尺寸得走 图片尺寸调整;而 exif6_400x300.jpg 的 SOF 写 400×300、浏览器解成 300×400,三个出口就全是 300×400。
三个还会被再问一遍的点
Q:带透明的 logo 该走哪个出口?
只有 PNG 和保留透明的 WebP 能让背景不存在,JPG 那侧一定有底色落进透明区。非 JPG 不可就把底色改成投放位置的背景色,比默认白底更难看出接缝。
Q:选 PNG 是不是等于什么都没动?
编码那一层不看滑杆也不填色,源 alpha 逐档原样写出,但这不等于原样:全透明那档的 RGB 在画布上已被解码器清成 (0,0,0),出口一律写成 RGBA,源图没有 alpha 通道的也补一层——纯色图从 2083 B 涨到 10606 B 主要是这层容器开销。
Q:批量转 JPG 之前该防什么?
先把只差后缀的同名文件错开,撞名之后包里只留后写入的那一张;转完看一眼汇总条,它写「体积增加」的时候就是真的增加了。