为什么同一个截屏图形会被不同的工具保存成不一样大小的PNG

使用不同的软件工具,比如Gimp或者Windows Paint,保存同样的屏幕截图的时候,产生的PNG文件的大小会不同。在这个帖子里,我将用Python脚本尝试解析PNG文件,同时了解造成前面所说差异的原因。

简介

PNG是一种在Internet上常用的图形格式。我习惯于把屏幕截图保存为PNG格式。和普通的照片比起来,软件工具的屏幕截图用到的颜色数目会更少,所以我们可以通过数据压缩技术得到比较小的图形文件。另外,PNG是个开源的图形格式。基于这些原因,我更倾向于使用PNG格式来保存软件工具的屏幕截图。

说老实话,过去我很少注意软件产生的PNG文件的大小,直到最近,当我需要给个人网站准备图形文件的时候,为了更好的网页浏览效果,我才开始注意各种PNG文件的大小。一般来说,我主要使用Gimp和Windows Paint工具来创建软件工具的屏幕截图。前者Gimp是个强大的免费图形处理工具软件,用它来生成屏幕截图真的有点大材小用的感觉。WindowsPaint是Windows自带的图形工具。从Windows 7起,下Windows系统自带的Paint工具支持一些简单的图形编辑功能,勉强可以用来创建屏幕截图。在使用这些工具时,我假设这些工具可以生成最优的PNG图形文件。可是直到最近,我才发现,对于同样的屏幕截图,Gimp和Windows Paint生成的PNG文件大小并不一样。而且,尽管Paint并没有和PNG文件相关的选项,可是它仍然能生成比Gimp中选择最高压缩比的选项所产生的PNG还要小的文件。处于好奇,我尝试理解造成这种区别的原因。在这个帖子里,我将分享我从这个过程中学到的东东。

PNG 格式

这里,PNG指的是Portable Network Graphics。它是一个位图格式。最初,PNG是以作为GIF格式的替代品为设计、开发目标的。如想了解更多有关PNG格式,请移步相关Wikipedia网页。

简单来说,PNG文件的开始包含8字节的头信息,然后就是各种具有特别意义的数据块。每个数据块包含如下四个部分:

  1. 数据块的长度 – 4 字节
  2. 数据块的类型 – 4 字节
  3. 数据块数据 – 前面提到的数据块长度指定的大小
  4. CRC – 4 字节

也就是说,如果一个数据块的前四个字节是个数值为8192的整数,它表明数据块中的数据大小为8192字节。该数据块的大小即为 4+4+8192+4=8204 字节.

数据块的第二个部分(从第五到第八字节)指明了该数据块的类别。它通常是区分大小写的ASCII文本。一个PNG文件至少包含如下几个关键的数据块:

  1. IHDR: 必须是文件中的第一个数据块,它记录图形的包含宽、高等元信息。
  2. PLTE or sRGB: 图形所用到的所有颜色
  3. IDAT: 图形数据
  4. IEND: 图形结束的地方

一个PNG文件可以包含多个IDAT数据块。解析PNG文件的时候,所有的IDAT块需要按照它们在文件中存储的顺序依次连接起来,然后才解压缩。

下面的截图演示了一个PNG文件在TotalCommander的Lister工具中以十六进制选项查看的例子:

以十六进制方式查看一个PNG图形文件
十六进制显示的PNG文件

使用 Python 解析 PNG 文件

首先我们创建一个描述PNG文件中数据块的类:

 

 

解析PNG文件的时候,首先打开该文件,读取所有内容到内存,然后解析文件的头信息以及数据块:

一个PNG文件通常包含至少一个IDAT数据块。IDAT数据块包含了压缩后的图形数据。图形数据是个常见的矩形像素数组。数组的每个元素指明了相关的颜色。PNG图形文件中常见的颜色类型有三种:

  1. 灰度像素。最小像素大小:1 位
  2. 颜色索引像素。最小像素大小:1 位
  3. 真彩色像素:最小像素大小:24 位

另外,每个像素还可以包含关于透明颜色的信息。这个信息通常是8位,所以一个带有透明信息的真彩色像素大小为4字节。

PNG文件中的图形数据是从上到下依次排列的。每行的像素数据从左到右依次排列,形成一条扫描线。每条扫描线都包含两部分:第一个字节指明了所用的滤波方法,接下来就是从左到右依次排列的图形数据。所有的扫描线最后用和zlib兼容的DEFLATE算法压缩。因为PNG文件所用的数据压缩算法和zlib兼容,所以我们可以使用Python的zlib模块来解压缩PNG图形:

 

