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

版權(quán)聲明:本文為博主原創(chuàng)文章,未經(jīng)博主允許不得轉(zhuǎn)載。
系列博客:源碼閱讀系列
源碼:GifDecoder

大家要是看到有錯(cuò)誤的地方或者有啥好的建議,歡迎留言評(píng)論

前言:閱讀優(yōu)秀的源碼可以大大提高我們的開發(fā)水平,遂開個(gè)新坑 記錄優(yōu)秀源碼(Android源代碼、各種開源庫等等)的分析和解讀,學(xué)習(xí)別人是怎樣實(shí)現(xiàn)某個(gè)功能的。本期我們的主角是 GIF的解碼,我們將從GIF解碼的源碼 GifDecoder入手,分析其實(shí)現(xiàn)的原理和過程,希望能幫到大家~( GifDecoder源碼(博主已對(duì)源碼里面各方法及參數(shù)進(jìn)行了注釋,請(qǐng)放心食用 ~)鏈接已在上方貼出來了,該源碼參考了Glide開源庫解析GIF部分的代碼,但由于是很久之前看到的,具體出處已無從考證,有知道的小伙伴可以留言告訴我)

目錄
  • GIF結(jié)構(gòu)簡述
  • GifDecoder的初始化
  • 判斷傳入文件格式
  • 讀取GIF大小、顏色深度等全局屬性
  • 提取各幀圖片

GIF結(jié)構(gòu)簡述

相關(guān)博文鏈接

gif 格式圖片詳細(xì)解析

在分析源碼之前,我們得先對(duì)GIF圖片的構(gòu)成有一個(gè)初步的了解(詳細(xì)解析請(qǐng)看上方鏈接),見下圖

圖中加粗部分既是保存我們所需要提取圖片的地方(一幀圖像對(duì)應(yīng)一個(gè)圖像塊)。雖然我們知道了存儲(chǔ)每一幀圖像信息的位置,但我們不能直接從中取出圖片,因?yàn)樵谟?jì)算機(jī)中,所有的文件都是以二進(jìn)制的形式存儲(chǔ)的,而Java讀取文件需要按順序一個(gè)一個(gè)字節(jié)地讀。因此GIF的解碼過程,實(shí)際上就是從文件頭(File Header)開始,按順序遍歷每一個(gè)字節(jié),當(dāng)讀到我們需要的信息(圖像數(shù)據(jù))時(shí),就將其提取出來。下面我們就開始分析GifDecoder是如何實(shí)現(xiàn)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();
}

其中參數(shù)uri為GIF圖片的Uri路徑,frameList為解碼的結(jié)果,即GIF圖片中各幀的集合,里面包括各幀靜態(tài)圖Bitmap延遲時(shí)間。GifFrame是保存各幀的對(duì)象,具體實(shí)現(xiàn)和內(nèi)部屬性如下

/**
 * 各幀對(duì)象
 */
public static class GifFrame {
    public Bitmap image;//靜態(tài)圖Bitmap
    public int delay;//圖像延遲時(shí)間

    public GifFrame(Bitmap im, int del) {
        image = im;
        delay = del;
    }
}

GifDecoder定義了三種解碼狀態(tài)

public static final int STATUS_OK = 0;//解碼成功
public static final int STATUS_FORMAT_ERROR = 1;//圖片格式錯(cuò)誤
public static final int STATUS_OPEN_ERROR = 2;//打開圖片失敗

GifDecoder的使用示例中,我們可以看到GifDecoder解碼GIF圖片的入口為read(InputStream is)方法,具體實(shí)現(xiàn)如下

protected int status;//解碼狀態(tài)
protected Vector<GifFrame> frames;//存放各幀對(duì)象的數(shù)組
protected int frameCount;//幀數(shù)
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;
}

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

/**
 * 判斷當(dāng)前解碼過程是否出錯(cuò)
 * @return
 */
protected boolean err() {
    return status != STATUS_OK;
}

可以看到read(InputStream is)方法中體現(xiàn)了完整的解碼流程以及狀態(tài)判斷,其調(diào)用的readHeader()readContents()即為具體的GIF內(nèi)部數(shù)據(jù)讀取方法。下一節(jié)我們將深入readHeader()方法看看GifDecoder是如何處理GIF文件頭(File Header)


