Exif是一种图像文件格式,可以记录数码照片的属性信息和拍摄数据;
实际上Exif格式就是在JPEG格式头部插入了数码照片的信息,包括拍摄时的相机品牌、型号、光圈、焦距、白平衡等相机硬件信息和图片参数信息。
主要包括以下几类信息:
- 拍摄日期
- 拍摄器材(机身、镜头、闪光灯等)
- 拍摄参数(快门速度、光圈F值、ISO速度、焦距、测光模式等)
- 图像处理参数(锐化、对比度、饱和度、白平衡等)
- 图像描述及版权信息
- GPS定位数据
- 缩略图
例如:
项目 | 信息 |
---|---|
制造厂商 | Canon |
相机型号 | Canon EOS-1Ds Mark III |
曝光时间 | 0.00800 (1/125) sec |
光圈值 | F22 |
闪光灯 | 关闭 |
… | … |
… | … |
… | … |
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
--------------------------------------------------------------------------------------------------------------------------
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);
在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();
}
}
}