Android相機拍照方向旋轉的解決方案:ExifInterface

一、 碰到的問題

寫這篇文章的動機源自于這波迭代中碰到的一個問題:

在IM拍照時,在三星s7 eadge上拍完照片后從sd上拿到的地址設置給Imageview后顯示時,圖片旋轉了90度。But我拍照的時候明明是豎著拍的,相冊預覽也是豎著的,為什么拿到圖片后就成了橫著的?
對比了另一臺手機錘子堅果U1,沒這個問題,因此懷疑是跟相機的機型相關。

想到的解決方案:

把讀取到的圖片作為一個bitmap放在一個畫布上,然后旋轉畫布來控制圖片展示的方向。

問題來了:

這樣確實解決了三星上的問題,但是原來沒問題的機型上——歪了。

至此,問題明確了:我如何拿到的圖片的實際方向?

二、Exif

借助強大的Google,搜到了一個叫做Exif的東西,它是什么呢?

維基百科如是說:

EXIF:可交換圖像文件格式(英語:Exchangeable image file format,官方簡稱Exif), 是專門為數(shù)碼相機的照片設定的,可以記錄數(shù)碼照片的屬性信息和拍攝數(shù)據(jù)。

包括:分辨率,旋轉方向,感光度、白平衡、拍攝的光圈、焦距、分辨率、相機品牌、型號、GPS等信息。

Exif可以附加于JPEG、TIFF、RIFF等文件之中,為其增加有關數(shù)碼相機拍攝信息的內容和索引圖或圖像處理軟件的版本信息。

下圖是維基百科提供的一個exif圖片:

image

問題來了:

知道了Exif這個信息,對我有什么用?

可以看到在exif中有一個叫做圖像方向的東西,那是否可以借助這個屬性來解決我的問題呢?

三、ExifInterface 源碼解析

求助強大的Google 爸爸:

exif android

爸爸給我呈現(xiàn)了如下結果:

  1. ExifInterface 官方文檔
  2. ExifInterface 支持庫簡介

有了這兩個文檔我的問題迎刃而解。

看看ExifInterface是毛:

乍一看這是個接口挺迷的,點進文檔一看是個class。

ExifInterface是Android為我們提供的一個支持庫,隨著 25.1.0 支持庫的發(fā)布,支持庫大家庭迎來了一名新成員:ExifInterface 支持庫。由于 Android 7.1 引入了對框架 ExifInterface 的重大改進,最低可以支持到API 9+。

在build.gradle文件中引入下面的代碼,便可以使用ExifInterface了:

implementation 'com.android.support:exifinterface:27.1.1'

如何使用ExifInterface解決我的問題,定位到它的源碼,可以看到它為我們提供了3個構造方法:

    /**
     * 從給定的圖片路徑中讀取圖片的exif tag信息.
     */
    public ExifInterface(String filename) throws IOException {
        ......
        try {
            ......
            loadAttributes(in);
        } finally {
            IoUtils.closeQuietly(in);
        }
    }

    /**
     * 從指定的圖像文件描述符中讀取Exif標簽. 屬性突變僅支持可寫和可搜索的文件描述符. 此構造函數(shù)不會倒回給定文件描述符的偏移量。開發(fā)人員在使用后應關閉文件描述符。
     */
    public ExifInterface(FileDescriptor fileDescriptor) throws IOException {
        ......
        try {
            in = new FileInputStream(fileDescriptor);
            loadAttributes(in);
        } finally {
            IoUtils.closeQuietly(in);
        }
    }

    /**
     * 從給定的輸入流中讀取圖片的exif 信息. 對文件輸入流的屬性圖片不支持. 開發(fā)者在使用完之后應該關閉輸入流.
     */
    public ExifInterface(InputStream inputStream) throws IOException {
        ......
        loadAttributes(inputStream);
    }

可以看到的是在這三個構造方法里無一例外的都調用了loadAttributes(inputstream)方法。