判斷傳入文件格式

解碼之前肯定要先判斷解碼的對(duì)象是否為GIF圖片,readHeader()中就實(shí)現(xiàn)了此判斷過程,判斷文件格式的代碼部分如下

/**
 * 讀取GIF 文件頭、邏輯屏幕標(biāo)識(shí)符、全局顏色列表
 */
protected void readHeader() {
    //根據(jù)文件頭判斷是否GIF圖片
    String id = "";
    for (int i = 0; i < 6; i++) {
        id += (char) read();
    }
    if (!id.toUpperCase().startsWith("GIF")) {
        status = STATUS_FORMAT_ERROR;
        return;
    }
    
    //解析GIF邏輯屏幕標(biāo)識(shí)符和全局顏色列表
    ...
}

/**
 * 按順序一個(gè)一個(gè)讀取輸入流字節(jié),失敗則設(shè)置讀取失敗狀態(tài)碼
 * @return
 */
protected int read() {
    int curByte = 0;
    try {
        curByte = in.read();
    } catch (Exception e) {
        status = STATUS_FORMAT_ERROR;
    }
    return curByte;
}

怎么理解這段代碼呢?前文我們提到文件頭(File Header)中包含了GIF的文件署名版本號(hào),共占6個(gè)字節(jié)(見下圖),其中前3個(gè)字節(jié)存放的是GIF的文件署名,即‘G’、‘I’、‘F’三個(gè)字符,那么這段代碼就很好理解了,就是根據(jù)讀取出來的文件頭字符串開頭是否為‘GIF’來判斷此文件格式符不符合要求

文件頭(File Header)

讀取GIF大小、顏色深度等全局屬性

readHeader()中還有一部分代碼,如下

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

protected void readHeader() {
    //根據(jù)文件頭判斷是否GIF圖片
    ...
    
    //讀取GIF邏輯屏幕標(biāo)識(shí)符
    readLSD();
    
    //讀取全局顏色列表
    if (gctFlag && !err()) {
        gct = readColorTable(gctSize);
        bgColor = gct[bgIndex];//根據(jù)索引在全局顏色列表拿到背景顏色
    }
}

其對(duì)應(yīng)的正是GIF數(shù)據(jù)流(GIF Data Stream)的前兩部分邏輯屏幕標(biāo)識(shí)符(Logical Screen Descriptor)全局顏色列表(Global Color Table)的解析,也就是說readHeader()完成了讀取GIF圖像數(shù)據(jù)前所有全局屬性配置信息的讀取與解析。接下來我們先看readLSD()方法是如何解析邏輯屏幕標(biāo)識(shí)符(Logical Screen Descriptor)(見下圖)的

邏輯屏幕標(biāo)識(shí)符(Logical Screen Descriptor)
protected int width;//完整的GIF圖像寬度
protected int height;//完整的GIF圖像高度
protected int pixelAspect; //像素寬高比(Pixel Aspect Radio)

/**
 * 讀取邏輯屏幕標(biāo)識(shí)符(Logical Screen Descriptor)與全局顏色列表(Global Color Table)
 */
protected void readLSD() {
    //獲取GIF圖像寬高
    width = readShort();
    height = readShort();

    /**
     * 解析全局顏色列表(Global Color Table)的配置信息
     * 配置信息占一個(gè)字節(jié),具體各Bit存放的數(shù)據(jù)如下
     *    7   6 5 4   3   2 1 0  BIT
     *  | m |   cr  | s | pixel |
     */
    int packed = read();
    gctFlag = (packed & 0x80) != 0;//判斷是否有全局顏色列表(m,0x80在計(jì)算機(jī)內(nèi)部表示為1000 0000)
    gctSize = 2 << (packed & 7);//讀取全局顏色列表大?。╬ixel)

    //讀取背景顏色索引和像素寬高比(Pixel Aspect Radio)
    bgIndex = read();
    pixelAspect = read();
}

/**
 * 讀取兩個(gè)字節(jié)的數(shù)據(jù)
 * @return
 */
