Feb 1

XCode构建过程中pngcrush的逆优化与如何修复

Lrdcq , 2022/02/01 00:54 , 程序 , 閱讀(3237) , Via 本站原創
在上次讨论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确实能做到不俗的压缩程度。他们的属性如下:
//原始图像 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格式的优化。
关键词:ios , pngcrush , png
logo