Android源码阅读——GIF解码(如何提取各帧图片)

版权声明:本文为博主原创文章,未经博主容许不得转载java

系列博客:源码阅读系列android

源码:GifDecodergit

你们要是看到有错误的地方或者有啥好的建议,欢迎留言评论github

前言:阅读优秀的源码能够大大提升咱们的开发水平,遂开个新坑 记录优秀源码(Android源代码、各类开源库等等)的分析和解读,学习别人是怎样实现某个功能的。本期咱们的主角是 GIF的解码,咱们将从GIF解码的源码 GifDecoder入手,分析其实现的原理和过程,但愿能帮到你们~( GifDecoder源码(博主已对源码里面各方法及参数进行了注释,请放心食用 ~)连接已在上方贴出来了,该源码参考了Glide开源库解析GIF部分的代码,但因为是好久以前看到的,具体出处已无从考证,有知道的小伙伴能够留言告诉我)数组

目录

  • GIF结构简述
  • GifDecoder的初始化
  • 判断传入文件格式
  • 读取GIF大小、颜色深度等全局属性
  • 提取各帧图片

GIF结构简述

相关博文连接

gif 格式图片详细解析app

在分析源码以前,咱们得先对GIF图片的构成有一个初步的了解(详细解析请看上方连接),见下图ide

图中加粗部分既是保存咱们所须要提取图片的地方(一帧图像对应一个图像块)。虽然咱们知道了存储每一帧图像信息的位置,但咱们不能直接从中取出图片,由于在计算机中,全部的文件都是以二进制的形式存储的,而Java读取文件须要按顺序一个一个字节地读。所以GIF的解码过程,实际上就是从文件头(File Header)开始,按顺序遍历每个字节,当读到咱们须要的信息(图像数据)时,就将其提取出来。下面咱们就开始分析GifDecoder是如何实现GIF解码的post


GifDecoder的初始化

先来看看GifDecoder的初始化和使用示例,代码以下学习

try {
	InputStream is = getContentResolver().openInputStream(uri);
	GifDecoder gifDecoder = new GifDecoder();
	int code = gifDecoder.read(is);
	
	if (code == GifDecoder.STATUS_OK) {//解码成功
		GifDecoder.GifFrame[] frameList = gifDecoder.getFrames();
		
	} else if (code == gifDecoder.STATUS_FORMAT_ERROR) {//图片格式不是GIF

	} else {//图片读取失败

	}
}catch (FileNotFoundException e){
	e.printStackTrace();
}
复制代码

其中参数uri为GIF图片的Uri路径frameList为解码的结果,即GIF图片中各帧的集合,里面包括各帧静态图Bitmap延迟时间GifFrame是保存各帧的对象,具体实现和内部属性以下spa

/** * 各帧对象 */
public static class GifFrame {
	public Bitmap image;//静态图Bitmap
	public int delay;//图像延迟时间

	public GifFrame(Bitmap im, int del) {
		image = im;
		delay = del;
	}
}
复制代码

GifDecoder定义了三种解码状态

public static final int STATUS_OK = 0;//解码成功
public static final int STATUS_FORMAT_ERROR = 1;//图片格式错误
public static final int STATUS_OPEN_ERROR = 2;//打开图片失败
复制代码

GifDecoder的使用示例中,咱们能够看到GifDecoder解码GIF图片的入口为read(InputStream is)方法,具体实现以下

protected int status;//解码状态
protected Vector<GifFrame> frames;//存放各帧对象的数组
protected int frameCount;//帧数
protected int[] gct; //全局颜色列表
protected int[] lct; //局部颜色列表

/** * 解码入口,读取GIF图片输入流 * @param is * @return */
public int read(InputStream is) {
	init();
	if (is != null) {
		in = is;
		readHeader();
		if (!err()) {
			readContents();
			if (frameCount < 0) {
				status = STATUS_FORMAT_ERROR;
			}
		}
	} else {
		status = STATUS_OPEN_ERROR;
	}
	try {
		is.close();
	} catch (Exception e) {
		e.printStackTrace();
	}
	return status;
}

/** * 初始化参数 */
protected void init() {
	status = STATUS_OK;
	frameCount = 0;
	frames = new Vector<GifFrame>();
	gct = null;
	lct = null;
}

/** * 判断当前解码过程是否出错 * @return */
protected boolean err() {
	return status != STATUS_OK;
}
复制代码

能够看到read(InputStream is)方法中体现了完整的解码流程以及状态判断,其调用的readHeader()readContents()即为具体的GIF内部数据读取方法。下一节咱们将深刻readHeader()方法看看GifDecoder是如何处理GIF文件头


判断传入文件格式

解码以前确定要先判断解码的对象是否为GIF图片,readHeader()中就实现了此判断过程,判断文件格式的代码部分以下

/** * 读取GIF 文件头、逻辑屏幕标识符、全局颜色列表 */
protected void readHeader() {
	//根据文件头判断是否GIF图片
	String id = "";
	for (int i = 0; i < 6; i++) {
		id += (char) read();
	}
	if (!id.toUpperCase().startsWith("GIF")) {
		status = STATUS_FORMAT_ERROR;
		return;
	}
	
	//解析GIF逻辑屏幕标识符和全局颜色列表
	...
}

