GIF图像是基于颜色列表的(存储的数据是该点的颜色对应于颜色列表的索引值),最多只支持8位(256色)。GIF文件内部分成许多存储块,用来存储多幅图像或者是决定图像表现行为的控制块,用以实现动画和交互式应用。GIF文件还通过LZW压缩算法压缩图像数据来减少图像尺寸。
下面,我们直接通过代码来解析GIF标准。
1、GIF署名(Signature)和版本号(Version)
GIF署名用来确认一个文件是否是GIF格式的文件, 这一部分由三个字符组成:"GIF";文件版本号 也是由三个字节组成,可以为"87a"或"89a"
/*###################GIF署名(Signature)和版本号(Version)##################*/
/*GIF署名用来确认一个文件是否是GIF格式的文件,
* 这一部分由三个字符组成:"GIF";文件版本号
* 也是由三个字节组成,可以为"87a"或"89a"*/
protected void writeSignatureVersion() throws IOException
{
writeString("GIF89a");
}
2、逻辑屏幕标识符(Logical Screen Descriptor)
这一部分由7个字节组成,定义了GIF图像的大小(Logical Screen Width & Height)、 颜色深度(Color Bits)、背景色(Blackground Color Index)以及有无全局颜色列 表(Global Color Table)和颜色列表的索引数(Index Count)
/*##################逻辑屏幕标识符########################*/
/*这一部分由7个字节组成,定义了GIF图像的大小(Logical Screen Width & Height)、
* 颜色深度(Color Bits)、背景色(Blackground Color Index)以及有无全局颜色列
* 表(Global Color Table)和颜色列表的索引数(Index Count)*/
protected void writeLogicalScreenDescriptor() throws IOException {
writeShort(width); //逻辑屏幕宽度,像素数,定义GIF图像的宽度
writeShort(height); //逻辑屏幕高度,像素数,定义GIF图像的宽度
/*全局颜色列表标志(Global Color Table Flag),
* 当置位时表示有全局颜色列表,pixel值有意义。*/
out.write((0x80 |
0x70 | // cr - 颜色深度(Color ResoluTion),cr+1确定图像的颜色深度
0x00 | // s - 分类标志(Sort Flag),如果置位表示全局颜色列表分类排列
palSize)); // 全局颜色列表大小,pixel+1确定颜色列表的索引数(2的pixel+1次方
out.write(0); // 背景颜色(在全局颜色列表中的索引,如果没有全局颜色列表,该值没有意义)
out.write(0); // 像素宽高比(Pixel Aspect Radio)
}
3、全局颜色列表(Global Color Table)
全局颜色列表必须紧跟在逻辑屏幕标识符后面,每个颜色列表索引条目由三个字节组成,按R、G、B的顺序排列
/*########################全局颜色列表(Global Color Table)#######################*/
/*全局颜色列表必须紧跟在逻辑屏幕标识符后面,每个颜色
* 列表索引条目由三个字节组成,按R、G、B的顺序排*/
protected void writeGlobalColorTable() throws IOException {
out.write(colorTab, 0, colorTab.length);
int n = (3 * 256) - colorTab.length;
for (int i = 0; i < n; i++) {
out.write(0);
}
}
4、图像标识符(Image Descriptor)
一个GIF文件内可以包含多幅图像,一幅图像结束之后紧接着下是一幅图像的标识符,图像标识符以0x2C(",")字符开始,定义紧接着它的图像的性质,包括图像相对于逻辑屏幕边界的偏移量、图像大小以及有无局部颜色列表和颜色列表大小,由10个字节组成
/*###################图像标识符(Image Descriptor)#######################*/
/*一个GIF文件内可以包含多幅图像,一幅图像结束之后紧接着下是一幅图像的标识符,
* 图像标识符以0x2C(",")字符开始,定义紧接着它的图像的性质,包括图像相对于
* 逻辑屏幕边界的偏移量、图像大小以及有无局部颜色列表和颜色列表大小,由10个
* 字节组成*/
protected void writeImageDescriptor() throws IOException {
out.write(0x2c); // 图像标识符开始,固定值为0x2c
writeShort(0); // X方向偏移量,必须限定在逻辑屏幕尺寸范围内
writeShort(0); // Y方向偏移量,必须限定在逻辑屏幕尺寸范围内
writeShort(width); // 图像宽度
writeShort(height); // 图像高度
if (firstFrame) {
// 第一帧不需要局部颜色列表标志
out.write(0);
} else {
writeLocalColorTableFlag(); //局部颜色列表标志
}
}
5、局部颜色列表标志(Local Color Table Flag)
/*###################局部颜色列表标志(Local Color Table Flag)#######################*/
protected void writeLocalColorTableFlag() throws IOException {
out.write(0x80 | // 置位时标识紧接在图像标识符之后有一个局部颜色列表,供紧跟在它之后的一幅图像使用;值为0时使用全局颜色列表,忽略pixel值。
0 | // 交织标志(Interlace Flag),置位时图像数据使用交织方式排列,否则使用顺序排
0 | // 分类标志(Sort Flag),如果置位表示紧跟着的局部颜色列表分类排列
0 | // 保留,必须初始化为0
palSize); // 局部颜色列表大小(Size of Local Color Table),pixel+1就为颜色列表的位数
}
6、图形控制扩展(Graphic Control Extension)
这一部分是可选的(需要89a版本),可以放在一个图像块(图像标识符)或文本扩展块的前面,用来控制跟在它后面的第一个图像(或文本)的渲染(Render)形式
/*###################图形控制扩展(Graphic Control Extension)##################*/
/*这一部分是可选的(需要89a版本),可以放在一个图像块(图像标识符)或文本扩展块的前面,
* 用来控制跟在它后面的第一个图像(或文本)的渲染(Render)形式*/
protected void writeGraphicControlExtension() throws IOException {
out.write(0x21); // 标识这是一个扩展块,固定值0x21
out.write(0xf9); // 标识这是一个图形控制扩展块,固定值0xF9
out.write(4); // 块大小 - 不包括块终结器,固定值4
int transp, disp;
if (transparent == 0) {
transp = 0;
disp = 0; // dispose = no action
} else {
transp = 1;
disp = 2; // force clear if using transparent color
}
if (dispose >= 0) {
disp = dispose & 7; // user override
}
disp <<= 2;
// packed fields
out.write(0 | // 1:3 reserved
disp | // 处置方法(Disposal Method):指出处置图形的方法,当值为: 0 - 不使用处置方法; 1 - 不处置图形,把图形从当前位置移去;2 - 回复到背景色; 3 - 回复到先前状态; 4-7 - 自定义
0 | // 用户输入标志(Use Input Flag):指出是否期待用户有输入之后才继续进行下去,置位表示期待,值否表示不期待。用户输入可以是按回车键、鼠标点击等,可以和延迟时间一起使用,在设置的延迟时间内用户有输入则马上继续进行,或者没有输入直到延迟时间到达而继续
transp); // 透明颜色标志(Transparent Color Flag):置位表示使用透明颜色
writeShort(delay); // Delay Time - 单位1/100秒,如果值不为1,表示暂停规定的时间后再继续往下处理数据流
out.write(transIndex); // 透明色索引值
out.write(0); // 标识块终结,固定值0
}
7、应用程序扩展(Application Extension)
这是提供给应用程序自己使用的(需要89a版本),应用程序可以在这里定义自己的标识、信息等
/*########################应用程序扩展(Application Extension)###########################*/
/*这是提供给应用程序自己使用的(需要89a版本),应用程序可以在这里定义自己的标识、信息等*/
protected void writeApplicationExtension() throws IOException {
out.write(0x21); // 标识这是一个扩展块,固定值0x21
out.write(0xff); // 标识这是一个应用程序扩展块,固定值0xFF
out.write(11); // 块大小,固定值11
writeString("ELONGGIF"); // Application Identifier - 用来鉴别应用程序自身的标识(8个连续ASCII字符)
writeString("1.0"); //Application Authentication Code - 应用程序定义的特殊标识码(3个连续ASCII字符)
/*应用程序自定义数据块 - 一个或多个数据块(Data Sub-Blocks)组成,保存应用程序自己定义的数据*/
out.write(3); // sub-block size
out.write(1); // loop sub-block id
writeShort(0); // loop count (extra iterations, 0=repeat forever)
out.write(0); // 标识注释块结束,固定值0
}
8、文件终结器(Trailer)
这一部分只有一个值为0的字节,标识一个GIF文件结束
/*##############文件终结器(Trailer)###############*/
/*这一部分只有一个值为0的字节,标识一个GIF文件结束*/
protected void writeTrailer() throws IOException {
out.write(0x3b); // 标识GIF文件结束,固定值0x3B
}
最后贴出完整的代码:
public class GifEncoder {
protected boolean closeStream;
protected int colorDepth;
protected byte[] colorTab;
protected int delay = 0;
protected int dispose;
protected boolean firstFrame;
protected int height;
protected Bitmap image;
protected byte[] indexedPixels;
protected OutputStream out;
protected int palSize;
protected byte[] pixels;
protected int repeat = -1;
protected int sample;
protected boolean sizeSet;
protected boolean started;
protected int transIndex;
protected int transparent = 0;
protected boolean[] usedEntry;
protected int width;
public GifEncoder() {
boolean[] arrayOfBoolean = new boolean[256];
this.usedEntry = arrayOfBoolean;
this.palSize = 7;
this.dispose = -1;
this.closeStream = false;
this.firstFrame = true;
this.sizeSet = false;
this.sample = 10;
}
public boolean addFrame(Bitmap paramBitmap, int index) {
boolean isOk = true;
if (paramBitmap == null || !started) {
return false;
}
try {
this.image = paramBitmap;
if (!sizeSet) {
setSize();
}
long time01 = System.currentTimeMillis();
getImagePixels();
TimeUtil.logTime(time01, "getImagePixels");
long time02 = System.currentTimeMillis();
analyzePixels();
TimeUtil.logTime(time02, "analyzePixels");
if (firstFrame) {
writeLogicalScreenDescriptor(); //写逻辑屏幕标识符
writeGlobalColorTable(); //全局颜色列表(Global Color Table)
if (repeat >= 0)
writeApplicationExtension(); //应用程序扩展(Application Extension)
}
writeGraphicControlExtension(); //图形控制扩展(Graphic Control Extension)
writeImageDescriptor(); //图像标识符(Image Descriptor)
if (!firstFrame)
writeGlobalColorTable(); //全局颜色列表(Global Color Table)
writePixels();
this.firstFrame = false;
} catch (IOException localIOException1) {
isOk = false;
}
return isOk;
}
/*分析像素信息*/
protected void analyzePixels() {
int len = this.pixels.length;
int nPix = len / 3;
byte[] arrayOfByte1 = new byte[nPix];
this.indexedPixels = arrayOfByte1;
byte[] arrayOfByte2 = this.pixels;
int k = this.sample;
NeuQuant nq = new NeuQuant(arrayOfByte2, len, k);
this.colorTab = nq.process();
int l = 0;
int i1 = this.colorTab.length;
if (l >= i1) {
l = 0;
}
for (int i = 0; i < colorTab.length; i += 3) {
byte temp = colorTab[i];
colorTab[i] = colorTab[i + 2];
colorTab[i + 2] = temp;
usedEntry[i / 3] = false;
}
int k1 = 0;
for (int i = 0; i < nPix; i++) {
int index = nq.map(pixels[k1++] & 0xff, pixels[k1++] & 0xff, pixels[k1++] & 0xff);
usedEntry[index] = true;
indexedPixels[i] = (byte) index;
}
pixels = null;
colorDepth = 8;
palSize = 7;
if (transparent != 0) {
transIndex = findClosest(transparent);
}
}
protected int findClosest(int paramInt) {
if (colorTab == null) {
return -1;
}
int r = Color.red(paramInt);
int g = Color.green(paramInt);
int b = Color.blue(paramInt);
int minpos = 0;
int dmin = 256 * 256 * 256;
int len = colorTab.length;
for (int i = 0; i < len;) {
int dr = r - (colorTab[i++] & 0xff);
int dg = g - (colorTab[i++] & 0xff);
int db = b - (colorTab[i] & 0xff);
int d = dr * dr + dg * dg + db * db;
int index = i / 3;
if (usedEntry[index] && (d < dmin)) {
dmin = d;
minpos = index;
}
i++;
}
return minpos;
}
public boolean finish() {
if (!started)
return false;
boolean isOk = true;
started = false;
try {
writeTrailer(); //文件终结器(Trailer)
out.flush(); //将缓冲区清除,将数据写入到基础设备
if (closeStream) {
out.close();
}
} catch (IOException e) {
isOk = false;
}
// reset for subsequent use
transIndex = 0;
out = null;
image = null;
pixels = null;
indexedPixels = null;
colorTab = null;
closeStream = false;
firstFrame = true;
return isOk;
}
protected void getImagePixels() {
int w = this.image.getWidth();
int h = this.image.getHeight();
this.pixels = new byte[w * h * 3];
int[] arrayOfInt = new int[w * h];
this.image.getPixels(arrayOfInt, 0, w, 0, 0, w, h);
for(int i = 0; i < arrayOfInt.length; i++)
{
pixels[i * 3] = (byte) Color.blue(arrayOfInt[i]);
pixels[i * 3 + 1] = (byte) Color.green(arrayOfInt[i]);
pixels[i * 3 + 2] = (byte) Color.red(arrayOfInt[i]);
}
}
public void setDelay(int ms) {
delay = Math.round(ms / 10.0f);
}
public void setDispose(int code) {
if (code >= 0) {
dispose = code;
}
}
public void setFrameRate(float fps) {
if (fps != 0f) {
delay = Math.round(100f / fps);
}
}
public void setQuality(int quality) {
if (quality < 1)
quality = 1;
sample = quality;
}
public void setRepeat(int iter) {
if (iter >= 0) {
repeat = iter;
}
}
public void setSize() {
if (started && !firstFrame)
return;
width = this.image.getWidth();
height = this.image.getHeight();
if (width < 1)
width = 160;
if (height < 1)
height = 120;
sizeSet = true;
}
public void setTransparent(int c) {
this.transparent = c;
}
public boolean start(OutputStream os) {
if (os == null)
return false;
boolean isOk = true;
closeStream = false;
out = os;
try {
writeSignatureVersion(); // GIF署名(Signature)和版本号(Version)
} catch (IOException e) {
isOk = false;
}
return started = isOk;
}
public boolean start(String file) {
boolean isOk = true;
try {
out = new BufferedOutputStream(new FileOutputStream(file));
isOk = start(out);
closeStream = true;
} catch (IOException e) {
isOk = false;
}
return started = isOk;
}
/*###################GIF署名(Signature)和版本号(Version)##################*/
/*GIF署名用来确认一个文件是否是GIF格式的文件,
* 这一部分由三个字符组成:"GIF";文件版本号
* 也是由三个字节组成,可以为"87a"或"89a"*/
protected void writeSignatureVersion() throws IOException
{
writeString("GIF89a");
}
/*##################逻辑屏幕标识符########################*/
/*这一部分由7个字节组成,定义了GIF图像的大小(Logical Screen Width & Height)、
* 颜色深度(Color Bits)、背景色(Blackground Color Index)以及有无全局颜色列
* 表(Global Color Table)和颜色列表的索引数(Index Count)*/
protected void writeLogicalScreenDescriptor() throws IOException {
writeShort(width); //逻辑屏幕宽度,像素数,定义GIF图像的宽度
writeShort(height); //逻辑屏幕高度,像素数,定义GIF图像的宽度
/*全局颜色列表标志(Global Color Table Flag),
* 当置位时表示有全局颜色列表,pixel值有意义。*/
out.write((0x80 |
0x70 | // cr - 颜色深度(Color ResoluTion),cr+1确定图像的颜色深度
0x00 | // s - 分类标志(Sort Flag),如果置位表示全局颜色列表分类排列
palSize)); // 全局颜色列表大小,pixel+1确定颜色列表的索引数(2的pixel+1次方
out.write(0); // 背景颜色(在全局颜色列表中的索引,如果没有全局颜色列表,该值没有意义)
out.write(0); // 像素宽高比(Pixel Aspect Radio)
}
/*########################全局颜色列表(Global Color Table)#######################*/
/*全局颜色列表必须紧跟在逻辑屏幕标识符后面,每个颜色
* 列表索引条目由三个字节组成,按R、G、B的顺序排*/
protected void writeGlobalColorTable() throws IOException {
out.write(colorTab, 0, colorTab.length);
int n = (3 * 256) - colorTab.length;
for (int i = 0; i < n; i++) {
out.write(0);
}
}
/*###################图像标识符(Image Descriptor)#######################*/
/*一个GIF文件内可以包含多幅图像,一幅图像结束之后紧接着下是一幅图像的标识符,
* 图像标识符以0x2C(",")字符开始,定义紧接着它的图像的性质,包括图像相对于
* 逻辑屏幕边界的偏移量、图像大小以及有无局部颜色列表和颜色列表大小,由10个
* 字节组成*/
protected void writeImageDescriptor() throws IOException {
out.write(0x2c); // 图像标识符开始,固定值为0x2c
writeShort(0); // X方向偏移量,必须限定在逻辑屏幕尺寸范围内
writeShort(0); // Y方向偏移量,必须限定在逻辑屏幕尺寸范围内
writeShort(width); // 图像宽度
writeShort(height); // 图像高度
if (firstFrame) {
// 第一帧不需要局部颜色列表标志
out.write(0);
} else {
writeLocalColorTableFlag(); //局部颜色列表标志
}
}
/*###################局部颜色列表标志(Local Color Table Flag)#######################*/
protected void writeLocalColorTableFlag() throws IOException {
out.write(0x80 | // 置位时标识紧接在图像标识符之后有一个局部颜色列表,供紧跟在它之后的一幅图像使用;值为0时使用全局颜色列表,忽略pixel值。
0 | // 交织标志(Interlace Flag),置位时图像数据使用交织方式排列,否则使用顺序排
0 | // 分类标志(Sort Flag),如果置位表示紧跟着的局部颜色列表分类排列
0 | // 保留,必须初始化为0
palSize); // 局部颜色列表大小(Size of Local Color Table),pixel+1就为颜色列表的位数
}
/*###################图形控制扩展(Graphic Control Extension)##################*/
/*这一部分是可选的(需要89a版本),可以放在一个图像块(图像标识符)或文本扩展块的前面,
* 用来控制跟在它后面的第一个图像(或文本)的渲染(Render)形式*/
protected void writeGraphicControlExtension() throws IOException {
out.write(0x21); // 标识这是一个扩展块,固定值0x21
out.write(0xf9); // 标识这是一个图形控制扩展块,固定值0xF9
out.write(4); // 块大小 - 不包括块终结器,固定值4
int transp, disp;
if (transparent == 0) {
transp = 0;
disp = 0; // dispose = no action
} else {
transp = 1;
disp = 2; // force clear if using transparent color
}
if (dispose >= 0) {
disp = dispose & 7; // user override
}
disp <<= 2;
// packed fields
out.write(0 | // 1:3 reserved
disp | // 处置方法(Disposal Method):指出处置图形的方法,当值为: 0 - 不使用处置方法; 1 - 不处置图形,把图形从当前位置移去;2 - 回复到背景色; 3 - 回复到先前状态; 4-7 - 自定义
0 | // 用户输入标志(Use Input Flag):指出是否期待用户有输入之后才继续进行下去,置位表示期待,值否表示不期待。用户输入可以是按回车键、鼠标点击等,可以和延迟时间一起使用,在设置的延迟时间内用户有输入则马上继续进行,或者没有输入直到延迟时间到达而继续
transp); // 透明颜色标志(Transparent Color Flag):置位表示使用透明颜色
writeShort(delay); // Delay Time - 单位1/100秒,如果值不为1,表示暂停规定的时间后再继续往下处理数据流
out.write(transIndex); // 透明色索引值
out.write(0); // 标识块终结,固定值0
}
/*########################应用程序扩展(Application Extension)###########################*/
/*这是提供给应用程序自己使用的(需要89a版本),应用程序可以在这里定义自己的标识、信息等*/
protected void writeApplicationExtension() throws IOException {
out.write(0x21); // 标识这是一个扩展块,固定值0x21
out.write(0xff); // 标识这是一个应用程序扩展块,固定值0xFF
out.write(11); // 块大小,固定值11
writeString("ELONGGIF"); // Application Identifier - 用来鉴别应用程序自身的标识(8个连续ASCII字符)
writeString("1.0"); //Application Authentication Code - 应用程序定义的特殊标识码(3个连续ASCII字符)
/*应用程序自定义数据块 - 一个或多个数据块(Data Sub-Blocks)组成,保存应用程序自己定义的数据*/
out.write(3); // sub-block size
out.write(1); // loop sub-block id
writeShort(0); // loop count (extra iterations, 0=repeat forever)
out.write(0); // 标识注释块结束,固定值0
}
/*##############文件终结器(Trailer)###############*/
/*这一部分只有一个值为0的字节,标识一个GIF文件结束*/
protected void writeTrailer() throws IOException {
out.write(0x3b); // 标识GIF文件结束,固定值0x3B
}
/*################图像数据######################*/
protected void writePixels() throws IOException {
LZWEncoder encoder = new LZWEncoder(width, height, indexedPixels, colorDepth);
encoder.encode(out);
}
protected void writeShort(int value) throws IOException {
out.write(value & 0xff);
out.write((value >> 8) & 0xff);
}
protected void writeString(String s) throws IOException {
for (int i = 0; i < s.length(); i++) {
out.write((byte) s.charAt(i));
}
}
}