Feb
1
在上次讨论iDOT多线程png时,搜索到另一个领域的文章:iOS减包实战Compress PNG Files作用分析。其中提到了XCode构建过程中pngcrush的压缩过程会对png文件逆优化,就包括将png更新为iDOT格式(上文还不清楚iDOT的含义)。该文章建议的修复方案是修改资源属性,让构建过程对png不做处理,但是显然pngcrush对png渲染肯定有正向收益的,本文的目的即分析pngcrush的逆优化的实际缺陷与修复方案。(结合上文阅读)
pngcrush行为
要测试pngcrush行为,我们准备了一张正常的png图片标记为source.png,并且也将其投入tinypng压缩,生成source.tiny.png。这里引入tinypng的原因是我们绝大部分图片都会投入tinypng进行一次压缩,并且tinypng确实能做到不俗的压缩程度。他们的属性如下:
我们看tinypng做了什么:
1. 将32位图像转换为了8位调色版图像。IDAT像素数据原始长度便缩小了75%。
2. 调色版chunk添加,即PLTE与tRNS。
3. 移除了其他所有的辅助chunk。
当然,tinypng也有一个显然的缺陷:生成的图片均无filter,所有的数据filter均为0。这个可能的原因是filter编码逻辑运行成本:对于最最最最简单的filter逻辑,编码成本是无filter的5倍;最稍微追求压缩率的filter逻辑,则是O(n^2),n即行起步的成本了,因此tinypng作为在线服务,没有追求filter。而本地工具或者ci工具,则可以尝试追求filter。
我们分别对这两张图执行:xcrun -sdk iphoneos pngcrush -iphone in.png out.png。要注意的是pngcrush处理过的png pngcheck不认为是合法的png了,因此需要手动开文件解析。得到source.apple.png与source.tiny.apple.png:
同时也测试了24位图像,可以为pngcrush行为得到结论:
1. pngcrush的处理区分为调色板图像与像素图像。
2. 像素图像一律转换为32位CgBI图像,IDAT数据重新处理与拆解,会新增CgBI chunk与iDOT chunk。
3. 调色板图像保持不变,IDAT数据重新处理与拆解,会新增iDOT chunk。
4. 其他chunk不做删减。
关键缺陷与优化方案
上文描述的pngcrush行为,细说来看,都存在文件大小负优化的可能。大体罗列包括:
- 转换CgBI图像:CgBI图像一定是32位的,对于原本是32位的png还好,对于原本是24位的png如照片类,像素储存大小直接暴涨1/3。CgBI的目的是解码后像素数据不经过位图中转直接推入GPU使用,对解码渲染流程有收益。
- 转换iDOT图像:即上次提到的多线程技术,对解码性能有收益。但是iDOT格式需要对IDAT数据段进行拆解,对数据压缩率有损失,也会带来多余的IDAT信息占用。
- filter能力缺失:pngcrush与tinypng有一样的问题,不能动态计算合适的filter来完成高压缩率,只能固定一个filter。甚至有一个-f [num]参数来指定全体使用那个filter。而pngcrush的默认逻辑是使用filter = 1。有的图像试了一下,只要指定全体使用filter = 0,文件就会小一些,但是pngcrush连这个程度的filter测算都没有。另外根据之前对iDOT的探索,filter = 345实际不可用。
结合以上分析,我们在CgBI与iDOT都有必要的前提下进行讨论。在XCode构建行为中pngcrush处理前后,我们还可以通过脚本进行什么优化呢?
pngcrush处理前:
1. 使用tinypng或者保持和tinypng一致的逻辑,尽可能的将可能转换为调色板格式并且不关注解码性能的图像,均转换为调色板8位图。
2. 删除所有多余的chunk,pngcrush不会对chunk做处理。
pngcrush处理后:
0. 核心是二次重建IDAT与相关数据。
1. 数据执行filter测算逻辑,找到更合适的filter来继续提升压缩率。这一段应该能找到适当的开源代码。
2. 同时修改适当的iDOT配置来提升解码性能。
3. 同时保持CgBI格式的优化。
pngcrush行为
要测试pngcrush行为,我们准备了一张正常的png图片标记为source.png,并且也将其投入tinypng压缩,生成source.tiny.png。这里引入tinypng的原因是我们绝大部分图片都会投入tinypng进行一次压缩,并且tinypng确实能做到不俗的压缩程度。他们的属性如下:
//原始图像 32位rgba图像
$ pngcheck -vv source.png
File: source.png (1107226 bytes)
chunk IHDR at offset 0x0000c, length 13
1535 x 2480 image, 32-bit RGB+alpha, non-interlaced
chunk cHRM at offset 0x00025, length 32
White x = 0.31269 y = 0.32899, Red x = 0.63999 y = 0.33001
Green x = 0.3 y = 0.6, Blue x = 0.15 y = 0.05999
chunk gAMA at offset 0x00051, length 4: 0.45454
chunk sRGB at offset 0x00061, length 1
rendering intent = perceptual
chunk IDAT at offset 0x0006e, length 1107096
zlib: deflated, 32K window, maximum compression
row filters (0 none, 1 sub, 2 up, 3 avg, 4 paeth):
0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 2 2 1 1 1 1
2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 4 1 1 2 2 2 2
......
2 4 1 4 4 4 4 1 2 2 4 4 2 2 4 2 4 4 4 4 4 4 1 4 1
4 1 4 4 4 4 4 2 4 4 1 4 2 4 4 1 4 2 2 1 2 2 2 2 4
3 2 2 2 1 4 2 2 1 2 2 2 1 4 2 2 2 1 2 2 3 2 2 1 1
1 2 1 2 4 2 1 2 2 1 4 1 2 1 1 1 4 1 4 1 1 1 4 2 4
4 1 4 1 4 (2480 out of 2480)
chunk IEND at offset 0x10e512, length 0
No errors detected in source.png (6 chunks, 92.7% compression).
//tinypng图像 8位rgba调色板图像
$ pngcheck -vv source.tiny.png
File: source.tiny.png (151689 bytes)
chunk IHDR at offset 0x0000c, length 13
1535 x 2480 image, 8-bit palette, non-interlaced
chunk PLTE at offset 0x00025, length 765: 255 palette entries
chunk tRNS at offset 0x0032e, length 59: 59 transparency entries
chunk IDAT at offset 0x00375, length 150784
zlib: deflated, 32K window, maximum compression
row filters (0 none, 1 sub, 2 up, 3 avg, 4 paeth):
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
......
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 (2480 out of 2480)
chunk IEND at offset 0x25081, length 0
No errors detected in source.tiny.png (5 chunks, 96.0% compression).
我们看tinypng做了什么:
1. 将32位图像转换为了8位调色版图像。IDAT像素数据原始长度便缩小了75%。
2. 调色版chunk添加,即PLTE与tRNS。
3. 移除了其他所有的辅助chunk。
当然,tinypng也有一个显然的缺陷:生成的图片均无filter,所有的数据filter均为0。这个可能的原因是filter编码逻辑运行成本:对于最最最最简单的filter逻辑,编码成本是无filter的5倍;最稍微追求压缩率的filter逻辑,则是O(n^2),n即行起步的成本了,因此tinypng作为在线服务,没有追求filter。而本地工具或者ci工具,则可以尝试追求filter。
我们分别对这两张图执行:xcrun -sdk iphoneos pngcrush -iphone in.png out.png。要注意的是pngcrush处理过的png pngcheck不认为是合法的png了,因此需要手动开文件解析。得到source.apple.png与source.tiny.apple.png:
//原始图像 32位rgba图像 pngcrush处理后
//还是32位rgba图像 标记
//chunk列表:
CgBI //new
IHDR
gAMA
sRGB
cHRM
iDOT //new
IDAT x N //所有的filter = 1
IEND
//tinypng图像 8位rgba调色板图像 pngcrush处理后
//8位rgba调色板图像 标记
//chunk列表:
IHDR
PLTE
tRNS
iDOT //new
IDAT x 2 //所有的filter = 1
IEND
同时也测试了24位图像,可以为pngcrush行为得到结论:
1. pngcrush的处理区分为调色板图像与像素图像。
2. 像素图像一律转换为32位CgBI图像,IDAT数据重新处理与拆解,会新增CgBI chunk与iDOT chunk。
3. 调色板图像保持不变,IDAT数据重新处理与拆解,会新增iDOT chunk。
4. 其他chunk不做删减。
关键缺陷与优化方案
上文描述的pngcrush行为,细说来看,都存在文件大小负优化的可能。大体罗列包括:
- 转换CgBI图像:CgBI图像一定是32位的,对于原本是32位的png还好,对于原本是24位的png如照片类,像素储存大小直接暴涨1/3。CgBI的目的是解码后像素数据不经过位图中转直接推入GPU使用,对解码渲染流程有收益。
- 转换iDOT图像:即上次提到的多线程技术,对解码性能有收益。但是iDOT格式需要对IDAT数据段进行拆解,对数据压缩率有损失,也会带来多余的IDAT信息占用。
- filter能力缺失:pngcrush与tinypng有一样的问题,不能动态计算合适的filter来完成高压缩率,只能固定一个filter。甚至有一个-f [num]参数来指定全体使用那个filter。而pngcrush的默认逻辑是使用filter = 1。有的图像试了一下,只要指定全体使用filter = 0,文件就会小一些,但是pngcrush连这个程度的filter测算都没有。另外根据之前对iDOT的探索,filter = 345实际不可用。
结合以上分析,我们在CgBI与iDOT都有必要的前提下进行讨论。在XCode构建行为中pngcrush处理前后,我们还可以通过脚本进行什么优化呢?
pngcrush处理前:
1. 使用tinypng或者保持和tinypng一致的逻辑,尽可能的将可能转换为调色板格式并且不关注解码性能的图像,均转换为调色板8位图。
2. 删除所有多余的chunk,pngcrush不会对chunk做处理。
pngcrush处理后:
0. 核心是二次重建IDAT与相关数据。
1. 数据执行filter测算逻辑,找到更合适的filter来继续提升压缩率。这一段应该能找到适当的开源代码。
2. 同时修改适当的iDOT配置来提升解码性能。
3. 同时保持CgBI格式的优化。