GIF 字节格式解析

最近花了点时间用 C++ 写了一个 GIF 图片的解析程序,在这一过程中我找了许多中文相关的材料,但没有哪一篇是真正能够让读者完全理解 GIF 的文件格式和 LZW 在 GIF 中的应用(解析部分)。在查阅了一些官方文档后我算是顺利的将程序完成了,顺道我就把 GIF 文件的解析在这儿讲讲清除,方便大家学习。

下面这两个网页是我参考的比较权威的资料,大家也可以直接阅读。

http://giflib.sourceforge.net/index.html
https://www.w3.org/Graphics/GIF/spec-gif89a.txt

GIF 文件格式

下图是我从 http://giflib.sourceforge.net/index.html 拿过来的图,从图上可以很清晰的看到 GIF 文件的内部组成。

上图中一个有十一块,其中有八块是 GIF 图像必备的:Header(头)、Logical Screen Descriptor(逻辑屏幕描述符)、Image Descriptor(图像描述符)、Image Data(图像数据流)、Plain Text Extension(文本扩展)、Application Extension(应用扩展)、Comment Extension(注释扩展)、Trailer(尾部标记)。

另外还有三个可选块:Global Color Table(全局颜色表)、Graphic Control Extension(图形控制扩展)、Local Color Table(本地颜色表)。

传统的 Gif 解码器正是通过上述的这些块来对 GIF 文件进行解析,下面我们就按照顺序来详细了解一下这些块的内部字节格式。

为了比较直观的了解这些内容,我还是从 http://giflib.sourceforge.net/index.html 拿了一个 GIF 图过来,如下图:

其放大后的效果图如下:

上图经过放大处理,其代表了一个 10*10 个像素的 GIF 图,其内部字节如下:

47 49 46 38 39 61 
0A 00 0A 00 91 00 00 
FF FF FF FF 00 00 00 00 FF 00 00 00 
21 F9 04 00 00 00 00 00 
2C 00 00 00 00 0A 00 0A 00 00 
02 16 8C 2D 99 87 2A 1C DC 33 A0 02 75 EC 95 FA A8 DE 60 8C 04 91 4C 01 00
3B

GIF 文件由它的 Header 块最为其文件的入口,Header 一共包含六个字节,如下图:

其中前三个字节对应 ASCII 码中的 G、I、F 三个字符,后三个字节用于说明此 GIF 的版本号,目前的版本号有 87a 和 89a 两个。

对于上面的示例图来说,前六个字节分别是 47、49、46、38、39、61。 用 0xED 查看如下图:

Logical Screen Descriptor

Logical Screen Descriptor(逻辑屏幕描述符)通常紧跟在 Header 后面,它的作用是告诉解码器 GIF 图像的分辨率,背景色和 Global Color Table 等信息。先看一下其字节的组成:

按顺序看,

  • Canvas Width:表示 GIF 图像的宽度,单位是像素。
  • Canvas Height:表示 GIF 图像的高度,单位是像素。
  • Packed Field:这是一个包装字段,内部的不同 bit(位)表示有不同的含义
    • 从左边数第一位表示 Global Color Table Flag,如果其为 1 ,则表示存在 Global Color Table。如果为 0,则没有 Global Color Table。
    • 从左边数第二、三、四位表示 Color Resolution,用于表示色彩分辨率,如果为 s,则 Global Color Table 的颜色数为 2^(s+1)个,如果这是 s = 1,则一共有 4 中颜色,即每个像素可以用 2位(二进制) 来表示。
    • 从左边数第五位表示 Sort Flag,它有两个值 0 或 1。如果为 0 则 Global Color Table 不进行排序,为 1 则表示 Global Color Table 按照降序排列,出现频率最多的颜色排在最前面。
    • 最右边三位表示 Global Color Table 的颜色数,如其值为 s,则全局列表颜色个数的计算公式为 2^(s+1)。如 s = 1,则 Global Color Table 包含 4 个颜色。
  • Background Color Index:表示 GIF 的背景色在 Global Color Table 中的索引。
  • Pixel Aspect Ratio:表示用于计算原始图像中像素宽高比的近似因子,一般情况为 0,顾不做深入了解。

