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

版权声明:本文为博主原创文章,未经博主允许不得转载。
系列博客:源码阅读系列
源码:GifDecoder

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

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

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

GIF结构简述

相关博文链接

gif 格式图片详细解析

在分析源码之前,我们得先对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文件头(File Header)


判断传入文件格式

解码之前肯定要先判断解码的对象是否为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’三个字符,那么这段代码就很好理解了,就是根据读取出来的文件头字符串开头是否为‘GIF’来判断此文件格式符不符合要求

文件头(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就基本分析完了,如果有讲解不到位的地方欢迎大家留言指正。如果大家看了感觉还不错麻烦点个赞,你们的支持是我最大的动力~


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,595评论 4 59
  • 今天,5月第1天,假日中。 今天的假日,于我,只是概念上的假日,没有休息。于二医院为患者治疗的医生,也是概念上的假...
    梅洛的听雨轩阅读 300评论 0 0
  • 凌晨一点一刻 我的肚子叫了一声 “咕——” 我翻了个身,压到空气 又叫了一声 “咕噜——” 昂面躺着 摸摸瘪瘪的肚...
    宁绪绪阅读 158评论 2 1
  • 玫瑰花全世界的人都知道这种花,它是情侣之间的礼物代表作。玫瑰花代表着美丽和爱情,它属于蔷薇的一种,它的艳丽像女人一...
    21029cbbb386阅读 790评论 0 3