杂记 › 技术笔记 › 转 JPG 时透明被强制填底色:91 个质量档位在 PNG 面前不参与编码

转 JPG 时透明被强制填底色:91 个质量档位在 PNG 面前不参与编码

把一个只有描边的 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 导出体积
#ffffff5023 B
#7f7f7f4840 B
#0000004769 B
#ff00804196 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%)」。

PNG 源 JPG 转出 纯色图 2083 B 3160 B 照片 5445 B 8586 B 条带图 2576 B 5023 B

图 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。

photo_400.png 的 91 档实测 100 档 67341 B PNG 出口恒 26190 B 90 档 8586 B 10 50 90 100 质量档位

图 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 之前该防什么?

先把只差后缀的同名文件错开,撞名之后包里只留后写入的那一张;转完看一眼汇总条,它写「体积增加」的时候就是真的增加了。

📎 相关文章

后缀决定 MIME:同一份 70 字节 PNG 头改出 18 个后缀、13 种类型和 5 个未知 2026-10-23 「全部清除」后还剩 553 字节,「只去掉位置信息」连缩略图都没动 2026-10-23 序号位数只是下限、重名时 (2) 插在后缀之前:批量重命名 2000 个文件的实测口径 2026-10-23 证书指纹实测:1159 字节 DER 与 3 种摘要、4 种写法 2026-10-23 TOTP 口令差 30 秒就换码:±1 期反查能定位偏差,算法却只有 SHA-1 一档 2026-10-23