当前版本移植了lodepng项目,该项目除了可以分析PNG图片的结构外,还将Deflate即zlib的压缩与解压缩算法也包含进来了,对libc即C库文件的依赖也很小,因此可以方便的移植到hobby OS中 ...

    页面导航:
项目下载地址:

    v3.0.2版本的项目地址:

    github.com地址:https://github.com/zenglong/zenglOX (只包含git提交上来的源代码)

    Dropbox地址:点此进入Dropbox网盘 

    Google Drive地址:点此进入Google Drive云端硬盘

    sourceforge地址:https://sourceforge.net/projects/zenglox/files 

    在上面的三个网盘中(不包括github),v3.0.2版本的相关文件,都位于zenglOX_v3.0.2的文件夹中,在该文件夹里,zenglOX_v3.0.2.zip文件为当前版本的源代码的压缩包,zenglOX.iso文件为当前版本的iso镜像(读者可以将iso文件挂载到VirtualBox或VMware里,进行测试),lodepng-master.zip文件为lodepng项目的源代码的压缩包,readme.txt文件为当前版本的说明文档。

概述:

    当前版本移植了lodepng项目,该项目的链接地址为:http://lodev.org/lodepng/ ,lodepng除了可以分析PNG图片的结构外,还将Deflate即zlib的压缩与解压缩算法也包含
进来了,对libc即C库文件的依赖也很小,因此可以方便的移植到hobby OS中。

    当前版本就创建了一个基于lodepng项目的showpng程式,使用该程式可以将png图片显示出来,显示效果可以查看screenshot目录下的v302_1.jpg截图:


图1

    由于showpng程式默认位于iso镜像里,所以需要通过isoget -all命令来获取该程式及相关的png示例图片文件。

    如果磁盘没进行过分区格式化的话,先使用fdiskformat工具对磁盘进行分区格式化(当然,也可以将之前版本的磁盘镜像拷贝到当前版本里,Qemu模拟器的磁盘镜像文件为hd_qemu.img)。具体的分区格式化命令,如下所示(仅供参考,你可以根据需要设置自己的分区大小等):

zenglOX> fdisk -hd 0 -pt 1 -type zenglfs -start 0 -num 123456

fdisk write MBR success , .......

zenglOX> format -hd 0 -pt 1 -type zenglfs

format success , .....


    分区格式化的命令详情,请参考"zenglOX v1.5.0 zenglfs文件系统..."对应的官方文章。

    分区格式化后,使用mount命令挂载hd与iso目录:

zenglOX> mount hd 0 1
mount to [hd] success! .....
zenglOX> mount iso
mount iso to [iso] success! ...


    上面mount hd ...命令的详情,也可以参考"zenglOX v1.5.0 zenglfs文件系统..."的官方文章。

    接着就可以使用isoget -all命令了:

zenglOX> isoget -all
........................
copy content of iso/IMG/R.PNG;1 to hd/img/r.png success
copy content of iso/IMG/H.PNG;1 to hd/img/h.png success
copy content of iso/IMG/S.PNG;1 to hd/img/s.png success
copy content of iso/EXTRA/SHOWPNG.;1 to hd/bin/showpng success


    上面的isoget -all命令会将showpng程式拷贝到hd/bin目录下,并将相关测试用的png图片拷贝到hd/img目录中。

    由于showpng程式会自动搜索hd/img目录,因此,在使用时,直接给出png的文件名即可,如下所示:

zenglOX> showpng s.png
now load and decode hd/img/s.png
now wake up the parent task!

zenglOX> showpng h.png
now load and decode hd/img/h.png
now wake up the parent task!

zenglOX> showpng r.png
now load and decode hd/img/r.png
now wake up the parent task!


    解析并显示出一张图片后,showpng会将父任务即shell任务唤醒,让shell继续执行,因此上面就可以在shell中连续运行三个showpng程式了,从而不用为了显示三幅图片而启动三个shell了。

    showpng程式与依赖的lodepng项目的源代码位于build_initrd_img目录下的extra目录中,分别对应为showpng.c与lodepng.c文件,其中,lodepng.c文件是从网盘的lodepng-master.zip压缩包里提取出来的,将原来的lodepng.cpp重命名为lodepng.c,并对该文件的代码作了些移植处理,让其可以在zenglOX系统中正常执行。

    稍后会根据lodepng源码,对PNG图片结构及Deflate算法进行分析。

=========================================
BUG Fix(BUG修复)
=========================================

    1. 修复zlox_kheap.c(内核堆)与zlox_uheap.c(用户堆)中,在对页表项进行操作后,没有更新TLB缓存的BUG。有关TLB缓存的内容,在之前zenglOX v1.0.0版本对应的官方文章中解释过。

    之前版本由于没有更新TLB,就容易导致堆里的数据错误。例如,当从iso镜像拷贝比较大的文件时,当触发了堆中的expand(线性地址扩展)操作时,新分配的页表项不能及时更新到TLB里,从而会导致读到堆里的正常数据,会被突然变成一些无效的数据,这些无效的数据写入到磁盘里,就会导致磁盘里的文件会出现部分内容是无效的情况。

    2. 修复模拟器下可能出现的无法收到按键irq中断的BUG,主要是对zlox_ps2.c文件里的zlox_ps2_init即PS/2控制器的初始化函数进行调整,直接将PS/2控制器的configure byte即配置字节设置为0x47 ,然后写入到PS/2控制器中。

    省略掉之前版本里的读取configure byte的步骤,因为configure byte是通过0x60的I/O端口读取出来的,而0x60端口又被用于鼠标与键盘设备的输入事件中。在鼠标键盘没有被禁用的情况下,0x60端口容易读取出鼠标键盘设备的数据出来,而非真实的configure byte值。

    即便鼠标键盘被禁用的情况下,也无法保证0x60读出来的就是原始的configure byte值,因为,很多PS/2命令的反馈信息也需要通过0x60端口读出来。因此,就省略掉读操作,而直接将需要设置的值写进去即可。

    有关ps/2控制器的详情,可以参考 http://wiki.osdev.org/%228042%22_PS/2_Controller 该链接对应的维基百科文章。

    configure byte里控制了是否开启鼠标键盘设备,以及是否开启鼠标键盘的中断。因此,错误的configure byte值就会导致鼠标或键盘无响应的情况发生。

