GIF文件格式

96
小鸟君
2017.07.14 17:52* 字数 5124


参考文章:What's In A GIF

本文全面介绍了GIF文件格式的编码格式。

GIF文件内容

GIF现在是一个W3C标准。它由一系列数据块组成,前两个块是固定长度且固定格式的。之后的数据块可以是变长的,但需要自我描述;它们的组成为:一个标志块类型的字节,跟一个长度类型,再跟有效的数据负载。


GIF文件格式

上图描述了这些不同类型的块在GIF文件中的位置,中间的那个大块可以被重复任意次。

接下来,我们来详细说明每一个块。

Header Block

所有GIF都必须以一个header block(头部块)开始。header的长度固定为6个字节,以ASCII编码,开头三个字节称作signature(标志),它们必须为“GIF”这三个字母。接下来的三个字节指定version(版本),通常版本字符串为“89a“或者”87a“,现代GIF处理引擎均同时支持二者。考虑到最大兼容,除非文件包含GIF89的特性,GIF一般都会选择87a

Header Block

Logical Screen Descriptor

Logical Screen Descriptor(逻辑屏幕描述符)紧跟在header后面。这个块告诉decoder(解码器)图片需要占用的空间。它的大小固定为7个字节,以canvas width(画布宽度)canvas height(画布高度)开始。它们中的每个占用2个字节,均为无符号整形(0-65,535)。

在GIF文件格式中,所有的多字节值均以小端格式(little-endian format,从最后一个字节开始往前保存)存储。比如在我们看来 0A 00,这个值实际写作 000A,也就是十进制的10。

现代的处理引擎一般会忽略掉canvas width和canvas height,因为GIF最初被设计为一种类似picture wall(照片墙)的东西,思路是讲图片在一个virtual canvas(虚拟画布)中显示;现在GIF通常是作为用来保存动画中的帧的图片库,相当于是一个图片集合,由GIF处理引擎来在自己的canvas(画布)中处理图片,因此现在GIF格式中保存的信息显得没太大作用了。所以,现在canvas width和canvas height基本上就是个摆设了。

接下来的3个字节是4个域(field)打包值,也就是“logical screen descriptor”。例如,这个值为91 00 00,也就是二进制的 10010001,我们用一张图来描述这些位代表的意义:


Logical Screen Descriptor

最高位字节称为global color table flag(全局颜色表标志)。如果它为0,就表示没有global color table。如果为1,表示后面会跟上global color table。因此,上面举的例子表示开启了global color table(通常都是开启的)。

接下来的三个字节是color resolution(颜色分辨率)。只有当global color table flag为1时它们才有意义,它用来计算表的大小。比如如果这个值为N,就意味着global color table中包含2 ^ (N + 1)个条目。因此,例子中的001代表2 bits/pixel,如果是111则表示8 bits/pixel。

接下来的一个字节为sort flag(排序标志)。如果为1,global color table中的颜色以“重要性降低”(decreasing importance)来排序,通常也就是“频率降低”方式。这个标志可能会对decoder有所帮助,不过并不是必要的,因此这里的例子将它简单地设置为0。这个标志是历史遗留产物。

接下来的字节会给我们提供background color index(背景色索引)。仅当global color table flag为1才有意义,此时这个标志应该设为0。在之前的“picture wall”模式中,我们说到GIF会有一个virtual canvas画布,因此理所当然会存在一个背景色,而我们这个标志就是指出这个背景颜色。同样,在现代引擎中它也随着“picture wall模式”一起被弃用了。

最后一个字节是pixel aspect ratio(像素高宽比)。GIF标准没有给出它存在的理由,不过目前对它的处理方式只是读取其值并保存下来。

Global Color Table

GIF格式可以拥有global color table,或用于针对每个子图片集,提供local color table。每个color table由一个RGB(就像通常我们见到的(255,0,0)红色 那种)列表组成。