接下來跟蹤到loadAttributes方法中:

 /**
     * This function decides which parser to read the image data according to the given input stream
     * type and the content of the input stream. In each case, it reads the first three bytes to
     * determine whether the image data format is JPEG or not.
     */
    private void loadAttributes(@NonNull InputStream in) throws IOException {
        try {
            // Initialize mAttributes.
            for (int i = 0; i < EXIF_TAGS.length; ++i) {
                mAttributes[i] = new HashMap();
            }

            // Process RAW input stream
            if (mAssetInputStream != null) {
                long asset = mAssetInputStream.getNativeAsset();
                if (handleRawResult(nativeGetRawAttributesFromAsset(asset))) {
                    return;
                }
            } else if (mSeekableFileDescriptor != null) {
                if (handleRawResult(nativeGetRawAttributesFromFileDescriptor(
                        mSeekableFileDescriptor))) {
                    return;
                }
            } else {
                in = new BufferedInputStream(in, JPEG_SIGNATURE_SIZE);
                if (!isJpegInputStream((BufferedInputStream) in) && handleRawResult(
                        nativeGetRawAttributesFromInputStream(in))) {
                    return;
                }
            }

            // Process JPEG input stream
            getJpegAttributes(in);
            mIsSupportedFile = true;
        } catch (IOException e) {
            // Ignore exceptions in order to keep the compatibility with the old versions of
            // ExifInterface.
            mIsSupportedFile = false;
            Log.w(TAG, "Invalid image: ExifInterface got an unsupported image format file"
                    + "(ExifInterface supports JPEG and some RAW image formats only) "
                    + "or a corrupted JPEG file to ExifInterface.", e);
        } finally {
            addDefaultValuesForCompatibility();

            if (DEBUG) {
                printAttributes();
            }
        }
    }

從注釋得到如下信息:

  1. 這個方法根據(jù)輸入的數(shù)據(jù)流類型和數(shù)據(jù)流內容來決定使用哪種類型的解析器來解析這個流數(shù)據(jù)。不論在哪一種類型的中,它都會讀取前3個字節(jié)的數(shù)據(jù)來決定這是否是JPEG格式的圖片。

    換而言之——只有JPEG格式的圖片才會攜帶exif數(shù)據(jù),像PNG,WebP這類的圖片就不會有這些數(shù)據(jù)。

  2. 如果是JPEG類型的數(shù)據(jù)會將mIsSupportedFile 設置為true,并且調用getJpegAttributes(in)方法類獲取JPEG中屬性信息

  3. 在try-catch的finally方法中調用了addDefaultValuesForCompatibility()方法,這個方法會為每個JPEG格式的圖片添加默認的屬性。

瞜一眼addDefaultValuesForCompatibility的代碼:

 private void addDefaultValuesForCompatibility() {
        // The value of DATETIME tag has the same value of DATETIME_ORIGINAL tag.
        String valueOfDateTimeOriginal = getAttribute(TAG_DATETIME_ORIGINAL);
        if (valueOfDateTimeOriginal != null) {
            mAttributes[IFD_TIFF_HINT].put(TAG_DATETIME,
                    ExifAttribute.createString(valueOfDateTimeOriginal));
        }

        // Add the default value.
        if (getAttribute(TAG_IMAGE_WIDTH) == null) {
            mAttributes[IFD_TIFF_HINT].put(TAG_IMAGE_WIDTH,
                    ExifAttribute.createULong(0, mExifByteOrder));
        }
        if (getAttribute(TAG_IMAGE_LENGTH) == null) {
            mAttributes[IFD_TIFF_HINT].put(TAG_IMAGE_LENGTH,
                    ExifAttribute.createULong(0, mExifByteOrder));
        }
        if (getAttribute(TAG_ORIENTATION) == null) {
            mAttributes[IFD_TIFF_HINT].put(TAG_ORIENTATION,
                    ExifAttribute.createULong(0, mExifByteOrder));
        }
        if (getAttribute(TAG_LIGHT_SOURCE) == null) {
            mAttributes[IFD_EXIF_HINT].put(TAG_LIGHT_SOURCE,
                    ExifAttribute.createULong(0, mExifByteOrder));
        }
    }

這段代碼可以得到如下信息:

對于每一張JPEG圖片都會添加默認的屬性信息,包含:

  • 圖片的寬、高:TAG_IMAGE_WIDTH、TAG_IMAGE_LENGTH
  • 圖片的方向:TAG_ORIENTATION ,它的值大致有如下幾個:
  1. ORIENTATION_FLIP_HORIZONTAL
  2. ORIENTATION_FLIP_VERTICAL
  1. ORIENTATION_NORMAL
  1. ORIENTATION_ROTATE_180
  1. ORIENTATION_ROTATE_270
  1. ORIENTATION_ROTATE_90
  1. ORIENTATION_TRANSPOSE
  1. ORIENTATION_TRANSVERSE
  1. ORIENTATION_UNDEFINED
  • 圖片光源:TAG_LIGHT_SOURCE(我猜的,不一定對)

四、解決我的問題

源碼讀到這里,已經了然了:我只要拿到當前圖片的orientation,如果有旋轉那么給它轉一下,就可以了。

接下來的問題:

如何拿到圖片的方向?
從文檔里看到,ExifInterface為我們提供了如下方法:

  1. getAttribute(String tag)
  2. getAttributeDouble(String tag, double defaultValue)
  3. getAttributeInt(String tag, int defaultValue)

下面給出這個問題的解決方案,步驟如下:

  1. 根據(jù)選中的圖片路徑獲取ExifInterface;
  2. 從 ExifInterface中獲取到當前圖片的旋轉方向;
  3. 把對應路徑的圖片Bitmap映射到一個畫布上
  4. 通過Matrix旋轉畫布,解決方向的問題。
Matrix mat = new Matrix();
Bitmap bitmap = BitmapFactory.decodeFile(path, options);
ExifInterface ei = new ExifInterface(path);
int orientation = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
    case ExifInterface.ORIENTATION_ROTATE_90:
        mat.postRotate(90);
        break;
    case ExifInterface.ORIENTATION_ROTATE_180:
        mat.postRotate(180);
        break;
}
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), mat, true);

至此我的問題就解決了。

五、看我家萌汪的Exif信息:

先上圖 :

image

你可能會很好奇,為毛這個圖片是轉了個呢?沒錯,這就是用三星手機拍的照片。

來讀取下它的信息:

ExifInterface exifInterface = new ExifInterface(path);

String orientation = exifInterface.getAttribute(ExifInterface.TAG_ORIENTATION);
String dateTime = exifInterface.getAttribute(ExifInterface.TAG_DATETIME);
String make = exifInterface.getAttribute(ExifInterface.TAG_MAKE);
String model = exifInterface.getAttribute(ExifInterface.TAG_MODEL);
String flash = exifInterface.getAttribute(ExifInterface.TAG_FLASH);
String imageLength = exifInterface.getAttribute(ExifInterface.TAG_IMAGE_LENGTH);
String imageWidth = exifInterface.getAttribute(ExifInterface.TAG_IMAGE_WIDTH);
String latitude = exifInterface.getAttribute(ExifInterface.TAG_GPS_LATITUDE);
String longitude = exifInterface.getAttribute(ExifInterface.TAG_GPS_LONGITUDE);
String latitudeRef = exifInterface.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF);
String longitudeRef = exifInterface.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF);
String exposureTime = exifInterface.getAttribute(ExifInterface.TAG_EXPOSURE_TIME);
String aperture = exifInterface.getAttribute(ExifInterface.TAG_APERTURE);
String isoSpeedRatings = exifInterface.getAttribute(ExifInterface.TAG_ISO);
String dateTimeDigitized = exifInterface.getAttribute(ExifInterface.TAG_DATETIME_DIGITIZED);
String subSecTime = exifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME);
String subSecTimeOrig = exifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIG);
String subSecTimeDig = exifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME_DIG);
String altitude = exifInterface.getAttribute(ExifInterface.TAG_GPS_ALTITUDE);
String altitudeRef = exifInterface.getAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF);
String gpsTimeStamp = exifInterface.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP);
String gpsDateStamp = exifInterface.getAttribute(ExifInterface.TAG_GPS_DATESTAMP);
String whiteBalance = exifInterface.getAttribute(ExifInterface.TAG_WHITE_BALANCE);
String focalLength = exifInterface.getAttribute(ExifInterface.TAG_FOCAL_LENGTH);
String processingMethod = exifInterface.getAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD);