protected int readShort() {
    return read() | (read() << 8);
}

根據(jù)readLSD()的讀取結(jié)果,我們知道了此GIF圖像中是否含有全局顏色列表(Global Color Table)(見下圖),如果有,就調(diào)用readColorTable(int ncolors)方法獲取全局顏色列表

全局顏色列表(Global Color Table)
/**
 * 讀取顏色列表
 * @param ncolors 列表大小,即顏色數(shù)量
 * @return
 */
protected int[] readColorTable(int ncolors) {
    int nbytes = 3 * ncolors;//一個(gè)顏色占3個(gè)字節(jié)(r g b 各占1字節(jié)),因此占用空間為 顏色數(shù)量*3 字節(jié)
    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];//設(shè)置最大尺寸避免邊界檢查
        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()方法內(nèi)部是如何運(yùn)作的

/**
 * 讀取圖像塊內(nèi)容
 */
protected void readContents() {
    boolean done = false;
    while (!(done || err())) {
        int code = read();
        switch (code) {
            //圖象標(biāo)識(shí)符(Image Descriptor)開始
            case 0x2C:
                readImage();
                break;
            //擴(kuò)展塊開始
            case 0x21: //擴(kuò)展塊標(biāo)識(shí),固定值0x21
                code = read();
                switch (code) {
                    case 0xf9: //圖形控制擴(kuò)展塊標(biāo)識(shí)(Graphic Control Label),固定值0xf9
                        readGraphicControlExt();
                        break;

                    case 0xff: //應(yīng)用程序擴(kuò)展塊標(biāo)識(shí)(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: //其他擴(kuò)展都選擇跳過
                        skip();
                }
                break;

            case 0x3b://標(biāo)識(shí)GIF文件結(jié)束,固定值0x3B
                done = true;
                break;

            case 0x00: //可能會(huì)出現(xiàn)的壞字節(jié),可根據(jù)需要在此處編寫壞字節(jié)分析等相關(guān)內(nèi)容
                break;
            default:
                status = STATUS_FORMAT_ERROR;
        }
    }
}

readContents()的核心流程就是根據(jù)塊的標(biāo)識(shí)來判斷當(dāng)前解碼的位置,調(diào)用相應(yīng)的方法對(duì)數(shù)據(jù)塊進(jìn)行解碼。如果GIF版本為89a,則數(shù)據(jù)塊中可能含有擴(kuò)展塊(可選)。其中圖像延遲時(shí)間存放在圖形控制擴(kuò)展(Graphic Control Extension)中,因此我們重點(diǎn)分析如何讀取圖形控制擴(kuò)展(Graphic Control Extension)(見下圖),其他擴(kuò)展塊解碼大家可以對(duì)照著代碼注釋和GIF結(jié)構(gòu)的相關(guān)知識(shí)自行研究,這里就不多贅述了

圖形控制擴(kuò)展(Graphic Control Extension)

解碼圖形控制擴(kuò)展(Graphic Control Extension)的方法為readGraphicControlExt(),有了上圖對(duì)各字節(jié)的說明其代碼也就很容易理解了,如下

/**
 * 讀取圖形控制擴(kuò)展塊
 */
protected void readGraphicControlExt() {
    read();//按讀取順序,此處為塊大小

    int packed = read();//讀取處置方法、用戶輸入標(biāo)志等
    dispose = (packed & 0x1c) >> 2; //從packed中解析出處置方法(Disposal Method)
    if (dispose == 0) {
        dispose = 1; //elect to keep old image if discretionary
    }
    transparency = (packed & 1) != 0;//從packed中解析出透明色標(biāo)志

    delay = readShort() * 10;//讀取延遲時(shí)間(毫秒)
    transIndex = read();//讀取透明色索引
    read();//按讀取順序,此處為標(biāo)識(shí)塊終結(jié)(Block Terminator)
}

GIF中可能含有多個(gè)圖像塊,圖像塊包含圖象標(biāo)識(shí)符(Image Descriptor)(見下圖)、局部顏色列表(Local Color Table)(根據(jù)局部顏色列表標(biāo)志確定是否存在)以及基于顏色列表的圖象數(shù)據(jù)(Table-Based Image Data)