如想了解更多关于zlib压缩文件的格式,请移步zlib RFC网页 简而言之,zlib压缩后的数据在开头的地方有两个字节代表了和压缩算法有关的参数信息。如果以十六进制方式查看zlib压缩后的数据的时候,首两个字节看起来会像”78 da”。从这两个字节,我们可以猜出数据压缩时所用到的某些参数。但对于一些关键参数比如压缩级别,我们无法得知具体的数值,因为不同的压缩级参数会对应于相同的zlib头信息。为了获得真正的压缩参数,我们可以采取暴力测试的方式,用常见的压缩参数逐一测试。这种测试可以通过一个Python脚本来实现。

其实Python已经有可以解析PNG文件的模块,但是因为我需要测试不同的压缩参数,所以我自己写了一个简单的Python脚本来方便测试。完整的脚本可从这里下载。下载的脚本名为sspng.py。它可以用在Python2.7和3.4
环境中。运行该脚本时需要给定一个PNG文件名。脚本运行后会显示该PNG图形的基本信息,以及压缩信息。

Exampels

下面我们以TotalCommander 8.52a在Windows 10 x64上的屏幕截图为例。屏幕截图如下图所示

TotalCommander 8.52a在Windows 10 x64上的屏幕截图
TotalCommander 8.52a在Windows 10 x64上的屏幕截图

为了产生如上的屏幕截图,首先使得TotalCommander显示在桌面的最前端,按下“Alt-PrtScrn”。TotalCommander的截图将会被拷贝到系统的剪贴板上,然后分别把它黏贴到Gimp或者Windows Paint中,然后保存为PNG文件。这里我用的Gimp是最新的开发版本,Paint是系统自带的。在保存PNG文件的时候,Paint并没有任何和PNG相关的选项,而Gimp有包含PNG压缩级别等一些选项。这里全部采用缺省选项。Gimp和Paint产生的PNG文件大小分别是:

  • Paint: 125,149 字节
  • Gimp: 162,688 字节

假设Gimp产生的PNG文件路径为
c:\users\wdong\Pictures\totalcmd-en-gimp.png,我们可以按如下所示运行sspng.py:
 

 

数据块中有793条扫描线。图形的真彩色的,也就是说每个像素有三个字节。由于每条扫描线的第一个字节是关于滤波信息的,那么这个IDAT中所包含的图形数据就有(1+1010*3)*793=2403583字节。压缩后的数据总共有162326字节大小,以8192字节的大小分割为多个小块分别储存在多个IDAT数据块中。看起来Gimp 2.9仍然使用标准的zlib压缩算法,因为我们可以通过Python的zlib模块实现和该PNG一样的压缩比

用Windows Paint产生的PNG文件做实验的输出结果如下:

 

这说明我们无法用标准的zlib压缩算法重现该PNG文件中的压缩比。比较以上两次试验结果,我们可以看到如下所示几个关于在Windows 10 x64系统上,Windows Paint和Gimp所产生的PNG文件的不同之处:

  • Gimp以8192字节分割IDAT数据块,而Windows Paint以65445字节来分割
  • Windows 10系统的Paint应该是在压缩图形数据之前进行了某种滤波预处理以优化压缩
  • Windows 10系统的Paint并没有使用最优的压缩参数

使用Gimp 2.8以及Windows 7所带的Paint等做类似实验后发现,这些工具都是使用标准的zlib压缩算法在所产生的PNG中压缩数据的。

优化PNG文件

一般上有如下三种途径优化PNG图形文件:

  1. 使用颜色索引而不是真彩色,这样每个像素在图形数据中会更小
  2. 压缩之前对数据做预处理
  3. 使用压缩比更好的DEFLATE压缩算法实现,比如7-zip以及Google公布的Zopfli都是和zlib兼容的同时又有更高压缩比的DEFLATE算法实现

下面两个连接有更多关于优化PNG文件的信息:

  1. https://zoompf.com/blog/2010/01/top-png-optimizers-dont-use-zlib
  2. http://www.smashingmagazine.com/2009/07/clever-png-optimization-techniques/

发表评论

电子邮件地址不会被公开。 必填项已用*标注