3、PNG压缩策略:量化、过滤、Deflate算法,工具推荐(pngquant, pngcrush)
PNG 这格式,说实话,又爱又恨。爱它无损、支持透明,恨它体积经常比 JPG 大好几倍。我在做移动端界面移植时,经常遇到设计师丢过来一张 2MB 的 PNG 启动图,嘴里还念叨着「这是无损的呀」。嗯,无损不代表没得优化。
今天我们就来拆解一下 PNG 的压缩三板斧:量化、过滤、Deflate。搞懂这三步,你就能明白为什么 pngquant 和 pngcrush 能帮你省下 60%~80% 的体积。
3.1 PNG 压缩的整体流程
先看一张图,把整个压缩链路串起来。你想想看,一张原始位图从内存到磁盘文件,中间经历了什么?
说白了,PNG 压缩分三步走:先砍颜色(量化),再让数据更规律(过滤),最后用通用压缩算法收尾(Deflate)。每一步都有讲究,我们一个一个说。
3.2 量化:砍掉你不需要的颜色
量化,就是把一张 24 位真彩色图(1600 万色)变成 8 位索引色(256 色)甚至更少。你可能会问:「颜色少了,画质不就崩了吗?」
嗯,这里有个关键点:界面 UI 图通常颜色数很少。一个按钮、一个图标、一个背景渐变,可能本身就只有几十种颜色。我做过一个统计,移动端界面 PNG 平均颜色数不到 200 种。你强行保留 1600 万色,纯粹是浪费空间。
3.2.1 量化算法怎么选?
| 算法 | 原理 | 质量 | 速度 | 适用场景 |
|---|---|---|---|---|
| 中位切分 | 递归切分颜色立方体 | 中等 | 快 | 通用,pngquant 默认 |
| 八叉树 | 八叉树颜色聚类 | 较好 | 中等 | 渐变丰富的图 |
| K-Means | 迭代聚类中心 | 最好 | 慢 | 离线批量处理 |
| 固定调色板 | 使用预定义颜色集 | 差 | 极快 | Web 安全色 |
我个人习惯用 pngquant,它默认就是中位切分 + 抖动优化。我在项目中遇到过一张 512×512 的图标,原图 180KB,用 pngquant 压到 32KB,肉眼几乎看不出区别。你想想看,省了 80% 的体积,加载速度能不快吗?
--alpha 参数可以单独优化 alpha 通道,避免透明边缘出现锯齿。我一般用 pngquant --quality=65-80 --speed=1 input.png。
3.3 过滤:让数据更「好压」
量化之后,数据还是像素级的。Deflate 算法喜欢重复的字节序列,但原始像素数据往往不够「重复」。这时候就需要过滤(Filter)上场了。
过滤的本质是行间差分。PNG 定义了 5 种过滤模式:
- None(0):不做任何处理,原始数据直接送 Deflate。
- Sub(1):用当前像素减去左边像素。适合水平渐变。
- Up(2):用当前像素减去上方像素。适合垂直渐变。
- Average(3):用当前像素减去左边和上边的平均值。适合均匀区域。
- Paeth(4):用当前像素减去左边、上边、左上角的线性预测值。最智能,但计算量最大。
你可能会问:「那我该选哪种?」
答案是:每行单独选。PNG 编码器会对每一行尝试 5 种过滤模式,选压缩率最高的那个。这就是 pngcrush 的核心工作——它暴力尝试所有过滤组合,找到最优解。
3.4 Deflate:最后的通用压缩
过滤之后的数据,已经变成了「差值序列」。这些差值里有很多 0 和小数值,非常适合 Deflate 算法。
Deflate 是 LZ77 + Huffman 编码的组合拳:
- LZ77:找重复的字节序列,用「距离+长度」对替换。比如连续 100 个 0,会被编码成「距离=1,长度=100」。
- Huffman 编码:对出现频率高的符号用短码,频率低的用长码。进一步压缩。
这里有个坑:Deflate 的压缩级别。级别从 0(不压缩)到 9(最大压缩)。级别越高,LZ77 的搜索窗口越大,压缩率越好,但编码时间也越长。
| 级别 | 压缩率 | 编码速度 | 解码速度 | 推荐场景 |
|---|---|---|---|---|
| 0 | 0% | 极快 | 极快 | 调试用 |
| 1-3 | 低 | 快 | 快 | 开发环境 |
| 4-6 | 中等 | 中等 | 快 | 生产环境(推荐) |
| 7-9 | 高 | 慢 | 快 | 发布包、安装包 |
我个人习惯用级别 6。级别 9 虽然能多压 2%~5%,但编码时间可能多 10 倍。对于界面移植这种需要频繁迭代的场景,性价比不高。
3.5 工具推荐:pngquant vs pngcrush
这两个工具是 PNG 压缩界的「倚天剑」和「屠龙刀」。它们分工不同,我一般配合使用。
3.5.1 pngquant:量化神器
pngquant 专注于量化阶段。它把真彩色 PNG 转成 8 位索引色 PNG,同时用 Floyd-Steinberg 抖动算法减少色带。
常用命令:
# 基本用法:压缩到 256 色
pngquant input.png
# 指定质量范围(0-100),自动选择颜色数
pngquant --quality=65-80 input.png
# 保留透明通道,输出到指定目录
pngquant --ext .png --force --quality=65-80 input.png
# 速度优先(speed=1 最慢但质量最好,speed=10 最快但质量差)
pngquant --speed=1 --quality=65-80 input.png
3.5.2 pngcrush:过滤优化专家
pngcrush 不改变颜色数,它专注于过滤模式选择和IDAT 块重组。它会暴力尝试所有过滤组合,找到压缩率最高的方案。
常用命令:
# 基本用法:暴力搜索最优过滤
pngcrush -brute input.png output.png
# 指定过滤模式(0-4),减少搜索时间
pngcrush -l 9 -m 0 input.png output.png
# 只优化 IDAT 块,不重新压缩
pngcrush -rem alla -reduce input.png output.png
# 移除元数据(如 EXIF、ICC 等)
pngcrush -rem alla -brute input.png output.png
pngcrush -brute 压一张 4K 分辨率的大图,跑了 40 分钟还没结束。后来发现 -brute 会尝试所有过滤组合,对于大图来说计算量爆炸。建议大图用 -l 9 -m 0 指定过滤模式,或者先用 pngquant 量化缩小后再 crush。
3.5.3 组合拳:先量化,后 crush
我推荐的工作流是这样的:
- 第一步:用 pngquant 量化,把颜色数降到 256 色以内。
- 第二步:用 pngcrush 优化过滤和 IDAT 块。
- 第三步:对比压缩前后,确认质量可接受。
一个完整的脚本示例:
#!/bin/bash
# 批量压缩 PNG 脚本
for f in *.png; do
echo "处理: $f"
# 量化
pngquant --quality=65-80 --ext .quant.png --force "$f"
# crush
pngcrush -l 9 -m 0 "${f%.png}.quant.png" "${f%.png}.opt.png"
# 清理中间文件
rm "${f%.png}.quant.png"
done
echo "完成!"
3.6 总结:记住这三句话
- 量化砍颜色:界面图颜色少,用 pngquant 砍到 256 色以内,省 60%~80% 体积。
- 过滤找规律:pngcrush 暴力搜索最优过滤模式,让数据更「好压」。
- Deflate 收尾:级别 6 性价比最高,别盲目上 9。
嗯,这套组合拳我在多个项目里验证过,效果稳定。下次设计师再给你丢大 PNG,你就知道怎么「优雅地」把它变小了。