gif格式图片的解析

时间:2021-01-31 09:17:29

GIF(Graphics Interchange Format,图形交换格式)文件是由 CompuServe公司开发的图形文件格式,版权所有,任何商业目的使用均须 CompuServe公司授权。

GIF文件内部是按块划分的,包括控制块( Control Block )和数据块(Data Sub-blocks)两种。控制块是控制数据块行为的,根据不同的控制块包含一些不同的控制参数;数据块只包含一些8-bit的字符流,由它前面的控制块来决定它的功能,每个数据块大小从0到255个字节,数据块的第一个字节指出这个数据块大小(字节数),计算数据块的大小时不包括这个字节,所以一个空的数据块有一个字节,那就是数据块的大小0x00。

gif格式图片的解析

一个GIF文件的结构可分为文件头(File Header)、GIF数据流(GIF Data Stream)和文件终结器(Trailer)三个部分。文件头包含GIF文件署名(Signature)和版本号(Version);GIF数据流由控制标识符、图象块(Image Block)和其他的一些扩展块组成;文件终结器只有一个值为0x3B的字符(';')表示文件结束。下表显示了一个GIF文件的组成结构:

gif格式图片的解析

GIF署名用来确认一个文件是否是GIF格式的文件,这一部分由三个字符组成:"GIF";文件版本号也是由三个字节组成,可以为"87a"或"89a".具体描述见下表:

gif格式图片的解析

逻辑屏幕标识符(Logical Screen Descriptor)

gif格式图片的解析

背景色索引  -  背景 色指向全局色表。背景 色是指那些没有被 像覆盖的 屏部分的 色。若全局色表 志位置 0 则该 字段也被赋值 0 ,并且被忽略。

象素高  -  用于 算原 像中像素的近似高 比。如果 字段的 值为 0 象素的高 比由下面的公式
 = ( 象素高  + 15) / 64
字段的取 从最 的比 4 1 到最高的比 1 4 增的 1/64  0 -  没有比 1 255 -  用于 算的

全局色表  -  指示有没有全局色表,如果 该标 志位置 1 全局色表会 接在该块之后出 位也用于解 是否 用背景 色索引字段。若 位置 1 背景 色索引字段的 将指向背景 色表。

色彩方案  -  提供 原始 像的 色的位数减 1 代表 像中所使用的整个 色板的大小,而不是 像中所使用的 色的数量。例如,若 字段的 值为 3 则图 像中所使用的 色板的 个色 4 位。

 -  表明全局色表是否被排序。如果 位置 1 全局色表按照重要性 减的原 则进 行了排序。典型地,是按照 色的使用 减排序,使用 度最高的 色排在色表的最前面。 这样 便可帮助解 选择 最好的 色子集来成象。

全局色表的尺寸  -  如果全局色表 志位置 1 则该 字段的 值记录 全局色表中所占用的字 数。

全局颜色列表(Global Color Table)必须紧跟在逻辑屏幕标识符后面,每个颜色列表索引条目由三个字节组成,按R、G、B的顺序排列。
gif格式图片的解析

图象标识符(Image Descriptor)包含理一个基于像的表的必要参数,它是一幅所必需的,图象标识符以0x2C(',')字符开始。一幅对应一个图象标识符,图象标识符后面跟着对应的像数据。

gif格式图片的解析

局部颜色列表(Local Color Table)

如果上面的局部颜色列表标志置位的话,则需要在这里(紧跟在图象标识符之后)定义一个局部颜色列表以供紧接着它的图象使用,注意使用前应线保存原来的颜色列表,使用结束之后回复原来保存的全局颜色列表。如果一个GIF文件即没有提供全局颜色列表,也没有提供局部颜色列表,可以自己创建一个颜色列表,或使用系统的颜色列表。局部颜色列表的排列方式和全局颜色列表一样:RGBRGB......

图形控制扩展(Graphic Control Extension)

这一部分是可选的(需要89a版本),可以放在一个图象块(图象标识符)或文本扩展块的前面,用来控制跟在它后面的第一个图象(或文本)的渲染(Render)形式,组成结构如下:

gif格式图片的解析

充引入  -  用于 识别 一个 始, 字段 固定 0x21
像控制  -  识别 当前 是否 为图 形控制 充。 字段 固定  0xF9
尺寸  -  中所包含的字 数。从 尺寸字段 始到快 束符(不含 束符)。 字段包含固定 4
配置方法  -  指示 示后的 理方法。 :
0 -  无指定的配置,解 器不需要做任何 理。
1 -  不做配 像将被留在原位置。
2 -  背景 色。 像所占的区域必 须备 复为 背景 色。
3 -  以前的 色。解 器需要将 像区域恢 复为 原来成象的 色。
4-7 -  未定
户输  -  明在 继续处 理之前是否需要用 户输 入。可以和 入延 一起使用。
透明  -  表明在透明索引字段是否 定透明索引。
 -  如果不 0,  字段指定以 1/100 为单 位的 延数。
透明索引  -  如果遇到透明索引, 则显 设备 的相 象素不被改 继续处 理下一个象素。
块终止符  -  0 度字段 志着 像控制 充得 束。

基于颜色列表的图象数据(Table-Based Image Data),由两部分组成:LZW编码长度(LZW Minimum Code Size)和图象数据(Image Data)。

gif格式图片的解析

GIF图象数据使用了LZW压缩算法(详细介绍请看后面的『LZW算法和GIF数据压缩』),大大减小了图象数据的大小。图象数据在压缩前有两种排列格式:由图象标识符的交织标志控制)。连续方式按从左到右、从上到下的顺序排列图象的光栅数据;交织图象按下面的方法处理光栅数据:

创建四个通道(pass)保存数据,每个通道提取不同行的数据:
第一通道(Pass 1)提取从第0行开始每隔8行的数据;
第二通道(Pass 2)提取从第4行开始每隔8行的数据;
第三通道(Pass 3)提取从第2行开始每隔4行的数据;
第四通道(Pass 4)提取从第1行开始每隔2行的数据;

gif格式图片的解析
LZW for GIF 算法原理

LZW是一个字典式压缩算法,他在压缩原始数据时,对每一个新出现的原始数据串赋一个数值作为标号,那么下次又出现了这个串后,就可以用这个值来代替了。比如:

原始数据:  ABCCAABCDDAACCDB      ,
ABCD可以用0~3的数来表示。那么注意这个字符串中出现了好几个重复的字串:
AB CCA ABCDDAACCDB
那么就可以用 4来代表 AB,5来代表 CC 等等,原来的字符串就变为
压缩后的数据: 45 A 4 CDDAA5DB ,
变短了一点点。实际上上面这个字符串号可以进一步的压缩,等会再谈。

