Android实现JPEG图片压缩后同时保留图片的EXIF信息

移动开发 Android
ExifInterface类主要提供了读取、写入和缩略图处理这三个方面的功能。通过ExifInterface,可以获取到图片的多种属性,如方向(orientation)、拍摄时间(dateTime)、设备制造商(make)、设备型号(model)等。

EXIF信息是可交换图像文件格式(Exchangeable Image File Format)的缩写,是在JPEG格式的基础上发展起来的,其中包含了一系列按照一定标准制定的有关图像拍摄信息的数据和索引,包括快门速度、光圈、ISO感光度、曝光偏移、日期和时间、闪光使用情况、焦距、GPS定位数据等。

在实际开发中,对于图片数据不论是缓存在本地磁盘还是上传到后端,都需要先对图片进行压缩处理。在图片压缩的过程中,为了减小文件大小,一些不重要的元数据(包括EXIF信息)可能会被移除或修改。如果图片经过压缩处理,其原始的EXIF信息可能会丢失或不完整。

EXIF信息附加于JPEG、TIFF、RIFF等文件之中,可以记录数码照片的属性信息和拍摄数据。比如记录以下信息:

项目

资讯(举例)

制造厂商

Canon

相机型号

Canon EOS-1Ds Mark III

影像方向

正常(upper-left)

影像解析度X

300

影像解析度Y

300

解析度单位

dpi

软件

Adobe Photoshop CS Macintosh

最后异动时间

2005:10:06 12:53:19

YCbCrPositioning

2

曝光时间

0.00800 (1/125) sec

光圈值

F22

拍摄模式

光圈优先

ISO感光值

100

Exif资讯版本

30,32,32,31

影像拍摄时间

2005:09:25 15:00:18

影像存入时间

2005:09:25 15:00:18

曝光补偿(EV+-)

0

测光模式

点测光(Spot)

闪光灯

关闭

镜头实体焦长

12 mm

Flashpix版本

30,31,30,30

影像色域空间

sRGB

影像尺寸X

5616 pixel

影像尺寸Y

3744 pixel

有一些压缩工具或软件提供了保留EXIF信息的选项。在使用这些工具进行压缩时,可以选择保留EXIF信息,以确保压缩后的图片仍然包含完整的元数据。在实际开发中我们如何进行保留EXIF信息的同时进行图片压缩呢?

使用ExifInterface方案

ExifInterface是Android系统中用于描述多媒体文件(如JPG格式图片)附加信息的一个类。它主要涵盖了拍摄时的光圈、快门、白平衡、ISO、焦距、日期时间等各种拍摄条件,以及相机品牌、型号、色彩编码、拍摄时录制的声音以及全球定位系统(GPS)和缩略图等信息。简单来说,ExifInterface就是JPEG图像文件+拍摄参数的结合。

ExifInterface类主要提供了读取、写入和缩略图处理这三个方面的功能。通过ExifInterface,可以获取到图片的多种属性,如方向(orientation)、拍摄时间(dateTime)、设备制造商(make)、设备型号(model)等。

ExifInterface类只提供了 getXXX() 和 setAttributes(String tag, String value) 这种操作单个属性的方法,如果想将原图片文件中的所有EXIF信息完整复制到另一个图片中会非常繁琐。因此有人通过反射,对所有属性名进行遍历,从而实现了批量操作。也算是一种解决方案,具体如下:

public static void saveExif(String oldFilePath, String newFilePath) throws Exception {
    ExifInterface oldExif = new ExifInterface(oldFilePath);
    ExifInterface newExif = new ExifInterface(newFilePath);
    Class<ExifInterface> cls = ExifInterface.class;
    Field[] fields = cls.getFields();
    for (int i = 0; i < fields.length; i++) {
        String fieldName = fields[i].getName();
        if (!TextUtils.isEmpty(fieldName) && fieldName.startsWith("TAG")) {
            String fieldValue = fields[i].get(cls).toString();
            String attribute = oldExif.getAttribute(fieldValue);
            if (attribute != null) {
                newExif.setAttribute(fieldValue, attribute);
            }
        }
    }
    //将内存中的修改写入磁盘(IO操作)
    newExif.saveAttributes();
 }

以上方案弊端也很明显,就是需要对文件进行多次IO操作。观察上面方法中的两个参数都是文件路径,比如我们通过拍照进行图片压缩上传,那么拍完照通过 onPictureTaken(byte[] data, Camera camera) 回调方法拿到图片的 byte[] data 数据后处理是这样的:

  • 将data缓存到磁盘,路径为oldFilePath;(IO)
  • 将data转换成 bitmap 进行压缩、旋转、剪切等操作;
  • 将处理后的 bitmap 缓存到磁盘,路径为newFilePath;(IO)
  • 调用上面的 saveExif(oldFilePath, newFilePath) 方法; (IO)

能否只在内存中操作?发现有 ExifInterface (String filename) 和 ExifInterface (InputStream inputStream) 两种构造方法, 进行如下改造:

public static void saveExif(byte[] srcData, String destFilePath) throws Exception {
    ExifInterface oldExif = new ExifInterface(new ByteArrayInputStream(srcData));
    ExifInterface newExif = new ExifInterface(destFilePath);
    Class<ExifInterface> cls = ExifInterface.class;
    Field[] fields = cls.getFields();
    for (int i = 0; i < fields.length; i++) {
        String fieldName = fields[i].getName();
        if (!TextUtils.isEmpty(fieldName) && fieldName.startsWith("TAG")) {
            String fieldValue = fields[i].get(cls).toString();
            String attribute = oldExif.getAttribute(fieldValue);
            if (attribute != null) {
                newExif.setAttribute(fieldValue, attribute);
            }
        }
    }
    //将内存中的修改写入磁盘(IO操作)
    newExif.saveAttributes();
 }

