最近在做Android上面的GIF图片的缩放的开发,Android原生的框架并不支持这个功能,使用BitmapFactory去解码GIF图片也只是把GIF图片的第一帧解码到Bitmap中而已。
经过一定的调研搜索,我确定了有三种可行的方法:
1. 使用Java版本的GIF解码器和编码器将GIF中的每一帧都解码出来,然后缩放,再编码到新的GIF文件中。
2. 使用Android上面的ImageMagick库,https://github.com/paulasiimwe/Android-ImageMagick
3. 使用GIFlib库
Android版本的ImageMagick库很大,有好几兆,而且我试了一下用它提供的接口进行GIF的缩放是没有效果的,而且接口设计得也很不优雅,注释写的也很费解,最后果断放弃了这个方法。也考虑过将ImageMagick中与GIF相关的模块抠出来单独编译,以使库的体积减小,但是我尝试了一下,需要去摸清楚ImagMagick的框架和代码,大家也知道去阅读别人的代码,始终是一个吃力的活。
至于GIFlib库,貌似Android系统就是用的这个库,但是好像没有人做GIFlib的应用层移植,所以这个方法我也放弃了。
关于第一种方案,我在Github上面找到了Java版本的GIF解码器和编码器,初步尝试了将每一帧解码出来-》缩放-》编码的方法,初步的结论是可行,但是有一个致命的问题,就是重新编码后的图片的透明区域没有了。
开始时,我以为如果GIF图片没有透明区域(变成了有色的,通常情况下是黑色),那这个问题就不会被发现了,可是我想错了,动画的GIF图片中的非首帧通常情况下都是有透明区域的,因为如果某一帧的某个区域现对于前面一帧是没有变化的话,那么这一帧的这个区域就是透明的,透明区域会在GIF图片中大量存在,所以这个透明区域的问题一定得解决。
三种方案如何抉择?将自己有限的时间精力投入到哪一种方案会使问题得以解决?我的分析是第一种和第二种方案的代码量大,而且需要自己去读懂别人的代码,这是一个很吃力的活,需要投入大量的精力和时间,而且在阅读的过程中自己动手去做一些小demo进行验证的可能性是很小的。而第三种方案,Java版本的解码器和编码器的代码量小,如果加上自己去研究GIF的文件结构(http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp),再加上自己在这个过程中不断地写一些小demo进行验证,自己的积极性会被不断地提高,相信问题会很快解决,所以最终我选择了第三种方案。
首先要说一下GIF的透明区域的实现原理,GIF不像PNG或者WEBP那样有自己的alpha通道,GIF是通过指定颜色表(通常有256中颜色)中的某一个颜色为透明色,那么图片中只要有某个像素的颜色为这个值,那么在绘制的时候这个像素就会被绘制为透明的,所以在这种情况下,GIF能够用的颜色就少了一种,只有255中颜色了。
所以了解了这个原理,那么来看看Java版本的编码器下面这段代码:
protected void analyzePixels() {
int len = pixels.length;
int nPix = len / 3;
indexedPixels = new byte[nPix];
NeuQuant nq = new NeuQuant(pixels, len, sample);
// initialize quantizer
colorTab = nq.process(); // create reduced palette
// convert map from BGR to RGB
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;
}
// map image pixels to new palette
int k = 0;
for (int i = 0; i < nPix; i++) {
int index = nq.map(pixels[k++] & 0xff, pixels[k++] & 0xff, pixels[k++] & 0xff);
usedEntry[index] = true;
indexedPixels[i] = (byte) index;
}
pixels = null;
colorDepth = 8;
palSize = 7;
// get closest match to transparent color if specified
if (transparent != -1) {
transIndex = findClosest(transparent);
}
}
这段代码所做的事情就是生成颜色表,并将图像的像素点的颜色值与颜色表进行索引,transparent就是编码器的使用者设置的透明色,也就是说,只要某个像素的颜色值为transparent的值,那么这个点就是透明的。代码中findClosest这个方法是有问题的,找到一个最接近transparent的值的index作为transindex,那就是说非常有可能找的颜色值并不是transparent,而是一个接近transparent的值,那就是说在这样的情况下原本应该为透明的地方,却没有成为透明,修改如下: /**
* Analyzes image colors and creates color map.
*/
protected void analyzePixels() {
int len = pixels.length;
int nPix = len / 3;
indexedPixels = new byte[nPix];
NeuQuant nq = new NeuQuant(pixels, len, sample);
// initialize quantizer
colorTab = nq.process(); // create reduced palette
// convert map from BGR to RGB
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;
}
// map image pixels to new palette
int k = 0;
for (int i = 0; i < nPix; i++) {
int index = nq.map(pixels[k++] & 0xff, pixels[k++] & 0xff,
pixels[k++] & 0xff);
usedEntry[index] = true;
indexedPixels[i] = (byte) index;
}
pixels = null;
colorDepth = 8;
palSize = 7;
if (transparent != -1) {
transIndex = nq.map((transparent >> 0) & 0xff,
(transparent >> 8) & 0xff, (transparent >> 16) & 0xff);
}
}
从nq中直接map,因为传进来的transparent值是从decoder中取出来的(我已经在decoder中加了相应的代码),所以不用担心transparent值会被其他非透明像素点使用。而且在接口设计上,setTransparent这个值的传入参数transparent本来就应该从图片中取出来,因为你并不知道哪些颜色是没有被使用的,除非你写出相应的代码去判断。
编码器的修改主要在于,透明色的处理上面,在解码器上面也有一些修改,主要包括:
1. 增加获取loopCount的接口,loopCount参数指示动画的播放模式,是否重复等等
2. 增加获取透明色的接口,这里需要注意的是,在图像被加载为Bitmap后,如果config是ARGB8888的话,对于透明的像素,RGB的值在解码的时候是被忽略掉了的,所以ARGB都为0。
3. 获取每一帧的dispose参数的方法,getDisposalMethod,dispose参数决定下一帧在绘制时,对上一帧的处理方法,是留下上一帧呢,还是全部抹掉