关于如何从 URI 获取 Exif 数据的最终答案

A final answer on how to get Exif data from URI

此主题已在此处的许多问题中进行了讨论,结果大多不同,并且由于 API 更改和不同类型的 URI,没有明确的答案

我自己也没有答案,不过还是聊聊吧。 ExifInterface 有一个接受 filePath 的构造函数。这本身就很烦人,因为现在不鼓励依赖路径——你应该使用 Uris 和 ContentResolver。好的。

我们的 Uri 命名为 uri 可以从 onActivityResult 中的意图中检索(如果您使用 ACTION_GET_CONTENT 从图库中选择图片)或者可以是 Uri 我们以前有(如果你从相机中选择图片并调用 intent.putExtra(MediaStore.EXTRA_OUTPUT, uri))。

API<19

我们的 uri 可以有两种不同的架构:

据我所知,第二种情况应该用 ContentResolver 来处理,您可以用 Context.getContentResolver() 来处理。在任何情况下,以下 对我测试过的所有应用程序都有效

public static ExifInterface getPictureData(Context context, Uri uri) {
    String[] uriParts = uri.toString().split(":");
    String path = null;

    if (uriParts[0].equals("content")) {
        // we can use ContentResolver.
        // let’s query the DATA column which holds the path
        String col = MediaStore.Images.ImageColumns.DATA;
        Cursor c = context.getContentResolver().query(uri,
                new String[]{col},
                null, null, null);

        if (c != null && c.moveToFirst()) {
            path = c.getString(c.getColumnIndex(col));
            c.close();
            return new ExifInterface(path);
        }

    } else if (uriParts[0].equals("file")) {
        // it's easy to get the path
        path = uri.getEncodedPath();
        return new ExifInterface(path);
    }
    return null;
}

API19+

我的问题源于 Kitkat 之后的 content:// 个 URI。 Kitkat 引入了 Storage Access Framework(参见 here)以及一个新的意图 ACTION_OPEN_DOCUMENT 和一个平台选择器。不过据说

On Android 4.4 and higher, you have the additional option of using the ACTION_OPEN_DOCUMENT intent, which displays a picker UI controlled by the system that allows the user to browse all files that other apps have made available. From this single UI, the user can pick a file from any of the supported apps.

ACTION_OPEN_DOCUMENT is not intended to be a replacement for ACTION_GET_CONTENT. The one you should use depends on the needs of your app.

为了简单起见,假设我们可以使用旧的 ACTION_GET_CONTENT:它将触发一个选择器对话框,您可以在其中选择图库应用程序。

但是,内容方法不再适用。例如,有时它在 Kitkat 上有效,但 永远不会在 Lollipop 上有效。我不知道到底发生了什么变化。

我已经搜索并尝试了很多;另一种专门为 Kitkat 采取的方法是:

String wholeId = DocumentsContract.getDocumentId(uri);
String[] parts = wholeId.split(“:”);
String numberId = parts[1];

Cursor c = context.getContentResolver().query(
    // why external and not internal ?
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
    new String[]{ col },
    MediaStore.Images.Media._ID + “=?”,
    new String[]{ numberId },
    null);

这有时有效,但有时无效。具体来说,它在 wholeId 类似于 image:2839 时有效,但当 wholeId 只是一个数字时显然会中断。

您可以使用系统选择器尝试此操作(即使用 ACTION_OPEN_DOCUMENT 启动图库):如果您从“最近”中选择一张图片,它会起作用;如果您从“下载”中选择一张图片,它就会崩溃。

那怎么办?!

直接的答案是你不会,你不会在较新版本的 OS 中从内容 uris 中找到文件路径。可以说,并不是所有的内容uri都指向图片甚至文件。

这对我来说完全没问题,起初我努力避免这种情况。但是,如果我们不应该使用路径,我们应该如何使用 ExifInterface class?

我不明白现代应用程序是如何做到这一点的 - 查找方向和元数据是您立即面临的问题,并且 ContentResolver 不提供任何 API 在这个意义上。你有 ContentResolver.openFileDescriptor() 和类似的东西,但没有 APIs 来读取元数据(它确实在那个文件中)。可能有外部库从流中读取 Exif 东西,但我想知道 common/platform 解决这个问题的方法。

我在 google 的开源应用程序中搜索过类似的代码,但一无所获。

The following works with all apps I have tested, in any case:

只有当 Uri 恰好来自 MediaStore 时才有效。如果 Uri 恰好来自其他任何东西,它将失败。

The immediate answer is You don’t, you don’t find file paths from content uris in newer version of the OS. It could be said that not all content uris point to pictures or even files.

正确。我已经多次指出这一点,例如 here.

How are we supposed to use the ExifInterface class if we should not use paths?

你不知道。使用其他代码获取 EXIF headers。

There might be external libraries that read Exif stuff from a stream, but I’m wondering about the common/platform way to solve this.

使用外部库。

I have searched for similar code in google’s open source apps, but found nothing.

您会在 the Mms app 中找到一些。

更新:2020-01-10:使用 AndroidX 库中的 ExifInterface。支持使用InputStream读入EXIF数据,对于Uri标识的内容,可以通过ContentResolver.

获取InputStream

支持库中现在提供从内容 URI(实际上是 InputStream)获取 EXIF。 参见:https://android-developers.googleblog.com/2016/12/introducing-the-exifinterface-support-library.html