正如之前说到的,global color table的长度为2 ^ (N + 1),因此这个表占用 3 * 2 ^ (N + 1)个字节。


注意global color table被标记为可选的,因此它并不是每个GIF都有的,然而如果global color table flag被设置为1,就应该在logical screen Descriptor后跟上颜色表。


Global Color Table

Graphics Control Extension

Graphics Control Extension(图像控制扩展)块用于指定透明度以及控制动画。它是GIF89中的可选扩展。在后面(Transparency and Animation)我们会详细地介绍它的语义。

首个字节是extension introducer(扩展介绍符)。所有的扩展块都以21开始。接下来是graphics control label,F9,表示这是一个graphic control extension。第三个字节是block size(块大小)。

接下来的字节是一个打包域,其1-3字节保留尚未使用,4-6表示disposal method(处理方法)。倒数第二个位是user input flag(用户输入标志),最后一位transparent color flag(透明色标志)。随后的两个字节为delay time值,以unsigned格式保存。之后是transparent color index(透明色索引)。最后,以00作为Block Terminator(块结束符)。


Graphic Control Extension

Image Descriptor

一个GIF文件一般包含多个图片。之前的图片渲染模式一般是将多个图片绘制到一个大的(virtual canvas)虚拟画布上,而现在一般将这些图片集用于实现动画。

每个image都以一个image descriptor block(图像描述块)作为开头,这个块固定为10字节。

第一个字节是image separator(图像分隔符)。每个image descriptor以2C作为开头,后8个字节代表图片的位置以及随后的图片数据的大小。

GIF中一个image不需要占用整个在logical screen descriptor中定义的canvas画布大小。因此,image descriptor指定image left position和image top position来表示image在canvas中的起始位置。同样,在现代渲染模式中它们是无用的。

接下来的块指定image width和image height。每个值都是2字节的,无符号小端格式。

最后的字节是另一个packed field。首位是local color table flag(局部颜色表标志)。设置位1表示允许你指定图片数据使用一个不同的颜色表(跟在后面)以取代全局颜色表。

第二个位是interlace flag(隔行扫描标志)。隔行扫描方式会改变图像渲染到屏幕上的方式,从而减少烦人的视觉闪烁(类似垂直同步)。隔行扫描在显示器上的效果是,第一轮扫描先立即模糊地显示图像,随后再一轮将其填充锐化。这种方式会让人们感觉更舒服,因为它可以让人们模糊地意识到即将显示的东西是什么,而不是等待像素点被一行一行地填充绘制。要支持这种显示方式,图片的扫描行需要以一种特定的顺序来存储,需要分为4个部分,每个部分都是一个完整的模糊显示,通过4次显示加成使得图片越来越清晰,最终完全呈现。


Image Descriptor

Local Color Table

local Color Table(本地颜色表)的组织方式与global color table类似。当且仅当local color table flag被设置为1时,它才启用,这个块跟在Image Descriptor后。它仅仅对跟在它后面的那个Image Data(图像数据)有用。如果没有指定local color table,那么图像就会使用global color table。

local color table的大小可以通过image descriptor中的相关值计算。计算方式与global color table相同。

Image Data

终于到了图片数据实际存储的地方。Image Data是由一系列的输出编码(output codes)构成,它们告诉decoder(解码器)需要绘制在画布上的每个颜色信息。这些编码以字节码的形式组织在这个块中。

关于解码这些输出编码到一张图片的过程,之后还会详细讨论。这里我们仅仅来看一下这个块的大小如何确定。

这个块的第一个字节是LZW minimum code size(LZW是一种图片压缩方式)。这个值用来解码这个经过压缩的输出编码(同样,之后会讨论LZW压缩方式的工作过程)。剩下的字节则代表data sub-blocks(数据子块),它们以一种类似链表的方式组织。数据子块由一些从1-256的字节码组成,子块的第一个字节告诉你后面实际跟了多少字节的数据。这个值可以是0到255之间的值。当你读出的所有这些字节后,下一个读出的字节会告诉你后面还跟了多少数据。我们会一个接一个地读取这些数据子块,直到到达一个子块,它告诉你后面有0个字符,让你滚。


