Android實(shí)現(xiàn)JPEG圖片壓縮后同時(shí)保留圖片的EXIF信息
EXIF信息是可交換圖像文件格式(Exchangeable Image File Format)的縮寫,是在JPEG格式的基礎(chǔ)上發(fā)展起來的,其中包含了一系列按照一定標(biāo)準(zhǔn)制定的有關(guān)圖像拍攝信息的數(shù)據(jù)和索引,包括快門速度、光圈、ISO感光度、曝光偏移、日期和時(shí)間、閃光使用情況、焦距、GPS定位數(shù)據(jù)等。
在實(shí)際開發(fā)中,對(duì)于圖片數(shù)據(jù)不論是緩存在本地磁盤還是上傳到后端,都需要先對(duì)圖片進(jìn)行壓縮處理。在圖片壓縮的過程中,為了減小文件大小,一些不重要的元數(shù)據(jù)(包括EXIF信息)可能會(huì)被移除或修改。如果圖片經(jīng)過壓縮處理,其原始的EXIF信息可能會(huì)丟失或不完整。
EXIF信息附加于JPEG、TIFF、RIFF等文件之中,可以記錄數(shù)碼照片的屬性信息和拍攝數(shù)據(jù)。比如記錄以下信息:
項(xiàng)目 | 資訊(舉例) |
制造廠商 | Canon |
相機(jī)型號(hào) | Canon EOS-1Ds Mark III |
影像方向 | 正常(upper-left) |
影像解析度X | 300 |
影像解析度Y | 300 |
解析度單位 | dpi |
軟件 | Adobe Photoshop CS Macintosh |
最后異動(dòng)時(shí)間 | 2005:10:06 12:53:19 |
YCbCrPositioning | 2 |
曝光時(shí)間 | 0.00800 (1/125) sec |
光圈值 | F22 |
拍攝模式 | 光圈優(yōu)先 |
ISO感光值 | 100 |
Exif資訊版本 | 30,32,32,31 |
影像拍攝時(shí)間 | 2005:09:25 15:00:18 |
影像存入時(shí)間 | 2005:09:25 15:00:18 |
曝光補(bǔ)償(EV+-) | 0 |
測(cè)光模式 | 點(diǎn)測(cè)光(Spot) |
閃光燈 | 關(guān)閉 |
鏡頭實(shí)體焦長(zhǎng) | 12 mm |
Flashpix版本 | 30,31,30,30 |
影像色域空間 | sRGB |
影像尺寸X | 5616 pixel |
影像尺寸Y | 3744 pixel |
有一些壓縮工具或軟件提供了保留EXIF信息的選項(xiàng)。在使用這些工具進(jìn)行壓縮時(shí),可以選擇保留EXIF信息,以確保壓縮后的圖片仍然包含完整的元數(shù)據(jù)。在實(shí)際開發(fā)中我們?nèi)绾芜M(jìn)行保留EXIF信息的同時(shí)進(jìn)行圖片壓縮呢?
使用ExifInterface方案
ExifInterface是Android系統(tǒng)中用于描述多媒體文件(如JPG格式圖片)附加信息的一個(gè)類。它主要涵蓋了拍攝時(shí)的光圈、快門、白平衡、ISO、焦距、日期時(shí)間等各種拍攝條件,以及相機(jī)品牌、型號(hào)、色彩編碼、拍攝時(shí)錄制的聲音以及全球定位系統(tǒng)(GPS)和縮略圖等信息。簡(jiǎn)單來說,ExifInterface就是JPEG圖像文件+拍攝參數(shù)的結(jié)合。
ExifInterface類主要提供了讀取、寫入和縮略圖處理這三個(gè)方面的功能。通過ExifInterface,可以獲取到圖片的多種屬性,如方向(orientation)、拍攝時(shí)間(dateTime)、設(shè)備制造商(make)、設(shè)備型號(hào)(model)等。
ExifInterface類只提供了 getXXX() 和 setAttributes(String tag, String value) 這種操作單個(gè)屬性的方法,如果想將原圖片文件中的所有EXIF信息完整復(fù)制到另一個(gè)圖片中會(huì)非常繁瑣。因此有人通過反射,對(duì)所有屬性名進(jìn)行遍歷,從而實(shí)現(xiàn)了批量操作。也算是一種解決方案,具體如下:
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);
}
}
}
//將內(nèi)存中的修改寫入磁盤(IO操作)
newExif.saveAttributes();
}
以上方案弊端也很明顯,就是需要對(duì)文件進(jìn)行多次IO操作。觀察上面方法中的兩個(gè)參數(shù)都是文件路徑,比如我們通過拍照進(jìn)行圖片壓縮上傳,那么拍完照通過 onPictureTaken(byte[] data, Camera camera) 回調(diào)方法拿到圖片的 byte[] data 數(shù)據(jù)后處理是這樣的:
- 將data緩存到磁盤,路徑為oldFilePath;(IO)
- 將data轉(zhuǎn)換成 bitmap 進(jìn)行壓縮、旋轉(zhuǎn)、剪切等操作;
- 將處理后的 bitmap 緩存到磁盤,路徑為newFilePath;(IO)
- 調(diào)用上面的 saveExif(oldFilePath, newFilePath) 方法; (IO)
能否只在內(nèi)存中操作?發(fā)現(xiàn)有 ExifInterface (String filename) 和 ExifInterface (InputStream inputStream) 兩種構(gòu)造方法, 進(jìn)行如下改造:
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);
}
}
}
//將內(nèi)存中的修改寫入磁盤(IO操作)
newExif.saveAttributes();
}
使用自定義方案
不管是圖片還是其他文件,本質(zhì)都是格式化的數(shù)據(jù),都有專用的數(shù)據(jù)結(jié)構(gòu)。研究下JPG的數(shù)據(jù)結(jié)構(gòu),找到 EXIF 數(shù)據(jù)塊的起始索引,然后從源文件byte[]中復(fù)制插入到目標(biāo)文件byte[]對(duì)應(yīng)位置中就實(shí)現(xiàn)了。
圖片
JPEG文件的內(nèi)容都開始于一個(gè)二進(jìn)制的值 '0xFFD8', 并結(jié)束于二進(jìn)制值'0xFFD9'. 在JPEG的數(shù)據(jù)中有好幾種類似于二進(jìn)制 0xFFXX 的數(shù)據(jù)都統(tǒng)稱作 "標(biāo)記", 代表了一段JPEG的信息數(shù)據(jù)。
0xFFD8 的意思是 SOI圖像起始(Start of image) ,是Jpeg文件的魔數(shù)(Magic Number)。每種格式的文件都有固定的Magic Number,比如.class 字節(jié)碼文件的Magic Number是 “0xCAFEBABE”。0xFFD9 則表示 EOI圖像結(jié)束 (End of image)。
0xFF+標(biāo)記號(hào)(1個(gè)字節(jié))+數(shù)據(jù)大小描述符(2個(gè)字節(jié))+數(shù)據(jù)內(nèi)容(n個(gè)字節(jié))
對(duì)于EXIF數(shù)據(jù),使用的是APP1標(biāo)記,前兩個(gè)字節(jié)固定為 0xFFE1,后面緊跟著兩個(gè)字節(jié)記錄的是EXIF數(shù)據(jù)內(nèi)容的 length + 2,假設(shè)這兩個(gè)字節(jié)的值是 24,那么EXIF數(shù)據(jù)內(nèi)容的長(zhǎng)度就是22字節(jié)。所以只要找到EXIF在數(shù)組中的起始索引,摳出來插入到新數(shù)組中去就完成了。
圖片
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);
//目標(biāo)圖片中已有exif信息, 需要先刪除
int exifStartIndex = destImageHeaderParser.getExifStartIndex();
//構(gòu)建新數(shù)組
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);
//目標(biāo)圖片中沒有exif信息
byte[] newDestData = new byte[srcExifBlock.length + destData.length];
//copy 1st block (前兩個(gè)字節(jié))
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;
}
}
將原圖的數(shù)據(jù)流和壓縮處理后的數(shù)據(jù)流傳入,調(diào)用cloneExif方法,返回附加了EXIF信息的數(shù)據(jù)流,將返回的數(shù)據(jù)流存儲(chǔ)即得到一張帶有原EXIF信息的壓縮圖片。
「注意」上述方法只針對(duì)JPEG格式圖片,其他格式文件數(shù)據(jù)結(jié)構(gòu)不同,方法可能無(wú)效。