Log.e("TAG", "## orientation=" + orientation);
Log.e("TAG", "## dateTime=" + dateTime);
Log.e("TAG", "## make=" + make);
Log.e("TAG", "## model=" + model);
Log.e("TAG", "## flash=" + flash);
Log.e("TAG", "## imageLength=" + imageLength);
Log.e("TAG", "## imageWidth=" + imageWidth);
Log.e("TAG", "## latitude=" + latitude);
Log.e("TAG", "## longitude=" + longitude);
Log.e("TAG", "## latitudeRef=" + latitudeRef);
Log.e("TAG", "## longitudeRef=" + longitudeRef);
Log.e("TAG", "## exposureTime=" + exposureTime);
Log.e("TAG", "## aperture=" + aperture);
Log.e("TAG", "## isoSpeedRatings=" + isoSpeedRatings);
Log.e("TAG", "## dateTimeDigitized=" + dateTimeDigitized);
Log.e("TAG", "## subSecTime=" + subSecTime);
Log.e("TAG", "## subSecTimeOrig=" + subSecTimeOrig);
Log.e("TAG", "## subSecTimeDig=" + subSecTimeDig);
Log.e("TAG", "## altitude=" + altitude);
Log.e("TAG", "## altitudeRef=" + altitudeRef);
Log.e("TAG", "## gpsTimeStamp=" + gpsTimeStamp);
Log.e("TAG", "## gpsDateStamp=" + gpsDateStamp);
Log.e("TAG", "## whiteBalance=" + whiteBalance);
Log.e("TAG", "## focalLength=" + focalLength);
Log.e("TAG", "## processingMethod=" + processingMethod);

得到的log如下:

05-07 18:40:40.813 27181-27181/zhanggeng.www.exifdemo E/TAG: ## orientation=6
    ## dateTime=2018:04:21 14:32:41
    ## make=samsung
    ## model=SM-G9350
    ## flash=0
    ## imageLength=3024
    ## imageWidth=4032
    ## latitude=34/1,0/1,536875/10000
    ## longitude=109/1,0/1,97687/10000
    ## latitudeRef=N
    ## longitudeRef=E
    ## exposureTime=0.002544529262086514
    ## aperture=1.7
    ## isoSpeedRatings=50
    ## dateTimeDigitized=2018:04:21 14:32:41
    ## subSecTime=null
    ## subSecTimeOrig=null
    ## subSecTimeDig=null
    ## altitude=816000/1000
05-07 18:40:40.814 27181-27181/zhanggeng.www.exifdemo E/TAG: ## altitudeRef=0
    ## gpsTimeStamp=06:32:06
    ## gpsDateStamp=2018:04:30
    ## whiteBalance=0
    ## focalLength=420/100
    ## processingMethod=null

以上是這張照片的所有Exif信息,至于具體值是什么意思,我也不懂,借助HandShaker,來看一眼:

image

上面拿到的Exif屬性信息,其實就是上圖查看的屬性信息。

竟然可以看到我當時拍照的地點,豈不是暴露了我的行蹤,不怕:

在Android相機的設置中關閉“位置信息” 就看不到拍照的地點了。

參考鏈接:

  1. HandShaker錘子科技出品的一款在Mac上使用的Android文件管理器,很好用——免費的;
  2. ExifInterface 官方文檔
  3. ExifInterface 支持庫簡介
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,124評論 25 708
  • intent詳解(一)、intent詳解(二) 1、獲取圖片 1、相冊以隱氏intent的方式打開系統(tǒng)默認的圖庫,...
    i冰點閱讀 3,114評論 0 3
  • 什么是exif? Exif是一種圖像文件格式,它的數(shù)據(jù)存儲與JPEG格式是完全相同的。實際上Exif格式就是在JP...
    Android_馮星閱讀 8,798評論 2 1
  • 什么是恒溫花灑,顧名思義通過龍頭自帶的恒溫調節(jié)閥芯,在很短的時間內自動平衡冷水和熱水的水壓,以保持出水溫度的穩(wěn)定,...
    王昌挺閱讀 2,636評論 0 1
  • 7月6日,星期四,下雨, 今天是我第一天幫媽媽寫親子日記,因為昨晚我是在大伯睡的,早上和姐姐一去去北...
    許悅媽媽閱讀 293評論 0 1