使用自定义方案

不管是图片还是其他文件,本质都是格式化的数据,都有专用的数据结构。研究下JPG的数据结构,找到 EXIF 数据块的起始索引,然后从源文件byte[]中复制插入到目标文件byte[]对应位置中就实现了。

图片图片

JPEG文件的内容都开始于一个二进制的值 '0xFFD8', 并结束于二进制值'0xFFD9'. 在JPEG的数据中有好几种类似于二进制 0xFFXX 的数据都统称作 "标记", 代表了一段JPEG的信息数据。

0xFFD8 的意思是 SOI图像起始(Start of image) ,是Jpeg文件的魔数(Magic Number)。每种格式的文件都有固定的Magic Number,比如.class 字节码文件的Magic Number是 “0xCAFEBABE”。0xFFD9 则表示 EOI图像结束 (End of image)。

0xFF+标记号(1个字节)+数据大小描述符(2个字节)+数据内容(n个字节)

对于EXIF数据,使用的是APP1标记,前两个字节固定为 0xFFE1,后面紧跟着两个字节记录的是EXIF数据内容的 length + 2,假设这两个字节的值是 24,那么EXIF数据内容的长度就是22字节。所以只要找到EXIF在数组中的起始索引,抠出来插入到新数组中去就完成了。

图片图片

public static byte[] cloneExif(byte[] srcData, byte[] destData) {
    if (srcData == null || srcData.length == 0 || destData == null || destData.length == 0) return null;

    ImageHeaderParser srcImageHeaderParser = new ImageHeaderParser(srcData);
    byte[] srcExifBlock = srcImageHeaderParser.getExifBlock();
    if (srcExifBlock == null || srcExifBlock.length <= 4) return null;

    LOG.d(TAG, "pictureData src: %1$s KB; dest: %2$s KB", srcData.length / 1024, destData.length / 1024);
    LOG.d(TAG, "srcExif: %s B", srcExifBlock.length);
    ImageHeaderParser destImageHeaderParser = new ImageHeaderParser(destData);
    byte[] destExifBlock = destImageHeaderParser.getExifBlock();
    if (destExifBlock != null && destExifBlock.length > 0) {
        LOG.d(TAG, "destExif: %s B", destExifBlock.length);
        //目标图片中已有exif信息, 需要先删除
        int exifStartIndex = destImageHeaderParser.getExifStartIndex();
        //构建新数组
        byte[] newDestData = new byte[srcExifBlock.length + destData.length - destExifBlock.length];
        //copy 1st block
        System.arraycopy(destData, 0, newDestData, 0, exifStartIndex);
        //copy 2rd block (exif)
        System.arraycopy(srcExifBlock, 0, newDestData, exifStartIndex, srcExifBlock.length);
        //copy 3th block
        int srcPos = exifStartIndex + destExifBlock.length;
        int destPos = exifStartIndex + srcExifBlock.length;
        System.arraycopy(destData, srcPos, newDestData, destPos, destData.length - srcPos);
        LOG.d(TAG, "output image Data with exif: %s KB", newDestData.length / 1024);
        return newDestData;
    } else {
        LOG.d(TAG, "destExif: %s B", 0);
        //目标图片中没有exif信息
        byte[] newDestData = new byte[srcExifBlock.length + destData.length];
        //copy 1st block (前两个字节)
        System.arraycopy(destData, 0, newDestData, 0, 2);
        //copy 2rd block (exif)
        System.arraycopy(srcExifBlock, 0, newDestData, 2, srcExifBlock.length);
        //copy 3th block
        int srcPos = 2;
        int destPos = 2 + srcExifBlock.length;
        System.arraycopy(destData, srcPos, newDestData, destPos, destData.length - srcPos);
        LOG.d(TAG, "output image Data with exif: %s KB", newDestData.length / 1024);
        return newDestData;
    }

}

将原图的数据流和压缩处理后的数据流传入,调用cloneExif方法,返回附加了EXIF信息的数据流,将返回的数据流存储即得到一张带有原EXIF信息的压缩图片。

「注意」上述方法只针对JPEG格式图片,其他格式文件数据结构不同,方法可能无效。

责任编辑:武晓燕 来源: 沐雨花飞蝶
相关推荐

2009-08-20 12:35:41

C#读取图片的EXIF

2009-08-24 17:02:18

C#旋转图片EXIF

2020-05-07 09:45:16

前端JS图片压缩

2020-10-20 11:12:11

Nodejs

2013-07-29 10:02:42

2023-11-04 12:43:44

前端图片参数

2022-08-08 08:29:55

图片压缩前端互联网

2013-06-27 11:16:27

Android异步加载

2018-10-29 09:24:41

Web图片压缩网页加速

2023-01-15 20:28:32

前端图片压缩

2022-07-17 11:22:35

前端开发图片裁切压缩上传

2016-03-29 10:18:48

Android图片代码

2023-08-21 12:13:53

2010-10-12 13:57:43

GoogleWebP

2011-04-11 14:14:29

checkboxlistviewAndroid

2020-08-21 09:58:16

谷歌Android工具

2022-06-14 07:29:51

squoosh压缩工具开源

2011-05-30 13:23:11

Android 动画

2013-05-15 10:27:05

R语言

2011-06-03 10:48:23

Android ListView
点赞
收藏

51CTO技术栈公众号