GIF 文件格式解析
图像互换格式主要分为两个版本,即图像互换格式 87a 和图像互换格式 89a。
图像互换格式 87a:是在 1987 年制定的版本。
图像互换格式 89a:是在 1989 年制定的版本。在这个版本中,为图像互换格式文档扩充了图形控制区块、备注、说明、应用程序接口等四个区块,并提供了对透明色和多帧动画的支持。
现在我们一般所说的 GIF 动画都是指 89a 的格式。
GIF 包含的数据块:
文件头(Header)
逻辑屏幕标识符(Logical Screen Descriptor)
全局颜色表(Global Color Table)
图形控制扩展(Graphic Control Extension)
图像标识符(Image Descriptor)
局部颜色表(Local Color Table)
基于颜色表的图像数据(Image Data)
Plain Text Extension
Application Extension
Comment Extension
文件结尾(Trailer)
(0) 准备 GIF 图片 & 十六进制查看工具
本文所有分析,都是基于下面这张 GIF 图片。
(1) 文件头(Header)
GIF 的前 6 个字节内容是 GIF 的署名和版本号。
我们可以通过前 3 个字节判断文件是否为 GIF 格式,后 3 个字节判断 GIF 格式的版本:
- 87a:是在 1987 年制定的版本
- 89a:是在 1989 年制定的版本
(2) 逻辑屏幕标识符(Logical Screen Descriptor)
逻辑屏幕标识符配置了 GIF 一些全局属性,我们通过读取解析它,获取 GIF 全局的一些配置。
逻辑屏幕标识符(7 个字节):
屏幕逻辑宽度:定义了 GIF 图像的像素宽度,大小为 2 字节;
屏幕逻辑高度:定义了 GIF 图像的像素高度,大小为 2 字节;
-
打包值,大小为 1 字节
- m - 全局颜色表标志(Global Color Table Flag),当置位时表示有全局颜色列表,pixel 值有意义;
- cr - 颜色深度(Color ResoluTion),cr+1 确定图象的颜色深度;
- s - 分类标志(Sort Flag),如果置位表示全局颜色列表分类排列;
- pixel - 全局颜色列表大小,pixel+1 确定颜色列表的索引数(2^(pixel+1));
背景颜色:背景颜色在全局颜色列表中的索引(PS:是索引而不是 RGB 值,所以如果没有全局颜色列表时,该值没有意义),大小为 1 字节;
像素宽高比:全局像素的宽度与高度的比值,大小为 1 字节;
从图中可以看出,这张 GIF 图片:
- 宽度:700
- 高度:700
- 有全局颜色表
- 颜色深度 8
- 全局颜色表大小 8
PS: Glide 中在读取了全局的宽高之后,忽略了颜色深度和分类标志,像素宽高比也只是读取,后续并没有使用到。
(3) 全局颜色表(Global Color Table)
全局颜色表,在逻辑屏幕标识之后,每个颜色索引由三字节组成,按 RGB 顺序排列。
在 (2) 逻辑屏幕标识符(Logical Screen Descriptor)
中得到,全局颜色表大小是 8 个颜色,每个颜色占 3 字节(R、G、B)。
(4) Application Extension
接下来出现的是 21
FF
,特定于应用程序的信息,这个并没有太大用处。唯一已知的公共文件是 Netscape 2.0 扩展(如下所述),用于循环动画GIF文件。
Netscape 2.0 循环块扩展必须立即出现在逻辑屏幕描述符的全局颜色表之后。它有19个字节长。
byte 1 : 33 (hex 0x21) GIF Extension code
byte 2 : 255 (hex 0xFF) Application Extension Label
byte 3 : 11 (hex 0x0B) Length of Application Block
(eleven bytes of data to follow)
bytes 4 to 11 : "NETSCAPE"
bytes 12 to 14 : "2.0"
byte 15 : 3 (hex 0x03) Length of Data Sub-Block
(three bytes of data to follow)
byte 16 : 1 (hex 0x01)
bytes 17 to 18 : 0 to 65535, an unsigned integer in
little-endian byte format. This specifies the
number of times the loop should
be executed.
byte 19 : 0 (hex 0x00) a Data Sub-Block Terminator.
Application Extension 这 19 个字节基本上目前所有 GIF 都一样。
(5) 图形控制扩展(Graphic Control Extension)
在 89a 版本,GIF 添加了图形控制扩展块。放在一个图象块(图象标识符)的前面,用来控制它后面的第一个图象的显示。
处置方法(Disposal Method):指出处置图形的方法:
- 0 - 不使用处置方法
- 1 - 不处置图形,把图形从当前位置移去
- 2 - 回复到背景色
- 3 - 回复到先前状态
- 4-7 - 自定义用户输入标志(Use Input Flag):指出是否期待用户有输入之后才继续进行下去,置位表示期待,值否表示不期待。
用户输入可以是按回车键、鼠标点击等,可以和延迟时间一起使用,在设置的延迟时间内用户有输入则马上继续进行,或者没有输入直到延迟时间到达而继续。
透明颜色标志(Transparent Color Flag):置位表示使用透明颜色。
从图中可以看出,这张 GIF 图片:
- 不使用处置方法
- 不使用透明色
- 后面的图像延迟 50 (单位:1/100 秒)
(6) Comment Extension
接下来出现的是 21
FE
,这允许你将 ASCII 文本嵌入到 GIF 文件,有时被用来图像描述、图像信贷或其他人类可读的元数据,如图像捕获的 GPS 定位。
下面是这张图片的 Comment Extension:
(7) 图像标识符(Image Descriptor)
一个 GIF 文件中可以有多个图像块,每个图像块就会有图像标识符,描述了当前帧的一些属性。下面我们来看看图像标识符中包含的一些信息。
图像标识符以 ',' (0x2c) 作为开始标志。接着定义了当前帧的偏移量和宽高。
最后 5 个标志的意义分别为:
- m - 局部颜色表标志(Local Color Table Flag)
置位时标识紧接在图象标识符之后有一个局部颜色列表,供紧跟在它之后的一幅图象使用;值否时使用全局颜色列表,忽略 pixel 值。 - i - 交织标志(Interlace Flag),置位时图象数据使用交织方式排列,否则使用顺序排列。
- s - 分类标志(Sort Flag),如果置位表示紧跟着的局部颜色列表分类排列.
- r - 保留,必须初始化为 0.
- pixel - 局部颜色表大小(Size of Local Color Table),pixel+1 就为颜色表的大小
从图中可以看出,这张 GIF 图片:
- 没有局部颜色表
- 顺序排列
- 局部颜色表大小为 0
(8) 局部颜色表(Local Color Table)
如果有局部颜色表,则跟 (3) 全局颜色表(Global Color Table)
一样的格式。
(9) 基于颜色表的图像数据(Image Data)
接下来就是图像数据(已使用 LZW 算法压缩,解压后才是真正的 基于颜色表的图像数据
)。
数据的第一个字节表示 LZW 编码初始表大小的位数,用于使用 LZW 算法解压数据。
后面的是图像数据块:
- 每个数据块第一个字节表示数据块大小(不包括这个字节)
- 数据块后面的一个字节表示后续数据块大小
- 当数据块后面的一个字节是
0
,表示数据结束了
如上图所示:
- LZW 编码初始表大小的位数:3
- 标蓝色的所有字节就是完整图像块数据
最后一步,我们将使用 LZW 算法解压图像数据块,并根据颜色表还原出整张图像(GIF 的一帧)的 RGB 文件。
需要删除标蓝色以外的所有字节,保存为rainbow-compressed.gif.frame
。
(10) Plain Text Extension
这个特性不起作用; 浏览器和图片处理应用程序,如 Photoshop 忽略它, GIFLIB 并不试图解释它。
(11) 文件结尾(Trailer)
标识 GIF 文件结束,固定值 0x3B。
当解析程序读到 0x3B 时,文件终结。
根据图像数据块 & 颜色表还原图像
根据 (9) 基于颜色表的图像数据(Image Data)
,可以得到 GIF 一帧图像的数据(已使用 LZW 算法压缩)。
文 件 名:rainbow-compressed.gif.frame
文件大小:3428字节
LZW 解压
这里直接使用 github.com/jefftime/lzw 这个库。
#include "stdio.h"
#include "stdlib.h"
#include "lzw/src/lzw.h"
int main () {
// LZW 编码初始表大小的位数:3
unsigned char code_size = 3;
// GIF 一帧图像的数据压缩文件(rainbow-compressed.gif.frame)大小
long total_bytes;
// GIF 一帧图像的数据压缩数据
unsigned char *img_compressed;
// GIF 一帧图像的数据解压后的数据
unsigned char *img;
// GIF 一帧图像的数据解压后大小
unsigned long decompressed_size;
FILE *gif_compressed_frame = fopen("/Users/staff/Desktop/rainbow-compressed.gif.frame", "rb+");
fseek(gif_compressed_frame, 0L, SEEK_END);
total_bytes = ftell(gif_compressed_frame);
fseek(gif_compressed_frame, 0L, SEEK_SET);
printf("Gif 一帧压缩文件大小:%li\n", total_bytes);
img_compressed = malloc((unsigned long) total_bytes);
fread(img_compressed, total_bytes, 1, gif_compressed_frame);
// 进行 LZW 解压
lzw_decompress(
code_size,
total_bytes,
img_compressed,
&decompressed_size,
&img
);
printf("Gif 一帧解压文件大小:%li\n", decompressed_size);
FILE *gif_decompressed_frame = fopen("/Users/staff/Desktop/rainbow-decompressed.gif.frame", "wb+");
fwrite(img, decompressed_size, 1, gif_decompressed_frame);
fflush(gif_decompressed_frame);
free(img_compressed);
free(img);
fclose(gif_compressed_frame);
fclose(gif_decompressed_frame);
return 0;
}
解压后得到解压文件:rainbow-decompressed.gif.frame
解压后文件大小:490000字节 (700x700x1)
解压后文件中每一个字节代表颜色表的一个颜色索引
还原出 RGB 文件
#include "stdio.h"
#include "stdlib.h"
#include "lzw/src/lzw.h"
// 颜色表
uint32_t rainbowColors[] = {
0XFF0000, // 赤
0X00FF00, // 绿
0XFFA500, // 橙
0XFFFF00, // 黄
0X0000FF, // 蓝
0X007FFF, // 青
0X8B00FF, // 紫
0X000000 // 黑
};
int main () {
......
FILE *gif_frame_rgb = fopen("/Users/staff/Desktop/rainbow-decompressed.gif.frame.rgb", "wb+");
for(int i = 0; i < decompressed_size; i++) {
// 颜色索引值
unsigned char color_index = img[i];
// 根据颜色索引取出颜色表中的颜色
uint32_t color_rgb = rainbowColors[color_index];
// 当前颜色 R 分量
uint8_t R = (color_rgb & 0xFF0000) >> 16;
// 当前颜色 G 分量
uint8_t G = (color_rgb & 0x00FF00) >> 8;
// 当前颜色 B 分量
uint8_t B = color_rgb & 0x0000FF;
fputc(R, gif_frame_rgb);
fputc(G, gif_frame_rgb);
fputc(B, gif_frame_rgb);
}
fflush(gif_frame_rgb);
......
}
这一步,得到了 GIF 一帧图像的 RGB 文件:rainbow-decompressed.gif.frame.rgb
GIF 一帧图像的 RGB 文件大小为:1470000 字节(700x700x3)
查看还原出来的 RGB 文件
ffplay -f rawvideo -pixel_format rgb24 -s 700x700 rainbow-decompressed.gif.frame.rgb
参考资料: