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();
        }
    }
}

參考:

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