Android實現JPEG圖片壓縮后同時保留圖片的EXIF信息
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格式圖片,其他格式文件數據結構不同,方法可能無效。