Exif 格式介绍和操作

Exif 是什么

Exif是一种图像文件格式,可以记录数码照片的属性信息和拍摄数据;
实际上Exif格式就是在JPEG格式头部插入了数码照片的信息,包括拍摄时的相机品牌、型号、光圈、焦距、白平衡等相机硬件信息和图片参数信息。

主要包括以下几类信息:

  1. 拍摄日期
  2. 拍摄器材(机身、镜头、闪光灯等)
  3. 拍摄参数(快门速度、光圈F值、ISO速度、焦距、测光模式等)
  4. 图像处理参数(锐化、对比度、饱和度、白平衡等)
  5. 图像描述及版权信息
  6. GPS定位数据
  7. 缩略图

例如:

项目 信息
制造厂商 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();
        }
    }
}

参考:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章