/** * 按顺序一个一个读取输入流字节,失败则设置读取失败状态码 * @return */
protected int read() {
	int curByte = 0;
	try {
		curByte = in.read();
	} catch (Exception e) {
		status = STATUS_FORMAT_ERROR;
	}
	return curByte;
}
复制代码

怎么理解这段代码呢?前文咱们提到文件头(File Header)中包含了GIF的文件署名版本号,共占6个字节(见下图),其中前3个字节存放的是GIF的文件署名,即G、I、F三个字符,那么这段代码就很好理解了,就是根据读取出来的文件头字符串开头是否为G、I 、F来判断此文件格式符不符合要求

文件头(File Header)


读取GIF大小、颜色深度等全局属性

readHeader方法中还有一部分代码,以下

protected boolean gctFlag;//是否使用了全局颜色列表
protected int bgIndex; //背景颜色索引
protected int gctSize; //全局颜色列表大小
protected int bgColor; //背景颜色

protected void readHeader() {
	//根据文件头判断是否GIF图片
	...
	
	//读取GIF逻辑屏幕标识符
	readLSD();
	
	//读取全局颜色列表
	if (gctFlag && !err()) {
		gct = readColorTable(gctSize);
		bgColor = gct[bgIndex];//根据索引在全局颜色列表拿到背景颜色
	}
}
复制代码

其对应的正是GIF数据流(GIF Data Stream)的前两部分逻辑屏幕标识符(Logical Screen Descriptor)全局颜色列表(Global Color Table)的解析,也就是说readHeader()完成了读取GIF图像数据前全部全局属性配置信息的读取与解析。接下来咱们先看readLSD()方法是如何解析逻辑屏幕标识符(Logical Screen Descriptor)(见下图)的

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

protected int width;//完整的GIF图像宽度
protected int height;//完整的GIF图像高度
protected int pixelAspect; //像素宽高比(Pixel Aspect Radio)

/** * 读取逻辑屏幕标识符(Logical Screen Descriptor)与全局颜色列表(Global Color Table) */
protected void readLSD() {
	//获取GIF图像宽高
	width = readShort();
	height = readShort();

	/** * 解析全局颜色列表(Global Color Table)的配置信息 * 配置信息占一个字节,具体各Bit存放的数据以下 * 7 6 5 4 3 2 1 0 BIT * | m | cr | s | pixel | */
	int packed = read();
	gctFlag = (packed & 0x80) != 0;//判断是否有全局颜色列表(m,0x80在计算机内部表示为1000 0000)
	gctSize = 2 << (packed & 7);//读取全局颜色列表大小(pixel)

	//读取背景颜色索引和像素宽高比(Pixel Aspect Radio)
	bgIndex = read();
	pixelAspect = read();
}

/** * 读取两个字节的数据 * @return */
protected int readShort() {
	return read() | (read() << 8);
}
复制代码

根据readLSD()的读取结果,咱们知道了此GIF图像中是否含有全局颜色列表(Global Color Table)(见下图),若是有,就调用readColorTable(int ncolors)方法获取全局颜色列表

全局颜色列表(Global Color Table)

/** * 读取颜色列表 * @param ncolors 列表大小,即颜色数量 * @return */
protected int[] readColorTable(int ncolors) {
	int nbytes = 3 * ncolors;//一个颜色占3个字节(r g b 各占1字节),所以占用空间为 颜色数量*3 字节
	int[] tab = null;
	byte[] c = new byte[nbytes];
	int n = 0;
	try {
		n = in.read(c);
	} catch (Exception e) {
		e.printStackTrace();
	}
	if (n < nbytes) {
		status = STATUS_FORMAT_ERROR;
	} else {//开始解析颜色列表
		tab = new int[256];//设置最大尺寸避免边界检查
		int i = 0;
		int j = 0;
		while (i < ncolors) {
			int r = ((int) c[j++]) & 0xff;
			int g = ((int) c[j++]) & 0xff;
			int b = ((int) c[j++]) & 0xff;
			tab[i++] = 0xff000000 | (r << 16) | (g << 8) | b;
		}
	}
	return tab;
}
复制代码

至此readHeader方法咱们就分析完了,接下来分析readContents方法是如何提取GIF图像的各帧图片


提取各帧图片

咱们先直接观察readContents方法内部是如何运做的

