Exif 是什麼
Exif是一種圖像文件格式,可以記錄數碼照片的屬性信息和拍攝數據;
實際上Exif格式就是在JPEG格式頭部插入了數碼照片的信息,包括拍攝時的相機品牌、型號、光圈、焦距、白平衡等相機硬件信息和圖片參數信息。
主要包括以下幾類信息:
- 拍攝日期
- 拍攝器材(機身、鏡頭、閃光燈等)
- 拍攝參數(快門速度、光圈F值、ISO速度、焦距、測光模式等)
- 圖像處理參數(銳化、對比度、飽和度、白平衡等)
- 圖像描述及版權信息
- GPS定位數據
- 縮略圖
例如:
項目 | 信息 |
---|---|
製造廠商 | Canon |
相機型號 | Canon EOS-1Ds Mark III |
曝光時間 | 0.00800 (1/125) sec |
光圈值 | F22 |
閃光燈 | 關閉 |
… | … |
… | … |
… | … |
Exif 格式
Exif信息以0xFFE1作爲開頭標記,後兩個字節表示Exif信息的長度。所以Exif信息最大爲64 kB.
具體格式如下所示:
JPEG 數據格式
JPEG 數據格式
--------------------------------------------------------------------------------------------------------------------------
| SOI 標記 | Exif 的大小=SSSS | 標記 YY 的大小=TTTT | SOS 標記 的大小=UUUU | 圖像數據流 | EOI 標記
--------------------------------------------------------------------------------------------------------------------------
| FFD8 | FFE1 SSSS DDDD...... | FFYY TTTT DDDD...... | FFDA UUUU DDDD.... | I I I I.... | FFD9
--------------------------------------------------------------------------------------------------------------------------
如何修改Exif數據
1. 修改單個或者多個數據
Android 提供了操作Exif信息的類 ExifInterface
;
可以通過該類的相關方法來獲取以及修改JPEG圖片中的Exif信息。
獲取Exif信息
try {
//oldPath:圖片地址
ExifInterface exifInterface = new ExifInterface(oldPath);
String dateData = exifInterface.getAttribute(ExifInterface.TAG_DATETIME);
} catch (IOException e) {
e.printStackTrace();
}
修改Exif信息
try {
//newPath:圖片地址
ExifInterface exifInterface = new ExifInterface(newPath);
exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION,"6");
exifInterface.saveAttributes();
} catch (IOException e) {
e.printStackTrace();
}
其中ExifInterface
有三種構造函數,可以根據實際情況創建適當的對象
public ExifInterface(String filename);
public ExifInterface(InputStream inputStream);
public ExifInterface(FileDescriptor fileDescriptor);
2. 複製整個Exif數據
在Camera開發過程中,在生成最終的保存圖片之前,往往會因爲特效等操作需要對圖片添加濾鏡等特效,在這個過程中會將原始的JPEG數據轉換爲BMP格式以方便操作,但是在BMP格式中Exif數據會丟失,導致最終將BMP轉換爲所保存的JPEG格式數據中無Exif信息。
這種情況,可以在將JPEG轉換爲BMP格式之前,將Exif數據讀取,然後再插入到最終要生成的JPEG圖片中,以達到Exif數據完整的目的。
具體如何讀取可以根據Exif格式的來操作
JPEG 數據格式
--------------------------------------------------------------------------------------------------------------------------
| SOI 標記 | Exif 的大小=SSSS | 標記 YY 的大小=TTTT | SOS 標記 的大小=UUUU | 圖像數據流 | EOI 標記
--------------------------------------------------------------------------------------------------------------------------
| FFD8 | FFE1 SSSS DDDD...... | FFYY TTTT DDDD...... | FFDA UUUU DDDD.... | I I I I.... | FFD9
--------------------------------------------------------------------------------------------------------------------------
從JPEG數據格式可知,JPEG文件開始於一個二進制的值0xFFD8
,結束於二進制值0xFFD9
.
其中SOI(Start of image)
表示圖像開始,EOI(End of image)
表示圖像結束。
從Exif格式我們可以得知,Exif信息是以FFE1
開頭,並在之後的兩個字節中記錄了Exif信息的長度(該長度=2+Exif數據的長度;即自己所佔的兩個字節也囊括其中);那麼在有標記信息和信息長度的情況下可以很方便的讀取到Exif的全部數據。
那麼代碼實現如下:
其中源圖片包括Exif信息,目標圖片中無Exif信息。
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
*
* JPEG 數據格式
* --------------------------------------------------------------------------------------------------------------------------
* | SOI 標記 | Exif 的大小=SSSS | 標記 YY 的大小=TTTT | SOS 標記 的大小=UUUU | 圖像數據流 | EOI 標記
* --------------------------------------------------------------------------------------------------------------------------
* | FFD8 | FFE1 SSSS DDDD...... | FFYY TTTT DDDD...... | FFDA UUUU DDDD.... | I I I I.... | FFD9
* --------------------------------------------------------------------------------------------------------------------------
*/
public class ImageHeaderParser {
private static final String TAG = "CAMap_ImageHeaderParser";
private static final int EXIF_MAGIC_NUMBER = 0xFFD8;
private static final int SEGMENT_SOS = 0xDA;
private static final int MARKER_EOI = 0xD9;
private static final int SEGMENT_START_ID = 0xFF;
private static final int EXIF_SEGMENT_TYPE = 0xE1;
private byte[] mExifOfJpeg;
private final StreamReader streamReader;
public ImageHeaderParser(byte[] data) {
this(new ByteArrayInputStream(data));
}
public ImageHeaderParser(InputStream is) {
streamReader = new StreamReader(is);
parserExif();
}
public static byte[] cloneExif(byte[] srcData, byte[] destData) {
if (srcData == null || destData == null || srcData.length == 0 || destData.length == 0) {
return null;
}
ImageHeaderParser srcImageHeaderParser = new ImageHeaderParser(srcData);
byte[] srcExif = srcImageHeaderParser.getExifOfJpeg();
int srcExifLength = srcExif.length;
if (srcExif == null || srcExifLength <= 4) {
return null;
}
ImageHeaderParser destImageHeaderParser = new ImageHeaderParser(destData);
byte[] destExif = destImageHeaderParser.getExifOfJpeg();
if (destExif == null || destExif.length == 0) {
byte[] newDestData = new byte[srcExifLength + destData.length];
//copy FFD8
System.arraycopy(destData, 0, newDestData, 0, 2);
//copy exif
System.arraycopy(srcExif, 0, newDestData, 2, srcExifLength);
//copy destData info except FFD8
System.arraycopy(destData, 2, newDestData, 2 + srcExifLength, destData.length - 2);
return newDestData;
}
return null;
}
public byte[] getExifOfJpeg() {
return mExifOfJpeg;
}
private void parserExif() {
try {
final int magicNumber = streamReader.getUInt16();
if (magicNumber == EXIF_MAGIC_NUMBER) {
mExifOfJpeg = getExifSegment();
}
} catch (IOException e) {
e.printStackTrace();
}
}
private byte[] getExifSegment() throws IOException {
short segmentId, segmentType;
int segmentLength;
while (true) {
segmentId = streamReader.getUInt8();
if (segmentId != SEGMENT_START_ID) {
Log.d(TAG, "[getExifSegment]: Unknown segmentId=" + segmentId);
return null;
}
segmentType = streamReader.getUInt8();
if (segmentType == SEGMENT_SOS) {
return null;
} else if (segmentType == MARKER_EOI) {
return null;
}
// Segment length includes bytes for segment length.
segmentLength = streamReader.getUInt16() - 2;
if (segmentType != EXIF_SEGMENT_TYPE) {
long skipped = streamReader.skip(segmentLength);
if (skipped != segmentLength) {
Log.d(TAG, "[getExifSegment]: Unable to skip enough data"
+ ", type: " + segmentType
+ ", wanted to skip: " + segmentLength
+ ", but actually skipped: " + skipped);
return null;
}
} else {
byte[] segmentData = new byte[segmentLength];
int read = streamReader.read(segmentData);
if (read != segmentLength) {
Log.d(TAG, "[getExifSegment]: Unable to read segment data"
+ ", type: " + segmentType
+ ", length: " + segmentLength
+ ", actually read: " + read);
return null;
} else {
byte[] block = new byte[2 + 2 + segmentLength];
block[0] = (byte) SEGMENT_START_ID;
block[1] = (byte) EXIF_SEGMENT_TYPE;
int length = segmentLength + 2;
block[2] = (byte) ((length >> 8) & 0xFF);
block[3] = (byte) (length & 0xFF);
System.arraycopy(segmentData, 0, block, 4, segmentLength);
return block;
}
}
}
}
private static class StreamReader {
private final InputStream is;
//motorola / big endian byte order
public StreamReader(InputStream is) {
this.is = is;
}
public int getUInt16() throws IOException {
return (is.read() << 8 & 0xFF00) | (is.read() & 0xFF);
}
public short getUInt8() throws IOException {
return (short) (is.read() & 0xFF);
}
public long skip(long total) throws IOException {
if (total < 0) {
return 0;
}
long toSkip = total;
while (toSkip > 0) {
long skipped = is.skip(toSkip);
if (skipped > 0) {
toSkip -= skipped;
} else {
// Skip has no specific contract as to what happens when you reach the end of
// the stream. To differentiate between temporarily not having more data and
// having finished the stream, we read a single byte when we fail to skip any
// amount of data.
int testEofByte = is.read();
if (testEofByte == -1) {
break;
} else {
toSkip--;
}
}
}
return total - toSkip;
}
public int read(byte[] buffer) throws IOException {
int toRead = buffer.length;
int read;
while (toRead > 0 && ((read = is.read(buffer, buffer.length - toRead, toRead)) != -1)) {
toRead -= read;
}
return buffer.length - toRead;
}
public int getByte() throws IOException {
return is.read();
}
}
}