为了区别代表串的值和原来的单个的数 据值,需要使它们的数值域不重合,比如说原来的数我们是以8位为单位来处理的(就算实际上不是8位的,我们也可以看作是8位的,反正是一个0101的数据 流),那么就认为原始的数的范围是0~255,压缩程序生成的标号的范围就不能为0~255,可以从256开始,但是这样一来就超过了8位的表示范围了, 所以 LZW 算法必须要扩展数据的位数,至少要扩一位,这样看起来不是反而是增加了数据流的体积了吗?不过如果能用一个数据代表一个原始数据串,那么还是划得来的。从这个原理也可以看出LZW算法的适用范围, 那就是原始数据串最好是有大量的子串多次重复出现,重复的越多,压缩效果越好。反之则越差,可能真的不减反增了
LZW算法在处理数据时,随着新的串的不断发现,标号的值也就不断增加,增加到一定的程度,出与查找效率和标号集(也就是字典)所需存储空间的考虑,就不能再让它增加了,那么怎么办呢?干脆就从头开始,在这里做一个标记( 清除标志 CLEAR),表示从这里我 重新开始构造字典字典了,以前的所有标记作废,开始使用新的标记。这个标号集的大小多少比较合适呢?据说理论上是越大压缩率越高(我个人感觉太大了也不见得就好),不过处理的开销也呈指数增长, 一般都是根据处理速度和内存空间选定一个大小,GIF规范规定的是12位,超过12位的表达范围就推倒重来,并且GIF为了提高压缩率,采用的是变长的字 长。比如说原始数据是8位,那么一开始,先加上一位再说,开始的字长就成了9位,然后开始加标号,当标号加到512时,也就是超过9为所能表达的最大数据 时,也就意味着后面的标号要用10位字长才能表示了,那么从这里开始,后面的字长就是10位了。依此类推,到了2^12也就是4096时,在这里插一个清 除标志,从后面开始,从9位再来。
GIF规定的清除标志 CLEAR 的数值是原始数据字长表示的最大值加 1,如果原始数据字长是8清除标志就是256,如果原始数据字长为4那么就是16。另外GIF还规定了一个 结束标志 END ,它的值是清除标志 CLEAR 再加 1。由于GIF规定的位数有1位(单色图),4位(16色)和8位(256色),而1位的情况下如果只扩展1位,只能表示4种状态,那么加上一个清除标志和结束标志就用完了,所以1位的情况下就必须扩充到3位。其它两种情况初始的字长就为5位和9位。
好了,现在开始谈谈LZW的具体算法。前面已经说了,LZW是基于字典的压缩方法,那么这个字典是怎么来的呢?难道先编一本“大百科字典”,随压缩包免费奉送?这显然是不可能的。LZW算法的优点就是可以 动态生成字典,并且这个字典的信息已经包含在压缩后的数据流中了,不必再另外储存字典信息了。下面以一个压缩过程为例来说明一下。这个例子是Bob Montgomery给出的,非常的经典,我这里充当一个翻译的工作,并稍微加一点我的解释。
比如有一个字符串,是由A、B、C、D四个字符构成的,那么就可以用0 1 2 3 来表示,两位就够了。
A B A B A B A B B B A B A B A A C D A C D A D C A B A A A B A B .....
首先要扩充一位,变成3位,定义 Clear=4,End=5。那么以后的标号就从6开始。
第一步,取第一个字符,是A,A已经在我们的定义中了,也就是说,我们已经(认识)他了,就不做处理了。
下一步,取第二个字符,现在的取到的字符串为 A B ,注意,这里引入一个 前缀( prefix 后缀( suffix 的概念,一个字符串可以用一个前缀加一个后缀来表示即 (前缀,后缀)前缀是一个标号,可以是原始的字符,也可以是一个代表字符串的标号,后缀则是一个字符。那么这里取到了( A A ),以前没见过,不认识,好,现在 6 代表( A B ), 下次就认识了。因为有不认识的了,所以把前缀  A  放入到输出流中,只保留后缀  B  ,让它变成前缀。
第三步,取下一个字符,是  A ,现在的字符串是( B A ),还是不认得,用一个新标号来表示他, 7 =( B A )。把前缀  B  放入到输出流, 变成前缀。
第四步,取下一个字符,是  B  ,那么取道的是( A B ),哈,这次认得了,不就是老 6 么。好,把字符串规约到  6 ,以 6 来作为前缀。
第五步,取下一个,是 ,即为( 6 A ),又不认得了,就令 8 =( 6 A ),把前缀  6 放到 输出流。现在的前缀变成  A 了。 注意 ,到这里标号已经超过 3 位能够表示的最大范围了,所以接下来必须要 扩展数据位 ,那么接下来的数据就是以 4 位字长来表示了。接下来的过程以此类推。

编码器伪代码
Initialize String Table;
[.c.] = Empty;
[.c.]k = First Character in CharStream;
while ([.c.]k != EOF )
{
  if ( [.c.]k is in the StringTable)
  {
    [.c.] = [.c.]k;
  }
  else
  {
    add [.c.]k to the StringTable;
    Output the Index of [.c.] in the StringTable to the CodeStream;
    [.c.] = k;
  }
  [.c.]k = Next Character in CharStream;
}
Output the Index of [.c.] in the StringTable to the CodeStream;


LZW的解压
LZW 的解压过程就是一个查字典的过程。那么这个字典是从哪里来的呢?这就是 LZW 的最大优点之一,那的字典信息是完全包含在压缩后的数据中的,解压程序可以动态的从压缩过的数据中构造出来,因此解压过程也非常类似于压缩过程。
以上面的例子压缩结果为例,它的输出就是我们的输入,为:
待解压的数据流为: A B 6 8 B 10 9 A A C D 14 16 D C 8 ....
解压的过程是以一对一对数据来处理的。首先我们要知道原始数据的位数,这一点是要在处理压缩数据以前就知道的。在 GIF 文件中,可以从。我们来一步步分析这个过程:
第一步,取第一个和第二个数据,是( A B ),不认得,令 6 =( A B ),把前缀 A 放入输出流中,后缀 B 变成前缀;
第二步,取第三个数据,现在变为( B 6 )。不认得, 那么令 7 =( B 6 )吗? …… NO 请先回过头去看压缩过程,我们定义一个新的标志,前缀可以是一个代表子串的标号,但是后缀都是一个单个的数据的,所以这里我们也不能让后缀是一个字串标号。那么后缀是什么呢?让我们把 6 展开, 6 A B ,那么这里原来的字串就是 B A B ……,现在知道了把,其实 7 =( B A ), 那么7的后缀就是代表的字符串的第一个字符 。现在我们把 B 放入输出流。
第三步,取第四个数据。第四个数据是……………????怎么,不就是 8 吗。那是,明摆着写着呢。如果我不这样写,写成 0001100001011001 …………你还认得是 8 吗?由于 GIF 是变位长的,所以我们一定要清楚在这里到地取几位。先回过头来看上一步,由于上一步我们已经把标号排到 7 了, 7 3 位的最大数值,所以从这一步以后,就应该把 字长加一位 ,所以这里我们要取 4 位,就是 1000 ,也就是 8 了。好了,现在得到的是( 6 8 )。不认得,那么令 8 =( 6 ,……………………………………… 8 ??)。 8 我们还不知道呢,怎么能用它来定义自己呢?回顾一下上一步我们定义 7 的时候,取的后缀是 6 的第一个字符,那么这里我 8 的后缀也应该是 8 的第一个字符,我们知道 8 的前缀是 6 ,那么 8 的第一个字符也就是 6 的第一个字符。也就是  A 。所以 8 =( 6 A ),现在把 6 代表的字符串 AB 放入输出流,让前缀变为 8
第四步,取第五个数,现在是( 8 B ),让 9 =( 8 B ),把 8 6A AB A 放入输出流,前缀为 B
第五步,取第六个,( B 10 ),同第三步,令 10 =( B 10 的第一个字符)=( B B ),把 B 放入输出流
解码器伪代码 Initialize String Table;
[code] = First Code in the CodeStream;
Output the String for [code] to the CharStream;
[old] = [code];
[code] = Next Code in the CodeStream;
while ([code] != EOF )
{
  if ( [code] is in the StringTable)
  {
    Output the String for [code] to the CharStream; // 输出[code]所对应的字符串
    [...] = translation for [old]; // [old]所对应的字符串
    k = first character of translation for [code]; // [code]所对应的字符串的第一个字符
    add [...]k to the StringTable;
    [old] = [code]; 
  }
  else
  {
    [...] = translation for [old]; 
    k = first character of [...]; 
    Output [...]k to CharStream;
    add [...]k to the StringTable;
    [old] = [code]; 
  }
  [code] = Next Code in the CodeStream;
}

实际上解压算法要简单得多,因为他每次都是按照读入的标记来查找的,所以采用(前缀。后缀)的格式存储成一个结构数组就可以了,可以根据标记直接找到它的位置,再根据它的前缀依次往前找,每次把后缀压入一个栈,到头后弹出到输出流就可以了。