Image Data

Plain Text Extension

GIF89标准允许你指定一段文本覆盖在其跟随的图片上作为标题。这个东西基本上没有什么卵用,浏览器、图像处理应用(如PhotoShop)会直接忽略它,GIFLIB也不打算解释它。

这个块以extension introducer(21)开始。紧跟着的一个字节是plain text label(纯文本标志)。这个标志设置为01,用以将plain text extension与其他extension区分开。下一个字节是block size(块大小),这个东西告诉你离实际数据还距离多少个字节,或者换句话说,你可以从此处跳过多少个字节。这个值可能为0C,表示你可以向后跳过12个字节。最后跟的数据子块的组织方式与Image Data中的类似。

Application Extension

GIF89标准允许在GIF中嵌入“应用指定“的信息。这个东西也不怎么用到。

同样,这个块以21开始。下一个字节同样是extension label, 在application extension中为FF。接下来是block size,表明距离实际的application data开始还有多少个字节。这个值应该设置为0B,表示有11个字节。这11个字节用来保存两个信息。第一个信息是application identifier,占用8个字节。后面3个字节是application authentication code。

Comment Extension

最后一个GIF89扩展类型是Comment Extension。这个块允许你在GIF文件中嵌入ASCII文本,有时它被用来作为图片的描述、图像署名,以及类似GPS坐标这种可读取的元数据。

第一个字节为21,extension label为FE标志这是一个comment label。同样,它的数据子块与前面的块采用相同的组织形式。


Comment Extension

Trailer

尾部标志用来指明你已经到达文件尾端,固定为3B。


LZW图片数据压缩算法

GIF文件中的图像是一种光栅格式,GIF存储像素点的颜色在颜色表(Global Color Table或Local Color Table)中的索引。


样本图片

颜色表来自global color table block。这些颜色以它们在文件中出现的先后顺序排列,第一个颜色的索引位置为0。我们以自左上到右下的顺序对每个像素进行编码,因此,样本图片可以这样来编码:

1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 2, 2, 2, ...

这个序列是前五行的像素信息的编码,我们可以以这种方式一直编码下去,直到表示完整个图片。不过,这样耿直的编码方式势必导致图片文件增大。还好,GIF格式允许我们利用数据重复输出的方式来压缩数据大小。

LZW压缩

