Android相机拍照方向旋转的解决方案:ExifInterface

一、 碰到的问题

写这篇文章的动机源自于这波迭代中碰到的一个问题:

在IM拍照时,在三星s7 eadge上拍完照片后从sd上拿到的地址设置给Imageview后显示时,图片旋转了90度。But我拍照的时候明明是竖着拍的,相册预览也是竖着的,为什么拿到图片后就成了横着的?
对比了另一台手机锤子坚果U1,没这个问题,因此怀疑是跟相机的机型相关。

想到的解决方案:

把读取到的图片作为一个bitmap放在一个画布上,然后旋转画布来控制图片展示的方向。

问题来了:

这样确实解决了三星上的问题,但是原来没问题的机型上——歪了。

至此,问题明确了:我如何拿到的图片的实际方向?

二、Exif

借助强大的Google,搜到了一个叫做Exif的东西,它是什么呢?

维基百科如是说:

EXIF:可交换图像文件格式(英语:Exchangeable image file format,官方简称Exif), 是专门为数码相机的照片设定的,可以记录数码照片的属性信息和拍摄数据。

包括:分辨率,旋转方向,感光度、白平衡、拍摄的光圈、焦距、分辨率、相机品牌、型号、GPS等信息。

Exif可以附加于JPEG、TIFF、RIFF等文件之中,为其增加有关数码相机拍摄信息的内容和索引图或图像处理软件的版本信息。

下图是维基百科提供的一个exif图片:

image

问题来了:

知道了Exif这个信息,对我有什么用?

可以看到在exif中有一个叫做图像方向的东西,那是否可以借助这个属性来解决我的问题呢?

三、ExifInterface 源码解析

求助强大的Google 爸爸:

exif android

爸爸给我呈现了如下结果:

  1. ExifInterface 官方文档
  2. ExifInterface 支持库简介

有了这两个文档我的问题迎刃而解。

看看ExifInterface是毛:

乍一看这是个接口挺迷的,点进文档一看是个class。

ExifInterface是Android为我们提供的一个支持库,随着 25.1.0 支持库的发布,支持库大家庭迎来了一名新成员: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标签. 属性突变仅支持可写和可搜索的文件描述符. 此构造函数不会倒回给定文件描述符的偏移量。开发人员在使用后应关闭文件描述符。
     */
    public ExifInterface(FileDescriptor fileDescriptor) throws IOException {
        ......
        try {
            in = new FileInputStream(fileDescriptor);
            loadAttributes(in);
        } finally {
            IoUtils.closeQuietly(in);
        }
    }

    /**
     * 从给定的输入流中读取图片的exif 信息. 对文件输入流的属性图片不支持. 开发者在使用完之后应该关闭输入流.
     */
    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. 这个方法根据输入的数据流类型和数据流内容来决定使用哪种类型的解析器来解析这个流数据。不论在哪一种类型的中,它都会读取前3个字节的数据来决定这是否是JPEG格式的图片。

    换而言之——只有JPEG格式的图片才会携带exif数据,像PNG,WebP这类的图片就不会有这些数据。

  2. 如果是JPEG类型的数据会将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. 根据选中的图片路径获取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 支持库简介
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,424评论 25 707
  • intent详解(一)、intent详解(二) 1、获取图片 1、相册以隐氏intent的方式打开系统默认的图库,...
    i冰点阅读 2,986评论 0 3
  • 什么是exif? Exif是一种图像文件格式,它的数据存储与JPEG格式是完全相同的。实际上Exif格式就是在JP...
    Android_冯星阅读 8,627评论 2 1
  • 什么是恒温花洒,顾名思义通过龙头自带的恒温调节阀芯,在很短的时间内自动平衡冷水和热水的水压,以保持出水温度的稳定,...
    王昌挺阅读 2,585评论 0 1
  • 7月6日,星期四,下雨, 今天是我第一天帮妈妈写亲子日记,因为昨晚我是在大伯睡的,早上和姐姐一去去北...
    许悦妈妈阅读 280评论 0 1