1.概述
最近参与的项目是一个和表情相关的App,也就理所当然的和GIF打交道了。下面就项目中遇到的和GIF相关的问题及解决办法分
享出来,第一次接触GIF,了解的不多,希望能和大家共同探讨学习。
本文学习参考博客:gif格式图片详细解析。
2.GIF简介
3.GIF文件结构
如上图,GIF格式的文件结构整体上分为三部分:文件头、GIF数据流、文件结尾。文件头包含GIF文件署名和版本号;GIF
数据流由控制标识符、图象块和其他的一些扩展块组成;文件终结器只有一个值为0x3B的字符(';')表示文件结束。关于更多细节
可以参考博客:gif格式图片详细解析。
结合GIF文件格式理论知识,简单介绍一下我是如何通过解析或者处理GIF文件完成项目中实际的需求的。若有不对的地方或者
不够优化的方法欢迎提出,大家互相交流学习。项目中遇到的关于GIF的需求较多,以下是三个较为常用的需求:
需求一:判断文件是否是GIF格式文件
需求二:获取GiF文件宽高属性
需求三:获取和修改GIF文件帧间隔
4.需求一 &GIF署名(Signature)和版本号(Version)
`
如上图,GIF文件的前6个字节内容是GIF的署名和版本号。我们可以通过前3个字节判断文件是否为GIF格式,后3个字节判断
GIF格式的版本。
可以借助第三方软件工具验证如上理论知识:使用16进制编辑器打开某个GIF文件,我们可以清晰的看到文件头,如下是部分
截图:16进制的 47 49 46 38 39 61所表示的正好是GIF89a。
有了如上理论知识,我们就可以很轻松的实现需求一了:
/**
* 判断是否gif图片
* @param inputStream
* @return
* @throws IOException
*/
private boolean isGif(InputStream inputStream) throws IOException {
byte[] temp = new byte[3];
int count = inputStream.read(temp);
//可读字节数小于3
if(count < 3) {
temp = null;
return false;
}
//满足下面的条件,说明是gif图
if(temp[0] == (byte) 'G' && temp[1] == (byte) 'I' && temp[2] == (byte) 'F') {
temp = null;
return true;
} else {
temp = null;
return false;
}
}
5.需求二 & 逻辑屏幕标识符(Logical Screen Descript)
如上图,逻辑屏幕标识符配置了GIF图片一些全局属性,可以通过读取解析获取GIF图片的一些全局配置。我们以需求二为例:
/**
* 获取gif图片宽高
* @param file
* @return
*/
public static int[] getGifFileWidthAndHeight(File file) {
int[] result = new int[] { -1, -1 };
if (file == null || !file.exists()) {
return result;
}
try {
InputStream inputStream = new FileInputStream(file);
if (!isGif(inputStream)) {//判断是否gif
return result;
}
inputStream.skip(3); //跳过中间3个
int lenTemp = 4;// need content len
int len = 2;// len for width and height , each occupy two byte .
byte[] temp = new byte[lenTemp];
byte[] width = new byte[len];
byte[] height = new byte[len];
inputStream.read(temp, 0, temp.length);
width[0] = temp[0];
width[1] = temp[1];
result[0] = bytes2ToInt(width);
height[0] = temp[2];
height[1] = temp[3];
result[1] = bytes2ToInt(height);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
注意:宽高分别占用2个字节,且高位字节在后。
6.需求三 & 图形控制扩展(Graphic Control Extension)
在89a版本,GIF添加了图形控制扩展块。放在一个图象块(图象标识符)的前面,用来控制紧跟在它后面的第一个图象的显示。
如上图:图形控制扩展块中包含了扩展块标识符、图形控制扩展标签、块大小、延迟时间等。可以通过读取解析获取GIF图片的
一些全局配置。我们以需求三获取和修改帧间隔为例:
/**
* 处理帧间隔为0的gif图片
* @param path 文件路径
* @throws IOException
*/
public static void dealGif(String path) {
long start = 0;
List<Integer> list = new ArrayList<>();
InputStream inputStream = null;
try {
inputStream = new FileInputStream(new File(path));
if (!isGif(inputStream)) {
return;
}
inputStream.skip(10);//跳过10个
start = System.currentTimeMillis();
for (int i = 13; ;) {//判断是否gif读取了3个+跳过了10个,所以这里从位置13开始
int code = inputStream.read();
i++;
if (code == 0x21) {
code = inputStream.read();
i++;
if (code == 0xF9) {
code = inputStream.read();
i++;
if (code == 4) {
inputStream.skip(1);
int delay = readDelayTime(inputStream);
i += 3;
LogUtils.d(TAG, "path="+path);
LogUtils.d(TAG, "delay="+delay);
if (delay == 0) {
try {
changeFile(path, i - 2, 1, 10);
} catch (Exception e) {
e.printStackTrace();
}
list.add(i - 2);
}
}
}
} else if (code == -1) { //文件尾-结束
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
LogUtils.d(TAG, "dealGif time=" + (System.currentTimeMillis() - start) / 1000);
}
/**
* 修改gif图片固定位置值
* @param fName
* @param start
* @param len
* @param targetValue 目标值
* @throws IOException
*/
public static boolean changeFile(String fName, int start, int len, int targetValue) throws Exception{
LogUtils.d(TAG, "path="+fName);
//创建一个随机读写文件对象 ??
java.io.RandomAccessFile raf = null;
//打开一个文件通道 ??
java.nio.channels.FileChannel channel = null;
//映射文件中的某一部分数据以读写模式到内存中 ??
java.nio.MappedByteBuffer buffer;
try {
raf = new java.io.RandomAccessFile(fName,"rw");
channel = raf.getChannel();
buffer = channel.map(FileChannel.MapMode.READ_WRITE, start, len);
//示例修改字节 ??
for(int i = 0; i < len; i++){
byte src = buffer.get(i);
LogUtils.d(TAG, "src="+String.valueOf(src));
buffer.put(i, (byte) targetValue);
}
buffer.force();//制输出,在buffer中的改动生效到文件
buffer.clear();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (channel != null) {
channel.close();
}
if (raf != null) {
raf.close();
}
}
return true;
}
/**
* 读取帧间隔
* @param inputStream
* @return
*/
private static int readDelayTime(InputStream inputStream) {
byte[] bytes = new byte[2];
try {
inputStream.read(bytes, 0, bytes.length);
return FileUtils.bytes2ToInt(bytes);
} catch (IOException e) {
e.printStackTrace();
}
return -1;
}
注意:如上方法通常是比较耗时的,根据具体的场景我们要有选择性的使用,或者可以放到服务端处理。