/** * 读取图像块内容 */
protected void readContents() {
	boolean done = false;
	while (!(done || err())) {
		int code = read();
		switch (code) {
			//图象标识符(Image Descriptor)开始
			case 0x2C:
				readImage();
				break;
			//扩展块开始
			case 0x21: //扩展块标识,固定值0x21
				code = read();
				switch (code) {
					case 0xf9: //图形控制扩展块标识(Graphic Control Label),固定值0xf9
						readGraphicControlExt();
						break;

					case 0xff: //应用程序扩展块标识(Application Extension Label),固定值0xFF
						readBlock();
						String app = "";
						for (int i = 0; i < 11; i++) {
							app += (char) block[i];
						}
						if (app.equals("NETSCAPE2.0")) {
							readNetscapeExt();
						} else {
							skip(); // don't care
						}
						break;
					default: //其余扩展都选择跳过
						skip();
				}
				break;

			case 0x3b://标识GIF文件结束,固定值0x3B
				done = true;
				break;

			case 0x00: //可能会出现的坏字节,可根据须要在此处编写坏字节分析等相关内容
				break;
			default:
				status = STATUS_FORMAT_ERROR;
		}
	}
}
复制代码

readContents()的核心流程就是根据块的标识来判断当前解码的位置,调用相应的方法对数据块进行解码。若是GIF版本为89a,则数据块中可能含有扩展块(可选)。其中图像延迟时间存放在图形控制扩展(Graphic Control Extension)中,所以咱们重点分析如何读取图形控制扩展(Graphic Control Extension)(见下图),其余扩展块解码你们能够对照着代码注释和GIF结构的相关知识自行研究,这里就很少赘述了

图形控制扩展(Graphic Control Extension)

解码图形控制扩展(Graphic Control Extension)的方法为readGraphicControlExt(),有了上图对各字节的说明其代码也就很容易理解了,以下

/** * 读取图形控制扩展块 */
protected void readGraphicControlExt() {
	read();//按读取顺序,此处为块大小

	int packed = read();//读取处置方法、用户输入标志等
	dispose = (packed & 0x1c) >> 2; //从packed中解析出处置方法(Disposal Method)
	if (dispose == 0) {
		dispose = 1; //elect to keep old image if discretionary
	}
	transparency = (packed & 1) != 0;//从packed中解析出透明色标志

	delay = readShort() * 10;//读取延迟时间(毫秒)
	transIndex = read();//读取透明色索引
	read();//按读取顺序,此处为标识块终结(Block Terminator)
}
复制代码

GIF中可能含有多个图像块图像块包含图象标识符(Image Descriptor)(见下图)、局部颜色列表(Local Color Table)(根据局部颜色列表标志肯定是否存在)以及基于颜色列表的图象数据(Table-Based Image Data)

图象标识符(Image Descriptor)

readContents()方法中遍历了全部图像块,并调用readImage方法进行解码,代码及注释以下

protected boolean lctFlag;//局部颜色列表标志(Local Color Table Flag)
protected boolean interlace;//交织标志(Interlace Flag)
protected int lctSize;//局部颜色列表大小(Size of Local Color Table)

/** * 按顺序读取图像块数据: * 图象标识符(Image Descriptor) * 局部颜色列表(Local Color Table)(有的话) * 基于颜色列表的图象数据(Table-Based Image Data) */
protected void readImage() {
	/** * 开始读取图象标识符(Image Descriptor) */
	ix = readShort();//x方向偏移量
	iy = readShort();//y方向偏移量
	iw = readShort();//图像宽度
	ih = readShort();//图像高度

	int packed = read();
	lctFlag = (packed & 0x80) != 0;//局部颜色列表标志(Local Color Table Flag)
	interlace = (packed & 0x40) != 0;//交织标志(Interlace Flag)
	// 3 - sort flag
	// 4-5 - reserved
	lctSize = 2 << (packed & 7);//局部颜色列表大小(Size of Local Color Table)

	/** * 开始读取局部颜色列表(Local Color Table) */
	if (lctFlag) {
		lct = readColorTable(lctSize);//解码局部颜色列表
		act = lct;//如有局部颜色列表,则图象数据是基于局部颜色列表的
	} else {
		act = gct; //不然都以全局颜色列表为准
		if (bgIndex == transIndex) {
			bgColor = 0;
		}
	}
	int save = 0;
	if (transparency) {
		save = act[transIndex];//保存透明色索引位置原来的颜色
		act[transIndex] = 0;//根据索引位置设置透明颜色
	}
	if (act == null) {
		status = STATUS_FORMAT_ERROR;//若没有颜色列表可用,则解码出错
	}
	if (err()) {
		return;
	}

	/** * 开始解码图像数据 */
	decodeImageData();
	skip();
	if (err()) {
		return;
	}
	frameCount++;
	image = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
	setPixels(); //将像素数据转换为图像Bitmap
	frames.addElement(new GifFrame(image, delay));//添加到帧图集合
	// list
	if (transparency) {
		act[transIndex] = save;//重置回原来的颜色
	}
	resetFrame();
}
复制代码

readImage方法中分三步进行:读取图象标识符(Image Descriptor)读取局部颜色列表(Local Color Table)解码图像数据。其中图像数据是如何解码并转换成Bitmap图像由于太复杂这里就不详细展开描述了,之后可能会专门写个番外篇进行分析,固然小伙伴们也能够自行阅读分析这部分源码:decodeImageData()setPixels()

至此 GifDecoder就基本分析完了,若是有讲解不到位的地方欢迎你们留言指正。若是你们看了感受还不错麻烦点个赞,大家的支持是我最大的动力~

相关文章
相关标签/搜索