用一些示例代码扩展 alex.dorokhov 的答案。支持库是一个很好的方法。

build.gradle

dependencies {
...    
compile "com.android.support:exifinterface:25.0.1"
...
}

示例代码:

import android.support.media.ExifInterface;
...
try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) {
      ExifInterface exif = new ExifInterface(inputStream);
      int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
    } catch (IOException e) {
      e.printStackTrace();
    }

一旦我们开始针对 api 25(也许 24+ 也是一个问题)但仍然支持回到 api 19,在 android 7 如果我将 URI 传递给仅引用文件的相机,我们的应用程序将崩溃。因此,我必须创建一个 URI 来像这样传递给相机意图。

FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".fileprovider", tempFile);

存在的问题是无法将 URI 转换为真实文件路径(除非保留临时文件路径)。

不要使用 EXIF。您可以像这样从 Uri 获取图像的方向:

private static int getOrientation(Context context, Uri photoUri) {
    Cursor cursor = context.getContentResolver().query(photoUri,
            new String[]{MediaStore.Images.ImageColumns.ORIENTATION}, null, null, null);

    if (cursor.getCount() != 1) {
        cursor.close();
        return -1;
    }

    cursor.moveToFirst();
    int orientation = cursor.getInt(0);
    cursor.close();
    cursor = null;
    //orientation here can be 90, 180, 270!
}

Android10API30

从图片 URI 获取 Exif 数据

 public static Bitmap decodeBitmap( Context context, Uri imagePath) {
    Logger.d("decodeBitmap imagePath: " + imagePath.getPath());

    if (imagePath == null) {
        return null;
    }

    InputStream in;
    ExifInterface exif;
    Bitmap image = null;
    try {
        in = context.getContentResolver().openInputStream(imagePath);
        image = BitmapFactory.decodeStream(in);

        //Close input stream consumed for Bitmap decode
        in.close();

        // Open stream again for reading exif information for acquiring orientation details.
        // Use new input stream otherwise bitmap decode stream gets reset.
        in =  context.getContentResolver().openInputStream(imagePath);

        int orientation = ExifInterface.ORIENTATION_UNDEFINED;
        try {
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
                exif = new ExifInterface(in);
            }else{
                exif = new ExifInterface(imagePath.getPath());
            }
            orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);

        } catch (IOException e) {
            Logger.d("IOException: " + e.getMessage());
        }

        //if you need, can correct orientation issues for gallery pick camera images with following.
        Logger.d("decodeBitmap orientation: " + orientation);
        switch (orientation) {
            case ExifInterface.ORIENTATION_ROTATE_90:
            case ExifInterface.ORIENTATION_TRANSPOSE:
                image = rotateImage(image, ROTATE_90);
                break;
            case ExifInterface.ORIENTATION_ROTATE_180:
            case ExifInterface.ORIENTATION_FLIP_VERTICAL:
                image = rotateImage(image, ROTATE_180);
                break;
            case ExifInterface.ORIENTATION_ROTATE_270:
            case ExifInterface.ORIENTATION_TRANSVERSE:
                image = rotateImage(image, ROTATE_270);
                break;
            default:
                break;
        }
        in.close();
    }  catch (IOException e) {
        Logger.d("IOException", e.getMessage());
    }
     return image;
}

就我而言,我在使用 InputStream 获取方向时遇到问题。因此,我没有从 InputStream 获取 ExifInterface,而是使用了 FileDescriptor。

此解决方案无效:

val inputStream = contentResolver.openInputStream(uri)
val bitmap: Bitmap? = BitmapFactory.decodeStream(inputStream)
val exifInterface = inputStream?.let { ExifInterface(inputStream) }
inputStream?.close()

当我为 ExifInterface 打开单独的 InputStream 时,我得到了更好的结果(但我不喜欢那样):

val inputStream = contentResolver.openInputStream(uri)
val bitmap: Bitmap? = BitmapFactory.decodeStream(inputStream)
inputStream?.close()

val inputStream2 = contentResolver.openInputStream(uri)
val fileDescriptor = contentResolver.openFileDescriptor(uri, "r")?.fileDescriptor
val exifInterface = inputStream2?.let { ExifInterface(inputStream2) }
inputStream2?.close()

但我最终使用 FileDescriptor 构建 ExifInterface 的方法:

fun Context.getImageFromGallery(uri: Uri): Bitmap? {
    return try {
        val inputStream = contentResolver.openInputStream(uri)
        val bitmap: Bitmap? = BitmapFactory.decodeStream(inputStream)
        inputStream?.close()

        val fileDescriptor = contentResolver.openFileDescriptor(uri, "r")?.fileDescriptor
        val exifInterface = fileDescriptor?.let { ExifInterface(fileDescriptor) }

        return when (exifInterface?.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)) {
            ExifInterface.ORIENTATION_ROTATE_90 -> TransformationUtils.rotateImage(bitmap!!, 90)
            ExifInterface.ORIENTATION_ROTATE_180 -> TransformationUtils.rotateImage(bitmap!!, 180)
            ExifInterface.ORIENTATION_ROTATE_270 -> TransformationUtils.rotateImage(bitmap!!, 270)
            ExifInterface.ORIENTATION_NORMAL -> bitmap
            else -> bitmap
        }
    } catch (e: java.lang.Exception) {
        e.printStackTrace()
        null
    }
}