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)); } } }