本文GIF文件格式参考 博客,后面的代码是我整理的博主的代码,修改了其中一小部分,是在配置好环境后可执行的代码。
1. GIF的存储格式
1)文件头
GIF格式文件头包含三部分:
-
(1).格式声明
Signature: 为 “GIF”3个字符
Version:为“87a”或“89a”3个字符 -
(2).逻辑屏幕描述块
- Logical Screen Width(2B): 以像素为单位的宽
- Logical Screen Depth(2B): 以像素为单位的高
- Global Color Table Flag(1b): 全局颜色表标志。为1时表明Logical Screen Descriptor后面跟的是全局颜色表。
- Color Resol(3b): 的值加1代表颜色表中每种基色用多少位表示,如为“111”时表示每种基色用8位表示,由于该值有时可能为0,一般在解码程序中,该3位不做处理,而直接由Global Color Table Size算出颜色表的大小。
- Sort Flag(1b): 表示重要颜色排序标志,标志为1时,表示颜色表中重要的颜色排在前面,有利于颜色数较少的解码器选择最好的颜色,一般该标志为0,不做处理。
- Global Color Table Size(3b): 的值加1作为2的幂, 算得的数即为颜色表的项数,实际上颜色表每项由RGB三基色构成,每种颜色占一个字节,则颜色表占字节数为项数的3倍。由于最大值为“111”,故颜色表的项数最多有256项,即256种颜色,8位每基色则颜色表大小有768Bytes。
- Background Color Index(1B): 背景颜色索引值。可以这样理解:在指定大小显示区,GIF图像的大小可能小于显示区域大小,
- Pixel Aspect Ratio(1B): 表示像素宽高比,一般为0,不做处理,直接以Logical Screen 宽和高作处理。如该项不为0,则参照GIF89a标准计算。
-
(3).全局调色盘
我们先考虑最直观的图像存储方式,一张分辨率M×N的图像,本质是一张点阵,如果采用Web最常见的RGB三色方式存储,每个颜色用8bit表示,那么一个点就可以由三个字节(3BYTE = 24bit)表达,比如0xFFFFFF可以表示一个白色像素点,0x000000表示一个黑色像素点。
如果我们采用最原始的存储方式,把每个点的颜色值写进文件,那么我们的图像信息就要占据就是3×M×N字节,这是静态图的情况,如果一张GIF图里有K帧,点阵信息就是3×M×N×K。
下面这张兔子snowball的表情有18帧,分辨率是200×196,如果用上述方式计算,文件尺寸至少要689K。
但实际文件尺寸只有192K,它一定经历过什么……
我们可以使用命令行图片处理工具gifsicle来看看它的信息。
gifsicle -I snowball.gif > snowball.txt
我们得到下面的文本
5.gif 19 images
logical screen 200x196
global color table [128]
background 93
loop forever
extensions 1
image #0 200x196 transparent 93
disposal asis delay 0.04s
image #1 200x188 transparent 93
disposal asis delay 0.04s
…
可以看到,global color table [128]就是它的调色盘,长度128。
为了确认,我们再用二进制查看器查看一下它的文件头
可以看到Packet里的字段的确符合我们的描述。
在实际情况中,GIF图具有下面的特征
(1)一张图像最多只会包含256个RGB值。
(2)在一张连续动态GIF里,每一帧之间信息差异不大,颜色是被大量重复使用的。
在存储时,我们用一个公共的索引表,把图片中用到的颜色提取出来,组成一个调色盘,这样,在存储真正的图片点阵时,只需要存储每个点在调色盘里的索引值。
如果调色盘放在文件头,作为所有帧公用的信息,就是公共(全局)调色盘,如果放在每一帧的帧信息中,就是局部调色盘。GIF格式允许两种调色盘同时存在,在没有局部调色盘的情况下,使用公共调色盘来渲染。
这样,我们可以用调色盘里的索引来代表实际的颜色值。
一个256色的调色盘,24bit的颜色只需要用9bit就可以表达了。
调色盘还可以进一步减少,128色,64色,etc,相应的压缩率就会越来越大……
还是以兔子为例,我们还可以尝试指定它的调色盘大小,对它进行重压缩
gifsicle --colors=64 5.gif > 5-64.gif
gifsicle --colors=32 5.gif > 5-32.gif
gifsicle --colors=16 5.gif > 5-16.gif
gifsicle --colors=2 5.gif > 5-2.gif
…
依然使用gifsicle工具,colors参数就是调色盘的长度,得到的结果
注意到了2的时候,图像已经变成了黑白二值图。
居然还能看出是个兔子……
所以我们得出结论——如果可以接受牺牲图像的部分视觉效果,就可以通过减色来对图像做进一步压缩。
2)图像帧信息
图像帧信息就是每一帧的图像信息和相关标志位,在逐项了解它之前,我们首先探究一下帧的存储方式。
我们已经知道调色盘相关的定义,除了全局调色盘,每一帧可以拥有自己的局部调色盘,渲染顺序更优先,它的定义方式和全局调色盘一致,只是作用范围不同
直观地说,帧信息应该由一系列的点阵数据组成,点阵中存储着一系列的颜色值。点阵数据本身的存储也是可以进行压缩的,GIF图所采用的是LZW压缩算法。
这样的压缩和图像本身性质无关,是字节层面的,文本信息也可以采用(比如常见的gzip,就是LZW和哈夫曼树的一个实现)。
除了采用LZW之外,帧信息存储过程中还采取了一些和图像相关的优化手段,以减小文件的体积,直观表述就是——公共区域排除、透明区域叠加
这是ImageMagick官方范例里的一张GIF图。
根据直观感受,这张图片的每一帧应该是这样的。
但实际上,进行过压缩优化的图片,每一帧是这样的。
首先,对于各帧之间没有变化的区域进行了排除,避免存储重复的信息。
其次,对于需要存储的区域做了透明化处理,只存储有变化的像素,没变化的像素只存储一个透明值。
理解了上面的以后,我们再来看具体的帧描述:
- Image Separator(1B):固定为0x2C
- Image Left position(2B) 和 Image Top position(2B):表示后面跟的一幅图像起始点相对逻辑屏幕左上角的位移。
- Image Width(2B) 和 Image Depth(2B):表示后面跟的一幅图像的实际宽度和高度。
- Packet Fields(2B)
- Local Color Table Flag(1b):局部颜色表标志,为1时表示Image Descriptor后面跟的是下幅图像所用的颜色表,此时Global Color Table无效。
- Interlace Flag(1b):交错显示标志,为1时表示图像数据是隔行方式存放的。最初GIF标准设置此标志的目的是考虑到通信设备间传输速度不理想情况下,用这种方式存放和显示图像,就可以在图像显示完成之前看到这幅图想的概貌,慢慢的变清晰,而不觉得显示时间过长。具体扫描行顺序参见GIF89a标准[1]。作为单机显示系统来讲,可以采用逐次行显示方式,也可以采取逐次全屏显示方式(此时将看不到交错效果),依据显示系统的刷新显示性能,解码程序设计者可以选择权衡的。
- Sort Flag(1b):表示重要颜色排序标志,标志位1时,表示颜色表中重要的颜色排在前面,有利于颜色数较少的解码器选择最好的颜色。一般该标志为0,不做处理。
- Reserved(2b):保留。
- Local Color Table Size(3b)局部颜色表大小,计算方式与Global Color Table种的方法一致。
3)图像数据存储方式
压缩后的数据组织结构
GIF文件格式所采用的是LZW图像压缩算法,它是一种基于 Striing Table 的字典压缩算法。
关于LZW算法,请看我接下来的一篇博客,下面我们先了解压缩后的数据组织结构:
- LZW Code Size(1B): 表示一个像素索引值所用的最少比特位数,如:该值为0x08是表示解码后的每个像素索引值为8位(一个字节,可以表示256种颜色)。
- Block: 压缩图像数据块,Block Size用一个unsigned char型数表示块的大小(单位为Byte),因此每块最大可为255Bytes。
- Block Data: 存放的是压缩后的图像数据。可以有若干个。
- Block Terminator(1B): 固定为0x00,表示该图像的结束。
帧数据扩展块
-
- 程序扩展结构(Application Extension)主要定义了生成该gif的程序相关信息
包含制作GIF文件的应用程序的相关信息。前8字节为应用程序标识符(程序名称),后3字节为应用程序识别码。
- 程序扩展结构(Application Extension)主要定义了生成该gif的程序相关信息
-
- 注释扩展结构(Comment Extension)一般用来储存图片作者的签名信息
说明图形,作者和其他任何非图形数据和控制信息的文本信息。
- 注释扩展结构(Comment Extension)一般用来储存图片作者的签名信息
-
- 图形控制扩展结构(Graphic Control Extension)这部分对图片的渲染比较重要
- Block Size:固定位0x04。
- Display Method:一般为1。
- User Input Flag:用户输入标志,为1时表示处理完该图像域后等待用户的输入后才开始下一图像域的处理。
- Delay Time:表示处理完该图像域到开始处理下一个图像域的延迟时间,单位是1/100秒。
- Transparent Color Flag:表示透明颜色索引标志,该标志置位表示透明颜色索引有效。
- Transparent Color Index:表示透明颜色索引,在透明颜色索引有效情况下,解码所得颜色索引与该索引值相等时,数据将不作处理。
- 注释:如果同时出现User Input Flag置位和Delay Time不为0的情况下,则在Delay Time定时到之前有用户输入或者没有用户输入而Delay Time定时到着两种情况下,都会出发下一图像域的处理。上面提到的透明区域的处理就是通过这一扩展块实现的。
- 图形控制扩展结构(Graphic Control Extension)这部分对图片的渲染比较重要
-
- 平滑文本扩展结构(Plain Text Control Extension)
89a标准允许我们将图片上的文字信息额外储存在扩展区域里,但实际渲染时依赖解码器的字体环境,所以实际情况中很少使用。
- 平滑文本扩展结构(Plain Text Control Extension)
以上扩展块都是可选的,只有Label置位的情况下,解码器才会去渲染。
2. GIF压缩
以下安装教程均是在Window下的。压缩主要是使用Imagemagick工具,Imagemagick的压缩就是前面介绍的通过透明区域排除的方式进行压缩,后面的代码主要是通过抽帧的方式进行进一步压缩。后面的代码是使用Nodejs编写的,没有Nodejs的小伙伴可以参考Nodejs安装教程,进行安装。
1) 环境配置
a. 安装Imagemagick
- 首先先安装imagemagick,Imagemagick下载地址
- Nodejs 上安装 imagemagick
npm install imagemagick --save
b. 安装 gm
npm install --save gm
c. 安装 gifsicle
npm install --save gifsicle
安装好后,如果下面的程序因为gifsicle报错,多半是因为没有将gifsicle所在文件夹中的gifsicle.exe所在位置添加进环境变量path中。
gifsicle文件夹的位置取决于你nodejs的安装位置正如我的是在:
2)压缩过程
- 使用Imagemagick工具
magick source.gif -layers Optimize dest.gif
这条命令简单来说就是我们介绍GIF压缩原理中进行透明区域叠加进行压缩GIF,所以说帧与帧之间差别较小得到帧能得到较好的压缩效果,但是对于帧之间差别较大的帧这条命令执行后,Gif甚至比原来更大。但是对于有些Gif是视频转化过来的,视频录制的时候难免会发生抖动,背景会有些许变化,针对这一问题,我们可以通过设置fuzz因子来设置,所以可以通过下面着一条命令来来压缩 magick test.gif -fuzz 15% -layers Optimize result.gif。 - 如果上诉方法还不能满足你的需求,那么只能通过抽帧的方式,或者resize大法来进行进一步压缩,下面贴出来了,使用Nodejs编写的抽帧的方式进行压缩的可执行代码。
3)Gif抽帧的可执行代码
const {spawn} = require('child_process');
const gm = require('gm');
const file = "gif文件的所在位置";
function countGap(frame){
let gap;
if(frame<=8) gap = 1;
else if(9 <= frame && frame <= 20) gap = 2;
else if(21 <= frame && frame <= 30) gap = 3;
else if(31 <= frame && frame <= 40) gap = 4;
else gap = 5;
return gap;
};
const image = gm(file)
image.identify((err, val) => {
if(!val.Scene){
console.log(file+" has err:"+err)
return
}
let frames_count = val.Scene[0].replace(/\d* of /, '') * 1
let gap = countGap(frames_count)
let delayList = [];
let totaldelay = 0
if(val.Delay!=undefined){
let i
for (i = 0; i < val.Delay.length; i ++) {
delayList[i] = val.Delay[i].replace(/x\d*/, '') * 1
totaldelay+=delayList[i]
}
for (; i < val.Scene.length; i ++) {
delayList[i] = 8
totaldelay+=delayList[i]
}
}else{
for (let i = 0; i < val.Scene.length; i ++) {
delayList[i] = 8
totaldelay+=delayList[i]
}
}
let totalFrame = parseInt(frames_count/gap)
//判断是否速度过慢,需要进行归一加速处理
if(totaldelay/totalFrame>20){
let scale =(totalFrame*1.0*20)/totaldelay
for (let i = 0; i < delayList.length; i ++) {
delayList[i] = parseInt(delayList[i] * scale)
}
}
let params=[]
params.push("--colors=255")
params.push("--unoptimize")
params.push("src2/"+file)
let tempdelay = delayList[0]
for (let i = 1; i < frames_count; i ++) {
if(i%gap==0){
params.push("-d"+tempdelay)
params.push("#"+(i-gap))
tempdelay=0
}
tempdelay += delayList[i]
}
params.push("--optimize=3")
params.push("-o")
params.push("src2/"+file+"gap-keepdelay.gif")
spawn("gifsicle", params, { stdio: 'inherit' })
})