对于我们是示例图,其 Logical Screen Descriptor 对应的字节如下:

0A 00 0A 00 91 00 00

其中:
Canvas Width = 0A00 = 10(十进制)
Canvas Height = 0A00 = 10(十进制)
Packed Field = 10010001(二进制),其中 Global Color Table 为 1,则存在 Global Color Table。Color Resolution 为 1,表示三原色分别用 2 位来表示。Sort Flag = 0,不排序。Global Color Table 中颜色数为 4 。
Background Color Index = 0,说明此 GIF 的背景色为 Global Color Table 中第一个颜色。
Pixel Aspect Ratio = 0,可忽略。

Global Color Table

Global Color Table 如果有的话就会跟在 Logical Screen Descriptor 块后面。其块中的字节格式如下:

在 Global Color Table 中每个字节仅代表一种颜色,所以 Global Color Table 的字节数 = 颜色数 * 3.在 Logical Screen Descriptor 中我们知道示例中包含 4 中颜色,即 Global Color Table 的字节数为 12 。所以读取接下的 12 个字节。其具体字节如下:

FF FF FF FF 00 00 00 00 FF 00 00 00 

根据上面的数据我们来构建 Global Color Table

索引 字节组合 颜色
0 FFFFFF 白色
1 FF0000 红色
2 0000FF 蓝色
3 000000 黑色

Graphics Control Extension

这里先不说 Graphics Control Extension,我们先看 Global Color Table 后面紧跟的那个字节,从示例中可以看到的21,21在 GIF 格式中是有特殊意义的,它表示 Extension Introducer(扩展入口),即接下来的一段数据为最开始提到的这几个扩展中的某一个扩展。

OK,那我们接着 21 往后看,下一个字节为F9,F9 也是有特殊含义的,表示这是一个 Graphics Control Extension。

Graphics Control Extension 算上 21 和 F9 一共有八个字节,其结构如下图:

其中前两个字节上面已经提到,看接下来的几个字节分别表示什么含义:

  • Byte Size:表示接下来的有效数据字节数。
  • Packed Field:是一个包装字段,内部不同位的意义也不同。
    • 从左边数一,二,三位表示Reserved for Future Use,即保留位,暂无用处。
    • 从左边数四,五,六位表示 Display Method,表示在进行逐帧渲染时,前一帧留下的图像作何处理:0:不做任何处理。1:保留前一帧图像,在此基础上进行渲染。2:渲染前将图像置为背景色。3:将被下一帧覆盖的图像重置。
    • 从右数第二位表示 User Input Flag,表示是否需要在得到用户的输入时才进行下一帧的输入(具体用户输入指什么视应用而定)。0 表示无需用户输入。1 表示需要用户输入。
    • 最右边一位,表示 Transparent Flag,当该值为 1 时,后面的 Transparent Color Index 指定的颜色将被当做透明色处理。为 0 则不做处理。
  • Delay Time:表示 GIF 动图每一帧之间的间隔,单位为百分之一秒。当为 0 时间隔由解码器管理。
  • Transparent Color Index:当 Transparent Flag 为 1 时,此字节有效,表示此索引在 Global Color Table 中对应的颜色将被当做透明色做处理。
  • Block Terminator:表示 Extension 到此结束。

下面看一下示例中 Graphics Control Extension 对应的字节:

21 F9 04 00 00 00 00 00

其中 21,F9 表示这是一个 Graphics Control Extension 块。
Byte Size 为 4。
其它值都为 0 ,概括来讲此 Graphics Control Extension 对应下一帧的渲染无需任何处理,也不需要用户输入,也没有需要做透明处理的颜色值。渲染器要做的就是直接把下一帧图像渲染在画布上即可。

Image Descriptor

上面讲到 21 上 Extension 的标识符。这里的 Image Descriptor 也有自己的标识符,为 2C。下面看一下 Image Descriptor 内部字节结构:

其中 Image Seperator 为固定值 2C。

  • Image Left:该值表示下一帧图像渲染位置离画布左边的距离(从 0 开始)。
  • Image Top:该值表示下一帧图像渲染位置离画布上边的距离(从 0 开始)。
  • Image Width:该值表示下一帧图像的宽度。
  • Image Height:该值表示下一帧图像的高度。
  • Packed Field:这是一个包装字段,内部不同位的意义也不同。
    • 从左数第一位:Local Color Table Flag,表示下一帧图像是否需要一个独立的颜色表。1 为需要,0 为不需要。
    • 从左数第二位:Interlace Flag,表示是否需要隔行扫描。1 为需要,0 为不需要。
    • 从左数第三位:Sort Flag,如果需要 Local Color Table 的话,这个字段表示其排列顺序,同 Global Color Table。
    • 从左数第四、五位:Reserved For Future Use,保留位。
    • 从左数最后三位:Size of Local Color Table,同 Global Color Table 中的该位。如需要本地颜色表,则该数有效。

接着看一下示例中的对应字节:

2C 00 00 00 00 0A 00 0A 00 00 

2C 表示 Image Descriptor
Image Left = 0
Image Top = 0
Image Width = 10
Image Height = 10
上面这四个数字表示即将渲染的一帧大小为 10*10 像素,正好与 GIF 图的分辨率一致。
打包字段都为零,表示下一帧不需要 Local Color Table,也不需要进行隔行扫描。

这里读者可能会好奇我们不是在 Logical Screen Descriptor 中知道了图像的分辨率吗,为什么还要在 Image Descriptor 中额外指定图像的宽和高。其实 GIF 在进行编码的时候并不一定对每一帧进行全尺寸的压缩。因为有时候一个 GIF 图只有中间区域是动的,四周都是静止的,那只需要对中间那部分进行压缩编码即可。所以这里的 Image Left、Image Top、Image Width 和 Image Height正好可以指定一个小于等于 GIF 分辨率的图像。

Local Color Table

local color table 在本示例中不涉及,我也不多介绍,在处理的时候按照 Global Color Table 处理即可。

Image Data

如果存在 Local Color Table,Image Data 就紧跟其后。如若不存在,则紧跟在 Image Descriptor 后。下面先看一下 Image Data 的内部字节组成。

  • LZW Minimum Code Size: GIF 在对每一帧的像素颜色在 Color Table 所对应的索引进行 LZW 压缩,这里的 LZW Minimum Code Size 就是 LZW 压缩中很关键的一个值,不过目前这个值先放着,等后面讲到对 LZW 解压缩时再讲。
  • Number of bytes of data in sun-blocks(01-FF):这个值表示在其后面的有效字节的个数。范围为 01-FF,当其值为 0,则表示 Image Data 到此为止,后面就是其他块的数据了。这里需要注意由于其最大值为 FF,但图像的像素个数可能会大于这个值,所以从图上也能知道这个 Data sub-Blocks是有可能接连出现很多个的。
  • Data Sub-Block(s):表示有效的字节块。
  • Block Terminator:表示 Image Data 的结束部分。

接着看一下示例中对应的字节:

02 16 8C 2D 99 87 2A 1C DC 33 A0 02 75 EC 95 FA A8 DE 60 8C 04 91 4C 01 00

02 表示 LZW Minimum Code Size。
16(十六进制) 表示后面紧跟着的 22 个字节用来表示下一帧的图像数据。
00 表示 Image Data 到此为止。

Trailer

从上面的示例看,最后还剩下一个字节 3B,这个在 GIF 中也有特殊含义,是尾部标记的意思,GIF 的字节内容到此就结束了。

Plain Text Extension、Application Extension和Comment Extension

最后还剩下上面三个 Extension,他们主要是为 GIF 提供一些额外的信息,本身的信息对实际的渲染没有多少影响。所以这里我也不多介绍,想深入了解的可以阅读一开始提到的两个网址。

总结

本文主要以 GIF 内部字节的格式作为出发点,简单介绍了十一种块。只有充分理解了各个块内部的含义,才能为其编写正确的解码器。但仅仅只了解各个块还是不够的,GIF 的图像数据采用的是 LZW 算法进行压缩,所以还需要对 LZW 有较深理解。在下一篇《GIF 与 LWZ》中我将结合本文中的示例图像,详细讲解如何通过 LWZ 对 GIF 的图像数据进行压缩和解压。

如文章对您有所帮助,可以请作者喝杯 🍵