=========================================
其他的一些改动
=========================================

    1. 当用户程式通过系统调用,出现内存地址不存在之类的分页错误时,只会当掉当前任务,不会当掉整个系统。改动的地方位于zlox_paging.c文件的zlox_page_fault函数里。

    2. 增加shift + tab系统热键,当你同时按下shift与tab键时,可以在各窗口之间进行切换,当然桌面是不参与窗口的这种切换的。

    之所以不用alt + tab组合键,是因为该组合键容易被模拟器所在的主机给劫持掉(主要是bochs模拟器),而且alt作为通用热键,在模拟器下有时候收不到释放信号(松开alt键时,有时收不到相关的中断信号)。

    shift + tab组合键最终会通过zlox_my_windows.c文件的zlox_shift_tab_window函数来进行切换窗口的操作。

    另外,在你拖动窗口时,该系统热键是不会起作用的。因为拖动窗口时,又进行切换窗口的话,容易导致一些显示上的问题。

PNG(Portable Network Graphics):

    在当前版本中,提供了几个测试用的png图片文件,下面我们以isodir/img/r.png文件为例,来对PNG结构进行分析:

[email protected]:~/Downloads/zenglOX_v3.0.2/isodir/img$ vim r.png
0000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
0000010: 0000 00c3 0000 00c5 0802 0000 0066 44d1  .............fD.
0000020: e000 00a6 1e49 4441 5478 daac 9d77 b85d  .....IDATx...w.]
0000030: 5599 fff7 b989 bf99 71c6 e933 2a88 d424  U.......q..3*..$
0000040: 0408 0402 1129 2110 a449 07e9 2028 3252  .....)!..I.. (2R
0000050: d451 71d0 5194 667b 9451 9447 10a4 490b  .Qq.Q.f{.Q.G..I.
..................................................................
..................................................................
..................................................................
[email protected]:~/Downloads/zenglOX_v3.0.2/isodir/img$ 


    Linux下可以使用vim编辑器来查看文件的十六进制格式的数据。一开始通过vim编辑器打开r.png文件时,会看到一堆乱码,因为png里存储的数据,大部分是一些图像结构数据和一些压缩数据。可以在vim中,先输入冒号进入交互式命令界面,然后输入%!xxd命令,就可以切换到如上所示的十六进制编辑界面。该界面与windows下的十六进制编辑器winhex软件的界面就很像了,适合在Linux下分析PNG文件的结构。要让vim返回到原始数据界面,可以在交互式命令中输入%!xxd -r命令(也就是给%!xxd添加一个-r参数)即可。由于我们不需要修改png文件,因此,在退出vim时,请使用q!命令:强制以不保存的方式来退出编辑器。

    在 http://en.wikipedia.org/wiki/Portable_Network_Graphics (维基百科链接) 和 http://tools.ietf.org/html/rfc2083 (RFC2083)这两个链接中,都对PNG的文件结构作了介绍。

    首先,PNG文件的开头是8个字节的标准头部信息:

[email protected]:~/Downloads/zenglOX_v3.0.2/isodir/img$ vim r.png
0000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
0000010: 0000 00c3 0000 00c5 0802 0000 0066 44d1  .............fD.
..................................................................
..................................................................
[email protected]:~/Downloads/zenglOX_v3.0.2/isodir/img$ 


    可以通过上面的89 50 4e 47 0d 0a 1a 0a这8个字节来大致的识别出一个文件是否是PNG类型。因此,这8个字节可以看作是PNG的signature(签名)。

    第1个字节0x89已经超过了ASCII表里的最后一个0x7f字符,当编辑器读取到这个字节时,就知道该文件是一个包含了非ASCII码在内的二进制文件,就可以将其当成二进制文件来处理。

    第2个到第4个字节:0x50 0x4e 0x47则是'P','N','G'这三个字符的ASCII码,使用普通的文本编辑器查看文件时,通过这三个字符就可以初略的看出文件类型出来。

    第5个到第6个字节:0x0d 0x0a则是DOS风格的换行符(CRLF)。

    第7个字节:0x1a是end-of-file(文件结束字符),当在dos下使用type命令显示文件内容时,通过这个文件结束符,就可以停止文件内容的显示,以防止在dos下将后面的图像数据以乱码的形式显示出来。

    第8个字节:0x0a则是unix风格的换行符。通过换行符就可以在普通文本编辑器中,将PNG的头部与其他数据通过换行隔开。

    PNG的标准头部信息之后,是由一系列的chunks(块)组成:

[email protected]:~/Downloads/zenglOX_v3.0.2/isodir/img$ vim r.png
0000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
0000010: 0000 00c3 0000 00c5 0802 0000 0066 44d1  .............fD.
0000020: e0.......................................................
..................................................................
[email protected]:~/Downloads/zenglOX_v3.0.2/isodir/img$ 


    上面红色标记的十六进制部分,就是PNG里的第一个chunk(块),从右边的ASCII显示区域里,可以看到这是一个IHDR类型的块。PNG头部信息之后的块,都必须以一个IHDR类型的块开始,并以一个IEND类型的块结束。在这两个块之间,可以包含多个其他类型的块。

    每个chunk(块)都由4个部分组成:第一部分是块的Length(长度)信息,例如上面的0000 000d这4个字节就表示IHDR块的第3个data数据部分的长度为13个字节。第二部分是块的类型,例如上面的4948 4452就是类型的ASCII编码,对应为IHDR这4个字符。第三部分为块的data数据部分,例如上面的0000 00c3 0000 00c5 0802 0000 00这13个字节就是IHDR块的数据部分,块数据部分的字节的二进制含义是跟块类型相关的。第四部分是块的CRC(Cyclic Redundancy Check)即循环冗余校验部分,例如上面的66 44d1 e0这4个字节,用于校验块类型与块数据部分是否正确(但不包括块长度部分),CRC校验算法在上面给出的RFC2083链接中有介绍。

    PNG文件里的第一个块必须是IHDR块,IHDR块的数据部分的结构如下(以上面提到的0000 00c3 0000 00c5 0802 0000 00这13个字节为例进行说明):
  • Width:使用4个字节来表示PNG图片的宽度(以像素为单位),上例中的0000 00c3这头4个字节,就表示r.png图片的宽为195像素。
  • Height:使用4个字节来表示PNG图片的高度(以像素为单位),上例中的0000 00c5这4个字节,就表示r.png图片的高为197像素。
  • Bit depth:使用1个字节来表示PNG图片的位深,上例中的08这个字节,就表示r.png图片的位深为8 ,从上面RFC2083链接中,可以看到,位深并非每个像素所占的位数,而是per sample(每个采样)或者per palette index(每个调色板索引)的二进制位数,该值需要和下面的Color Type字段配合起来,才能知道图片中每个像素的具体组成情况。
  • Color Type:该字段的值(单个字节)需要和上面的Bit depth字段配合起来,才能确定图片的像素的具体组成情况,上例中的02这个字节就表示r.png图片的Color Type为2。
  • Compression method:使用1个字节来表示图片所使用的压缩方法,目前只有一个有效的值即0,表示Deflate算法。上例中的第11个字节00就表示r.png采用的就是Deflate即zlib压缩算法。
  • Filter method:使用1个字节来表示图片所使用的过滤算法,目前也只有一个有效的值即0,上例中的第12个字节就是00 ,过滤算法可以参考上面的RFC2083链接。
  • Interlace method:使用1个字节来表示图片是否采用了Adam7隔行扫描,如果值是0则表示没有采用隔行扫描,如果值是1则表示采用了Adam7隔行扫描,上例中的第13个字节00,就表示r.png图片没有使用隔行扫描。在上面提到的维基百科链接与RFC2083链接中,都对隔行扫描进行了介绍。
    下面是RFC2083链接中的Color Type与Bit depth的组合情况,以及在不同组合下,对应的像素解释:

Color Type 允许的Bit depth值 像素解释
0 1, 2, 4, 8, 16 Each pixel is a grayscale sample.
解压缩后,每个像素值是一个灰度采样。
2 8, 16 Each pixel is an R,G,B triple.
解压缩后,每个像素值是由red, green, blue即红绿蓝三原色组成的。
3 1, 2, 4, 8 Each pixel is a palette index;
a PLTE chunk must appear.
解压缩后,每个像素值是一个调色板索引值。
由于涉及到调色板,因此,必须有一个PLTE块存在。
4 8, 16 Each pixel is a grayscale sample,
followed by an alpha sample.
解压缩后,每个像素值是一个灰度采样,同时,
后面还跟随一个alpha通道的采样值。
6 8, 16 Each pixel is an R,G,B triple,
followed by an alpha sample.
解压缩后,每个像素值是由红绿蓝三原色组成,同时,
后面还跟随一个alpha通道的采样值。

    在r.png图片中,跟随在IHDR块后面的是IDAT块:

[email protected]:~/Downloads/zenglOX_v3.0.2/isodir/img$ vim r.png
0000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
0000010: 0000 00c3 0000 00c5 0802 0000 0066 44d1  .............fD.
0000020: e000 00a6 1e49 4441 5478 daac 9d77 b85d  .....IDATx...w.]
0000030: 5599 fff7 b989 bf99 71c6 e933 2a88 d424  U.......q..3*..$
0000040: 0408 0402 1129 2110 a449 07e9 2028 3252  .....)!..I.. (2R
0000050: d451 71d0 5194 667b 9451 9447 10a4 490b  .Qq.Q.f{.Q.G..I.
..................................................................
..................................................................
..................................................................
[email protected]:~/Downloads/zenglOX_v3.0.2/isodir/img$ 


    根据前面介绍的块结构可知,上面红色标记的十六进制数据中,开头的00 00a6 1e这4个字节是块的长度,由于是PNG里的结构数据是采用的大字节序(与网络传输相对应,网络中也是采用的大字节序来进行数据传输),因此,IDAT块的data数据部分的长度为0x0000a61e即42526字节,长度信息后面的49 4441 54这4个字节则是'I','D','A','T'这4个字符的ASCII码。然后,接下来的78 daac 9d77 b85d 5599 fff7 ...... 这42526个字节的数据就是IDAT块的实际的数据部分,该部分的数据是经过zlib压缩过的数据。在zlib标准中,本身是可以包含不同的压缩算法在里面的,在PNG中目前主要使用的是Deflate压缩算法,zlib可以看作是对Deflate压缩好的数据的一层封装。

    在 http://tools.ietf.org/html/rfc1950 (RFC1950)的链接中,就介绍了zlib标准的封装格式,大致的封装格式如下所示:

  0   1
+---+---+
|CMF|FLG|   (more-->)
+---+---+

(if FLG.FDICT set)

	   0   1   2   3
	 +---+---+---+---+
	 |     DICTID    |   (more-->)
	 +---+---+---+---+

	 +=====================+---+---+---+---+
	 |...compressed data...|    ADLER32    |
	 +=====================+---+---+---+---+


    上面的CMF是Compression Method and flags的英文缩写,它由单个字节组成,该字节里的低4位用于表示CM(Compression method即压缩方法),高4位用于表示CINFO(Compression info即和压缩方法相关的一些压缩信息)。当低4位的CM为8时,表示上面所示的compressed data压缩数据部分是采用的Deflate压缩算法,当高4位的CINFO为7时,则表示LZ77编码的window size(窗口尺寸)为32K,LZ77编码是Deflate算法在正式压缩之前,对数据的一种预处理过程,它可以减少重复的数据所占用的空间,比如可以将重复的数据用distance距离偏移值及length长度来表示等,window size是编码算法中所使用的缓存的尺寸大小。前面r.png图片的IDAT块的数据部分的第一个字节就是0x78,低4位为8,说明r.png采用的是Deflate压缩算法,高4位为7,说明LZ77编码的窗口尺寸为32K。

    上面的FLG是flag标志字节,该字节由三个部分组成:

bits 0 to 4  FCHECK  (check bits for CMF and FLG)
bit  5       FDICT   (preset dictionary)
bits 6 to 7  FLEVEL  (compression level)


    低4位的FCHECK是用于校验CMF与FLG字段的。FDICT用于判断是否存在DICTID,当该位被置1时,则说明FLG与compressed data压缩数据之间存在DICTID即词典的ID值,当该位为0时,则不存在DICTID。FLEVEL字段用于表示压缩级别,具体的级别值与含义,请参考zlib的RFC1950链接。前面r.png里的IDAT块的数据部分的第二个字节是0xda,因此,r.png的FCHECK值为0x1a,FDICT为0,也就是没有DICTID,FLEVEL的值为3,即压缩级别为3,也就是maximum compression(最大化的压缩)。

    由于r.png没有DICTID,因此,跟在CMF与FLG这两个字节后面的数据就是compressed data(压缩的数据):

[email protected]:~/Downloads/zenglOX_v3.0.2/isodir/img$ vim r.png
0000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
0000010: 0000 00c3 0000 00c5 0802 0000 0066 44d1  .............fD.
0000020: e000 00a6 1e49 4441 5478 daac 9d77 b85d  .....IDATx...w.]
0000030: 5599 fff7 b989 bf99 71c6 e933 2a88 d424  U.......q..3*..$
0000040: 0408 0402 1129 2110 a449 07e9 2028 3252  .....)!..I.. (2R
0000050: d451 71d0 5194 667b 9451 9447 10a4 490b  .Qq.Q.f{.Q.G..I.
..................................................................
..................................................................
..................................................................
[email protected]:~/Downloads/zenglOX_v3.0.2/isodir/img$ 


    上面的ac 9d77 b85d 5599 fff7 ...... 这些压缩数据是采用Deflate算法来压缩的,要了解Deflate的解压缩过程,最好的方法是先通过一些简单的例子,结合gdb调试器,加上lodepng项目的源代码,分析清楚它的压缩过程,在了解了压缩过程后,解压缩过程也就一目了然了。因此,下面将通过lodepng项目的一个测试例子来进行说明。

    从网盘中下载并解压lodepng-master.zip文件:

[email protected]:~$ unzip -d lodepng lodepng-master.zip
[email protected]:~$ ls
lodepng/ .............
[email protected]:~$ 


    上面将lodepng-master.zip解压到了lodepng目录中,进入解压后的目录,并在该目录内创建一个mytest目录:

[email protected]:~$ cd lodepng/lodepng-master/
[email protected]:~/lodepng/lodepng-master$ ls
examples/              lodepng.h             lodepng_util.h
lodepng_benchmark.cpp  lodepng_unittest.cpp  pngdetail.cpp
lodepng.cpp            lodepng_util.cpp      README.md
[email protected]:~/lodepng/lodepng-master$ mkdir mytest
[email protected]:~/lodepng/lodepng-master$ cd mytest
[email protected]:~/lodepng/lodepng-master/mytest$ 


    将lodepng.cpp与lodepng.h拷贝到mytest里:

[email protected]:~/lodepng/lodepng-master/mytest$ cp ../lodepng.cpp ./lodepng.c
[email protected]:~/lodepng/lodepng-master/mytest$ cp ../lodepng.h ./lodepng.h
[email protected]:~/lodepng/lodepng-master/mytest$ ls
lodepng.c  lodepng.h
[email protected]:~/lodepng/lodepng-master/mytest$ 


    上面在拷贝时,将lodepng.cpp重命名为lodepng.c,因为,下面我们要测试的是C程式,在使用lodepng项目时,当编写C++程式时,就使用lodepng.cpp文件名,当编写C程式时,就使用lodepng.c文件名。

    使用编辑器创建一个mytest.c文件,内容如下:

#include "lodepng.h"
#include <stdlib.h>

#define UNUSED(x) (void)(x)

int main(int argc, char *argv[])
{
	char * in = "AABCCDD";
	size_t insize = 7;
	size_t outsize = 0;
	size_t outsize2 = 0;
  	unsigned char* out = (unsigned char*)malloc(10);
	unsigned char* out2 = (unsigned char*)malloc(10);
	UNUSED(argc);
	UNUSED(argv);
	lodepng_deflate(&out, &outsize, (const unsigned char*)in, insize, &lodepng_default_compress_settings);	
  	
	lodepng_inflate(&out2, &outsize2,
                         out, outsize,
                        &lodepng_default_decompress_settings);
	free(out);
	free(out2);
	return 0;
}


    上面测试代码中,会使用lodepng_deflate函数将in字符串进行Deflate压缩,压缩后的数据存储到out缓存里,接着再用lodepng_inflate函数将out里的压缩数据,解压缩到out2缓存里,可以先用gdb来进行简单的调试,通过对比in与out2的字符串值,就可以知道压缩与解压缩是否成功了:

[email protected]:~/lodepng/lodepng-master/mytest$ ls
lodepng.c  lodepng.h  mytest.c
[email protected]:~/lodepng/lodepng-master/mytest$ gcc lodepng.c mytest.c -ansi -pedantic -Wall -Wextra -g3 -o mytest
[email protected]:~/lodepng/lodepng-master/mytest$ gdb -q mytest
.......................................
Breakpoint 1, main (argc=1, argv=0xbffff394) at mytest.c:16
16		lodepng_deflate(&out, &outsize, (const unsigned char*)in, insize, &lodepng_default_compress_settings);	
(gdb) n
18		lodepng_inflate(&out2, &outsize2,
(gdb) p outsize
$1 = 17
(gdb) x/17bx out
0x80600a8:	0x05	0xc1	0x01	0x01	0x00	0x00	0x00	0x82
0x80600b0:	0xa0	0x6d	0xa6	0xff	0x37	0x05	0x30	0xad
0x80600b8:	0x03
(gdb) n
21		free(out);
(gdb) p out2
$2 = (unsigned char *) 0x8059018 "AABCCDD"
(gdb) p in
$3 = 0x80573fc "AABCCDD"
.......................................
[email protected]:~/lodepng/lodepng-master/mytest$ 


    上面先通过gcc将mytest.c与lodepng.c文件一起编译为mytest程式,通过gdb调试可以看到,out缓存里的0x05 0xc1 0x01 0x01 0x00 ...... 这17个字节就是字符串"AABCCDD"经过Deflate算法压缩后的数据,最后,out2里的解压缩后的字符串也是"AABCCDD",因此,压缩与解压缩测试通过。

    下面只需要通过gdb调试lodepng_deflate函数,就可以了解其压缩过程了,不过要完全理解Deflate的话,还必须先了解Deflate的一些基本标准(一些文字上的基本说明),Deflate算法的文字说明可以参考 http://tools.ietf.org/html/rfc1951 (RFC1951)的链接。另外,Deflate又用到了信息论里的霍夫曼编码,有关霍夫曼编码的详情,可以参考 霍夫曼编码_维基百科 该链接对应的维基百科文章,霍夫曼编码可以说是整个压缩算法的核心理论依据,所以,最好是先了解霍夫曼编码,再去看Deflate的RFC1951文档,RFC1951文档的开头部分还好理解(结合霍夫曼编码的理论知识来看),看到后面,如果没有lodepng项目的源代码的话,估计会比较难理解。

    lodepng_deflate函数最终会进入deflateDynamic函数来完成具体的压缩工作:

[email protected]:~/lodepng/lodepng-master/mytest$ gdb -q mytest
...................................................
(gdb) s
deflateDynamic (out=0xbffff270, bp=0xbffff230, hash=0xbffff218, 
    data=0x80573fc "AABCCDD", datapos=0, dataend=7, settings=0x80561e0, 
    final=1) at lodepng.c:1706
1706	  unsigned error = 0;
...................................................
[email protected]:~/lodepng/lodepng-master/mytest$ 


    该函数中,会先执行LZ77编码,前面提到过,LZ77用于在压缩之前,对数据做个预处理:

[email protected]:~/lodepng/lodepng-master/mytest$ gdb -q mytest
...................................................
(gdb) n
1763	      error = encodeLZ77(&lz77_encoded, hash, data, datapos, dataend, settings->windowsize,
(gdb) 
1765	      if(error) break;
(gdb) p lz77_encoded 
$1 = {data = 0x805e458, size = 7, allocsize = 42}
(gdb) p lz77_encoded->data 
$2 = (unsigned int *) 0x805e458
(gdb) p lz77_encoded->data[0]
$3 = 65
(gdb) p/c lz77_encoded->data[0]
$4 = 65 'A'
(gdb) p/c lz77_encoded->data[1]
$5 = 65 'A'
(gdb) p/c lz77_encoded->data[2]
$6 = 66 'B'
(gdb) p/c lz77_encoded->data[3]
$7 = 67 'C'
(gdb) p/c lz77_encoded->data[4]
$8 = 67 'C'
(gdb) p/c lz77_encoded->data[5]
$9 = 68 'D'
(gdb) p/c lz77_encoded->data[6]
$10 = 68 'D'
(gdb) 
...................................................
[email protected]:~/lodepng/lodepng-master/mytest$ 


    由于原始数据比较短,没什么长的重复性的数据,因此,预处理中,直接将"AABCCDD"这几个字符依次写入到data数组中,作为后面需要处理的数据源。

    deflateDynamic函数接着会通过下面的代码,来计算'A','B','C',‘D’这几个字符出现的频率:

/*Count the frequencies of lit, len and dist codes*/
for(i = 0; i != lz77_encoded.size; ++i)
{
  unsigned symbol = lz77_encoded.data[i];
  ++frequencies_ll.data[symbol];
  if(symbol > 256)
  {
	unsigned dist = lz77_encoded.data[i + 2];
	++frequencies_d.data[dist];
	i += 3;
  }
}
frequencies_ll.data[256] = 1; /*there will be exactly 1 end code, at the end of the block*/


    也就是统计'A'出现了几次,'B'出现了几次等等,同时还添加了一个额外的256,用来表示结束符。

    然后根据频率信息,就可以通过HuffmanTree_makeFromFrequencies函数来构建霍夫曼树了:

/*Make both huffman trees, one for the lit and len codes, one for the dist codes*/
error = HuffmanTree_makeFromFrequencies(&tree_ll, frequencies_ll.data, 257, frequencies_ll.size, 15);	


    在HuffmanTree_makeFromFrequencies函数里,会通过lodepng_huffman_code_lengths函数来构建霍夫曼的二叉树(采用相关的coins算法):

[email protected]:~/lodepng/lodepng-master/mytest$ gdb -q mytest
...................................................
(gdb) until 848
lodepng_huffman_code_lengths (lengths=0x805e988, frequencies=0x805e488, 
    numcodes=257, maxbitlen=15) at lodepng.c:848
848	    cleanup_coins(coins, coinmem);
(gdb) p coins[0]
$27 = {symbols = {data = 0x805ef30, size = 2, allocsize = 12},
 weight = 0.25}
(gdb) p/c coins[0]->symbols->data[0]
$28 = 66 'B'
(gdb) p coins[0]->symbols->data[1]
$29 = 256
(gdb) p coins[1]
$30 = {symbols = {data = 0x805efd8, size = 3, allocsize = 18},
 weight = 0.5}
(gdb) p/c coins[1]->symbols->data[0]
$31 = 66 'B'
(gdb) p coins[1]->symbols->data[1]
$33 = 256
(gdb) p/c coins[1]->symbols->data[2]
$34 = 65 'A'
(gdb) p coins[2]
$35 = {symbols = {data = 0x805ef40, size = 2, allocsize = 12},
 weight = 0.5}
(gdb) p/c coins[2]->symbols->data[0]
$36 = 67 'C'
(gdb) p/c coins[2]->symbols->data[1]
$37 = 68 'D'
(gdb) p coins[3]
$38 = {symbols = {data = 0x805eff0, size = 5, allocsize = 24},
 weight = 1}
(gdb) p/c coins[3]->symbols->data[0]
$40 = 66 'B'
(gdb) p coins[3]->symbols->data[1]
$42 = 256
(gdb) p/c coins[3]->symbols->data[2]
$43 = 65 'A'
(gdb) p/c coins[3]->symbols->data[3]
$44 = 67 'C'
(gdb) p/c coins[3]->symbols->data[4]
$45 = 68 'D'
...................................................
[email protected]:~/lodepng/lodepng-master/mytest$ 


    根据上面的coins,我们可以画出如下所示的二叉树:


图2

    从上图所示的二叉树里,我们可以得到A,B,C,D的编码位长度信息:

[email protected]:~/lodepng/lodepng-master/mytest$ gdb -q mytest
...................................................
(gdb) p lengths['A']
$47 = 2
(gdb) p lengths['B']
$48 = 3
(gdb) p lengths['C']
$49 = 2
(gdb) p lengths['D']
$50 = 2
(gdb) p lengths[256]
$51 = 3
...................................................
[email protected]:~/lodepng/lodepng-master/mytest$ 


    上面lengths['A']为2,即在压缩数据里,可以只用2个二进制位来表示'A'。同理,lengths['B']为3,即可以用3个二进制位来表示'B'。

    在lodepng_huffman_code_lengths函数得出编码的位长度信息后,就会进入到HuffmanTree_makeFromLengths2函数,来正式构建与编解码相关的tree(树)结构,在介绍该函数之前,需要先了解下HuffmanTree结构体,该结构体的定义如下(位于lodepng.c文件中):

/*
Huffman tree struct, containing multiple representations of the tree
*/
typedef struct HuffmanTree
{
  unsigned* tree2d;
  unsigned* tree1d;
  unsigned* lengths; /*the lengths of the codes of the 1d-tree*/
  unsigned maxbitlen; /*maximum number of bits a single code can get*/
  unsigned numcodes; /*number of symbols in the alphabet = number of codes*/
} HuffmanTree;


    在HuffmanTree结构体中,lengths(编码位长度)字段已经在之前的函数里得出来了,通过lengths字段可以进一步得出上面的tree1d字段,具体代码如下:

static unsigned HuffmanTree_makeFromLengths2(HuffmanTree* tree)
{
  ...................................................
  tree->tree1d = (unsigned*)lodepng_malloc(tree->numcodes * sizeof(unsigned));
  if(!tree->tree1d) error = 83; /*alloc fail*/

  ...................................................

  if(!error)
  {
    /*step 1: count number of instances of each code length*/
    for(bits = 0; bits != tree->numcodes; ++bits) ++blcount.data[tree->lengths[bits]];
    /*step 2: generate the nextcode values*/
    for(bits = 1; bits <= tree->maxbitlen; ++bits)
    {
      nextcode.data[bits] = (nextcode.data[bits - 1] + blcount.data[bits - 1]) << 1;
    }
    /*step 3: generate all the codes*/
    for(n = 0; n != tree->numcodes; ++n)
    {
      if(tree->lengths[n] != 0) tree->tree1d[n] = nextcode.data[tree->lengths[n]]++;
    }
  }

  ...................................................

  if(!error) return HuffmanTree_make2DTree(tree);
  else return error;
}


    通过上面的step 1step 2step 3这三个步骤(这3个步骤是上面提到的RFC1951文档里设计好的算法),就可以由lengths数组换算出tree1d数组的值出来:

[email protected]:~/lodepng/lodepng-master/mytest$ gdb -q mytest
...................................................
(gdb) until 643
HuffmanTree_makeFromLengths2 (tree=0xbffff174) at lodepng.c:643
643	  uivector_cleanup(&blcount);
(gdb) p tree->lengths['A']
$54 = 2
(gdb) p/x tree->tree1d['A']
$56 = 0x3f0
(gdb) p tree->lengths['B']
$57 = 3
(gdb) p/x tree->tree1d['B']
$58 = 0x7e6
(gdb) p tree->lengths['C']
$59 = 2
(gdb) p/x tree->tree1d['C']
$60 = 0x3f1
(gdb) p tree->lengths['D']
$61 = 2
(gdb) p/x tree->tree1d['D']
$62 = 0x3f2
...................................................
[email protected]:~/lodepng/lodepng-master/mytest$ 


    从上面gdb的输出信息里可以看到,'A'的编码位长为2,对应的tree1d['A']值为0x3f0,而0x3f0的二进制格式为:0011 1111 0000,则红色标记的00即0x3f0的最低2位就是'A'的二进制编码。

    'B'的编码位长为3,对应的tree1d['B']值为0x7e6,而0x7e6的二进制格式为:0111 1110 0110 ,则红色标记的110即0x7e6的最低3位就是'B'的二进制编码。

    'C'的编码位长为2,对应的tree1d['C']值为0x3f1,则'C'的二进制编码为01

    'D'的编码位长为2,对应的tree1d['D']值为0x3f2,则'D'的二进制编码为10

    但是实际存储的压缩编码是翻转后的编码,例如110会被存储为011,这需要理解下面的tree2d数组,以及解压缩时,二进制流的读取方式才能理解(二进制流是从低位往高位来读取的,tree2d数组则是从高位往低位来构建的)。

    在前面构建出tree1d数组后,就可以通过HuffmanTree_make2DTree函数来构建tree2d数组了:

static unsigned HuffmanTree_make2DTree(HuffmanTree* tree)
{
  ..................................................

  tree->tree2d = (unsigned*)lodepng_malloc(tree->numcodes * 2 * sizeof(unsigned));
  if(!tree->tree2d) return 83; /*alloc fail*/

  ..................................................

  for(n = 0; n < tree->numcodes * 2; ++n)
  {
    tree->tree2d[n] = 32767; /*32767 here means the tree2d isn't filled there yet*/
  }

  for(n = 0; n < tree->numcodes; ++n) /*the codes*/
  {
    for(i = 0; i != tree->lengths[n]; ++i) /*the bits for this code*/
    {
      unsigned char bit = (unsigned char)((tree->tree1d[n] >> (tree->lengths[n] - i - 1)) & 1);
      /*oversubscribed, see comment in lodepng_error_text*/
      if(treepos > 2147483647 || treepos + 2 > tree->numcodes) return 55;
      if(tree->tree2d[2 * treepos + bit] == 32767) /*not yet filled in*/
      {
        if(i + 1 == tree->lengths[n]) /*last bit*/
        {
          tree->tree2d[2 * treepos + bit] = n; /*put the current code in it*/
          treepos = 0;
        }
        else
        {
          /*put address of the next step in here, first that address has to be found of course
          (it's just nodefilled + 1)...*/
          ++nodefilled;
          /*addresses encoded with numcodes added to it*/
          tree->tree2d[2 * treepos + bit] = nodefilled + tree->numcodes;
          treepos = nodefilled;
        }
      }
      else treepos = tree->tree2d[2 * treepos + bit] - tree->numcodes;
    }
  }

  for(n = 0; n < tree->numcodes * 2; ++n)
  {
    if(tree->tree2d[n] == 32767) tree->tree2d[n] = 0; /*remove possible remaining 32767's*/
  }

  return 0;
}


    gdb中的调试输出情况如下:

[email protected]:~/lodepng/lodepng-master/mytest$ gdb -q mytest
(gdb) until 573
HuffmanTree_make2DTree (tree=0xbffff174) at lodepng.c:573
573	      unsigned char bit = (unsigned char)((tree->tree1d[n] >> (tree->lengths[n] - i - 1)) & 1);
(gdb) p/c n
$66 = 65 'A'
(gdb) p/x tree->tree1d['A']
$73 = 0x3f0
...................................................
(gdb) p tree->tree2d[0]
$68 = 258
(gdb) p tree->tree2d[2]
$70 = 65
...................................................
[email protected]:~/lodepng/lodepng-master/mytest$ 


    上面tree1d['A']的值为0x3f0,0x3f0的低2位为00,因此,bit = (unsigned char)((tree->tree1d[n] >> (tree->lengths[n] - i - 1)) & 1)语句在i为0时,读取出来的bit就是0,并且一开始treepos值为0,那么,tree->tree2d[2 * treepos + bit]对应的就是tree->tree2d[0],由于此时的bit不是last bit(最后一位),因此,tree->tree2d[0]就被设置为了nodefilled + tree->numcodes的值,其中,nodefilled值为1,tree->numcodes的值为257,因此,tree->tree2d[0]的值就是258

    接着,当i为1时,读取出的第二个bit也是0,treepos此时变为了1,由于该bit是最后一个二进制编码,因此tree->tree2d[2 * treepos + bit]即tree->tree2d[2]就被设置为了'A'的ASCII码,也就是65

    从这里可以看出来,tree2d数组实际上就是用来解压缩用的,当我们从需要解压的二进制流中第一次读取出bit(二进制位),且bit为0时,一开始treepos为0,则tree->tree2d[2 * treepos + bit] 就是 tree->tree2d[0] ,而tree->tree2d[0]的值为258,258大于257(总编码数),用258减去257,得到的1作为下一次的treepos值,接着,我们从二进制流中再读取出一个bit,假设该bit的值也为0,那么tree->tree2d[2 * treepos + bit]就是tree->tree2d[2],而tree->tree2d[2]的值为65,65小于257,那么这个65就是刚才读取出来的二进制编码00所对应的解压后的值了,65是'A'的ASCII码,这样就将'A'解压还原出来了。

    因此,"AABCCDD"的压缩过程,其实就是:先得到他们的lengths(编码位长度),由lengths计算出tree1d数组,压缩过程不计算tree2d应该也可以,只不过计算下tree2d可以防止溢出等错误发生,保存压缩数据时,只需先将lengths保存在前面,然后结合tree1d数组,将'A','B',‘C’,‘D’实际的二进制编码保存在lengths后面即可。

    解压缩时,先将lengths读取出来,由lengths计算出tree1d数组,再由tree1d与lengths计算出tree2d数组,最后将lengths后面的二进制流一位一位的读取出来,每读取一位,就将该二进制位在tree2d数组中进行定位,当从tree2d数组中读取出来的值小于257(总编码数)时,那么该值就是需要解压出来的数据了。

    在实际保存压缩数据时,Deflate算法还将lengths信息又做了一次霍夫曼编码,因为,lengths数组里存储的是257个编码的编码位长度信息,因此,有必要先压缩一下再输出。

    lodepng.c文件的deflateDynamic函数里的tree_cl ,就是上面的lengths经过霍夫曼编码后,所形成的霍夫曼树,tree_cl作为HuffmanTree结构体变量,里面同样存在lengths,tree1d,tree2d这些数组,因此,在最终形成的压缩数据里,一开始会是tree_cl的lengths信息,然后是由tree_cl的lengths与tree1d所得出的二进制编码流。所以,实际解压缩时,会先由tree_cl的lengths,计算出tree_cl的tree1d数组,再由tree_cl的lengths结合tree_cl的tree1d来得出tree_cl的tree_2d数组,接着,将其后的二进制流通过tree_cl的tree2d数组来还原出实际编码的lengths信息出来,由实际编码的lengths计算出实际编码的tree1d,再计算出实际编码的tree2d,最后,将余下的实际编码的二进制流,通过实际编码的tree2d来还原出需要解压缩的值出来。

    在tree_cl的lengths之前,Deflate算法还会放入一些其他的信息:

static unsigned deflateDynamic(ucvector* out, size_t* bp, Hash* hash,
                               const unsigned char* data, size_t datapos, size_t dataend,
                               const LodePNGCompressSettings* settings, unsigned final)
{
    ..............................................
    /*Write block type*/
    addBitToStream(bp, out, BFINAL);
    addBitToStream(bp, out, 0); /*first bit of BTYPE "dynamic"*/
    addBitToStream(bp, out, 1); /*second bit of BTYPE "dynamic"*/

    /*write the HLIT, HDIST and HCLEN values*/
    HLIT = (unsigned)(numcodes_ll - 257);
    HDIST = (unsigned)(numcodes_d - 1);
    HCLEN = (unsigned)bitlen_cl.size - 4;
    /*trim zeroes for HCLEN. HLIT and HDIST were already trimmed at tree creation*/
    while(!bitlen_cl.data[HCLEN + 4 - 1] && HCLEN > 0) --HCLEN;
    addBitsToStream(bp, out, HLIT, 5);
    addBitsToStream(bp, out, HDIST, 5);
    addBitsToStream(bp, out, HCLEN, 4);
    ..............................................
}


    第一个BFINAL二进制位用于表示当前块是否是压缩数据流中的最后一块,接着写入10作为BTYPE的二进制值(从低位往高位方向写入),表示采用dynamic Huffman codes(动态霍夫曼编码),将HLIT即tree_ll(该霍夫曼树里有'A',‘B’, 'C', 'D'之类的编码的lengths,tree1d,tree2d等信息)的总编码数减去257,并输出到接下来的5位中,将HDIST即tree_d(该霍夫曼树主要用于LZ77编码里重复性数据的处理)的总编码数减去1,并输出到接下来的5位中,最后得出HCLEN即上面提到过的tree_cl的lengths数组的长度信息,解压缩时,就需要先根据HCLEN的值,来将tree_cl的lengths数组给读取出来。此外,一开始读取出来的tree_cl的lengths数组的值是打乱过的,在lodepng.c文件里有一个CLCL_ORDER数组,该数组里的值就是根据RFC1951文档来设置的:

static const unsigned CLCL_ORDER[NUM_CODE_LENGTH_CODES]
  = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15};


    通过上面这个数组,就可以将tree_cl里打乱的lengths数组里的值还原为正常顺序。

    前面提到的mytest测试程序里的out变量用于存储压缩数据:

[email protected]:~/lodepng/lodepng-master/mytest$ gdb -q mytest
...................................................
(gdb) x/17bx out
0x80600a8:	0x05	0xc1	0x01	0x01	0x00	0x00	0x00	0x82
0x80600b0:	0xa0	0x6d	0xa6	0xff	0x37	0x05	0x30	0xad
0x80600b8:	0x03
...................................................
[email protected]:~/lodepng/lodepng-master/mytest$ 


    上面out变量里的17个字节是压缩数据,这些数据的解压过程如下图所示:


图3

    以上就是Deflate相关的内容。限于篇幅,作者只能介绍其中一部分内容,读者如果需要完全弄清楚里面的机制,还需要结合前面提到的RFC1951文档与lodepng.c文件的代码,进行更深层次的分析。

    lodepng项目,在将PNG图片里的压缩数据通过Deflate算法解压后,如果有必要的话,再经过lodepng_convert函数进行color Type的类型转换,就可以得到像素的R,G,B,A值,将还原后的像素颜色值输出到窗口界面,就可以看到PNG图片了。

    因此PNG结构中的难点,主要还是Deflate算法。

    PNG当中,其他没介绍到的内容,比如过滤算法,隔行扫描,调色板等内容,请参考前面提到过的维基百科文章与RFC2083文档。

文章中的相关链接:

    本篇文章里面,涉及到的一些资料的链接地址如下:

    http://en.wikipedia.org/wiki/Portable_Network_Graphics (维基百科链接) 和 http://tools.ietf.org/html/rfc2083 (RFC2083) 这两个链接中介绍了PNG的结构。

    http://tools.ietf.org/html/rfc1950 (RFC1950) 描述zlib的标准文档。

    http://tools.ietf.org/html/rfc1951 (RFC1951) 描述Deflate压缩数据格式的标准文档。

    霍夫曼编码_维基百科 维基百科里对霍夫曼编码的描述。

    如果文章中,有链接地址无法直接访问的话,请使用代理访问。

    OK,就到这里,休息,休息一下 o(∩_∩)o~~

    我可以教你学琴,但是给不了你成功。

—— 和你在一起
 
上下篇

下一篇: zenglOX v3.1.0 Sound Blaster 16

上一篇: zenglOX v3.0.0与v3.0.1 GUI窗口界面

相关文章

zenglOX v0.0.4 IRQ(中断请求)与PIT(可编程间隔定时器)

zenglOX v2.2.0 ee(easy editor)文本编辑器 C标准库函数 uheap(单独的用户堆空间) atapi驱动BUG zenglfs文件系统BUG 分页BUG 堆算法BUG等修复

zenglOX v0.0.2 VGA输出显示字符串

zenglOX v1.2.0 ISO 9660文件系统

zenglOX v3.1.0 Sound Blaster 16

zenglOX v3.2.0 USB v1.1, UHCI, USB KeyBoard, USB Mouse