圖象標(biāo)識(shí)符(Image Descriptor)

readContents()方法中遍歷了所有圖像塊,并調(diào)用readImage()進(jìn)行解碼,代碼及注釋如下

protected boolean lctFlag;//局部顏色列表標(biāo)志(Local Color Table Flag)
protected boolean interlace;//交織標(biāo)志(Interlace Flag)
protected int lctSize;//局部顏色列表大小(Size of Local Color Table)

/**
 * 按順序讀取圖像塊數(shù)據(jù):
 * 圖象標(biāo)識(shí)符(Image Descriptor)
 * 局部顏色列表(Local Color Table)(有的話)
 * 基于顏色列表的圖象數(shù)據(jù)(Table-Based Image Data)
 */
protected void readImage() {
    /**
     * 開始讀取圖象標(biāo)識(shí)符(Image Descriptor)
     */
    ix = readShort();//x方向偏移量
    iy = readShort();//y方向偏移量
    iw = readShort();//圖像寬度
    ih = readShort();//圖像高度

    int packed = read();
    lctFlag = (packed & 0x80) != 0;//局部顏色列表標(biāo)志(Local Color Table Flag)
    interlace = (packed & 0x40) != 0;//交織標(biāo)志(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;//若有局部顏色列表,則圖象數(shù)據(jù)是基于局部顏色列表的
    } else {
        act = gct; //否則都以全局顏色列表為準(zhǔn)
        if (bgIndex == transIndex) {
            bgColor = 0;
        }
    }
    int save = 0;
    if (transparency) {
        save = act[transIndex];//保存透明色索引位置原來的顏色
        act[transIndex] = 0;//根據(jù)索引位置設(shè)置透明顏色
    }
    if (act == null) {
        status = STATUS_FORMAT_ERROR;//若沒有顏色列表可用,則解碼出錯(cuò)
    }
    if (err()) {
        return;
    }

    /**
     * 開始解碼圖像數(shù)據(jù)
     */
    decodeImageData();
    skip();
    if (err()) {
        return;
    }
    frameCount++;
    image = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
    setPixels(); //將像素?cái)?shù)據(jù)轉(zhuǎn)換為圖像Bitmap
    frames.addElement(new GifFrame(image, delay));//添加到幀圖集合
    // list
    if (transparency) {
        act[transIndex] = save;//重置回原來的顏色
    }
    resetFrame();
}

readImage()中分三步進(jìn)行:讀取圖象標(biāo)識(shí)符(Image Descriptor)、讀取局部顏色列表(Local Color Table)解碼圖像數(shù)據(jù)。其中圖像數(shù)據(jù)是如何解碼并轉(zhuǎn)換成Bitmap圖像因?yàn)樘珡?fù)雜這里就不詳細(xì)展開描述了,以后可能會(huì)專門寫個(gè)番外篇進(jìn)行分析,當(dāng)然小伙伴們也可以自行閱讀分析這部分源碼:decodeImageData()setPixels()

至此 GifDecoder就基本分析完了,如果有講解不到位的地方歡迎大家留言指正。如果大家看了感覺還不錯(cuò)麻煩點(diǎn)個(gè)贊,你們的支持是我最大的動(dòng)力~


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,812評(píng)論 25 709
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,085評(píng)論 4 61
  • 今天,5月第1天,假日中。 今天的假日,于我,只是概念上的假日,沒有休息。于二醫(yī)院為患者治療的醫(yī)生,也是概念上的假...
    梅洛的聽雨軒閱讀 380評(píng)論 0 0
  • 凌晨一點(diǎn)一刻 我的肚子叫了一聲 “咕——” 我翻了個(gè)身,壓到空氣 又叫了一聲 “咕嚕——” 昂面躺著 摸摸癟癟的肚...
    寧緒緒閱讀 248評(píng)論 2 1
  • 玫瑰花全世界的人都知道這種花,它是情侶之間的禮物代表作。玫瑰花代表著美麗和愛情,它屬于薔薇的一種,它的艷麗像女人一...
    21029cbbb386閱讀 903評(píng)論 0 3

友情鏈接更多精彩內(nèi)容