GIF使用的压缩算法叫做LZW(Lempel-Ziv-Welch)Compression,我们简单地看一下它的核心思想。使用这种方式压缩,我们需要准备一张code table(编码表)。这个编码表允许我们指定一个简单的编码来代表一个较复杂的颜色序列。我们要做的第一步是初始化这个编码表。例如,我们可以建立一张如下的表(用#将编码与颜色值区分开):


编码表示例

而LZW压缩算法的核心思想,就是通过尽可能地提取出那些高频重复的序列,将这些重复的颜色序列在编码表中登记,然后用这些编码来替换原始图片数据,以达到压缩的目的。

具体细节,请阅读LZW Image Data

动画和透明

众所周知,除了作为一种图片存储格式外,GIF图像(特别是GIF89a)还可以实现很多特殊功能,比如透明和动画。这些东西可以通过Graphics Control Extension块的帮助来实现。下面是一个该块的例子:


Graphic Control Extension

接下来,我会展示如何操作此块以实现某些特殊的效果。

动画

我们知道,动画是通过快速播放一系列略微不同的图片,从而产生的一种效果。GIF动画也是如此,它存储一些列图片,先显示一张图片,然后告诉机器播放下一张图片之前需要等待的时间,到时则继续显示下一张图。

我们来看一个红绿灯GIF,下面是这个文件的二进制内容。


根据我们之前讲的GIF文件格式,我们可以轻易地读懂这些二进制码所代表的内容。首先是header,logical screen descriptor和全局颜色表,颜色表中定义了几个我们需要用到的颜色(0=红,1=绿,2=黄,3=浅灰,4=黑,5=白,6=黑(未使用),7=黑(未使用))。

然后跟着一个application extension block块,这点可以从21 和 FF标志看出,这个扩展块导致了这个GIF图片无限重复播放,而不仅仅是播放一两次就停了。从块的第三个0B可以直到后面跟着11个字节的固定长度数据,内容为“NETSCAPE2.0”。接下来则是实际的“application data”,它被放在子块中。子块中一共保存了两个值,第一个值是固定的01。然后,另一个值是一个无符号的值,指明动画需要被重复多少次。样本中,可以看到这个值为0,这说明这张图片的动画是一个无限循环的播放,这三个字节前的03告诉解码器数据长度为3个字节,并且以00作为结束符。


样本gif

这就是样本的显示效果,它由3个场景构成,红绿灯中的颜色会依次闪烁呈现。

第一个组块紧跟在application extension块后,它是我们碰到的第一个graphic control extension。和所有扩展块一样,它以21开头,随后的F9标志说明了它是什么类型的扩展块。然后我们会看到byte size,它总是为04。好了,这四个数据块中,首先是一个打包域。

这个打包域保存了3个值。最前面3个字节为保留字节。其次三个字节指明disposal method,这个东西指定解码器在解码下个图片数据块时采用的操作。3个字节的大小意味着我们可以使用0-7之间的值。样本图片采用1来执行动画,解码器在这种方式下会留下先前的图片信息在画布上,然后直接将下一张图片覆盖绘制在上面。如果是值2,解码器则会在绘制下一张图片之前,先将画布填充为背景色(在logical sreen descriptor中指定),之后再进行绘制。方式3不常用,而方式4-7尚未被定义。如果不想让gif执行动画,则应该指定为0。

第7个位是user input flag。当它为1时,意味着解码器将在移动到下一个场景之前会等待某些来自人类观看者的输入。实际上很难见到这个标志位不为0的情况。最终的位是transparency flag(透明标志)。后面还会详细介绍透明的信息,然而样本图片没有透明色,因此为0。

接下来的两个位是延迟时间。它表示在移动到下一个场景之前所等待的百分之一秒数。样本图片在第一个graphics control block中指定的值为100(64 00),意味着在播放下一张图片之前的延迟为1秒。

graphics control extension block以块结束符00结束。你可能注意到了这个快在后面还出现了两次,并且只有延迟时间上有点差别(黄灯到红灯只有半秒的延迟)。

下面的一个组块为image descriptor。这个块申明我们将会自左上到右下地在整个画布(11px x 29px)上绘制图片,这个块后跟着image data,它包含了绘制第一个场景的所有编码。

如果我们对比第一和第二个场景,会发现它们之间存在很多重复的像素色点。与其重新绘制整个画布,我们可以指定只绘制两个场景中不同的部分(也就是,一个覆盖两者改变区域的最小矩形)。你可以发现第二张图片的image descriptor指定了第二张图片的起始位置为(2,11),并且只绘制一个7x宽16px高的矩形区域。这个区域刚好覆盖了两个场景之间的不同部分。这一切得以实现,归功于之前在graphics control extension中选择了dispose method 1。

透明

GIF图片一般是一个矩形区域,它覆盖了图像下面的背景,而透明允许“显示图像下面的内容”。这实际上是一个非常简单的技巧:我们可以在颜色表中指定一个特殊的颜色,当绘制时遇到这个颜色时,我们就显示背景。很简单,在graphics control extension块中有两个标志位与设置透明相关,它们的意义很明确,这里就不再赘述了。

日记本
Web note ad 1