Android原始碼閱讀——GIF解碼(如何提取各幀圖片)

Anlia發表於2017-12-14

版權宣告:本文為博主原創文章,未經博主允許不得轉載

系列部落格:原始碼閱讀系列

原始碼:GifDecoder

大家要是看到有錯誤的地方或者有啥好的建議,歡迎留言評論

前言:閱讀優秀的原始碼可以大大提高我們的開發水平,遂開個新坑 記錄優秀原始碼(Android原始碼、各種開源庫等等)的分析和解讀,學習別人是怎樣實現某個功能的。本期我們的主角是 GIF的解碼,我們將從GIF解碼的原始碼 GifDecoder入手,分析其實現的原理和過程,希望能幫到大家~( GifDecoder原始碼(博主已對原始碼裡面各方法及引數進行了註釋,請放心食用 ~)連結已在上方貼出來了,該原始碼參考了Glide開源庫解析GIF部分的程式碼,但由於是很久之前看到的,具體出處已無從考證,有知道的小夥伴可以留言告訴我)

目錄

  • GIF結構簡述
  • GifDecoder的初始化
  • 判斷傳入檔案格式
  • 讀取GIF大小、顏色深度等全域性屬性
  • 提取各幀圖片

GIF結構簡述

相關博文連結

gif 格式圖片詳細解析

在分析原始碼之前,我們得先對GIF圖片的構成有一個初步的瞭解(詳細解析請看上方連結),見下圖

Android原始碼閱讀——GIF解碼(如何提取各幀圖片)

圖中加粗部分既是儲存我們所需要提取圖片的地方(一幀影象對應一個影象塊)。雖然我們知道了儲存每一幀影象資訊的位置,但我們不能直接從中取出圖片,因為在計算機中,所有的檔案都是以二進位制的形式儲存的,而Java讀取檔案需要按順序一個一個位元組地讀。因此GIF的解碼過程,實際上就是從檔案頭(File Header)開始,按順序遍歷每一個位元組,當讀到我們需要的資訊(影象資料)時,就將其提取出來。下面我們就開始分析GifDecoder是如何實現GIF解碼的


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是儲存各幀的物件,具體實現和內部屬性如下

/**
 * 各幀物件
 */
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就基本分析完了,如果有講解不到位的地方歡迎大家留言指正。如果大家看了感覺還不錯麻煩點個贊,你們的支援是我最大的動力~

相關文章