从图片回调中读取Android JPEG EXIF元数据

36

背景:我正在为一个即时通讯程序编写相机应用,但不能随时将拍摄的图像保存到持久磁盘。相机必须支持所有方向。我的实现方式是使用常见的Surfaceview示例,使用Display类来检测方向并旋转相机。在takePicture jpeg回调中,我从byte[]构造位图以解决一些纵横比问题:Camera API: Cross device issues

问题描述:在某些设备上,采用ROTATION_270(设备顺时针旋转90度)拍摄的构造位图上下颠倒了。到目前为止,似乎只有三星存在这种情况。我只能认为,可能相机焊接方式与其他设备不同,但这不是重点。虽然我可以检查位图是否侧着拍摄,但无法通过尺寸逻辑上检查是否上下颠倒,因此我需要访问EXIF数据。

Android提供了一个解析器http://developer.android.com/reference/android/media/ExifInterface.html,但不幸的是它只有一个接受文件的构造函数,而我没有也不想要一个文件。直觉上,我可以为一个字节数组编写构造函数,但由于它们调用本地代码,这似乎非常麻烦。http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.2.1_r1/android/media/ExifInterface.java

我的问题有两个部分:

  1. 是否有人知道byte[]数组是否包含完整的EXIF jpeg头数据,还是通过BitmapFactory.decode(...) / BitmapFactory.compress(...)添加了一些内容?

  2. 如果这个EXIF数据存在于字节数组中,如何以一种可靠的方式解析方向信息?

编辑 10/18/12

pcans的答案涉及到我问题的第二部分。如我在他的回答下面的评论中指出,如果要使用该解析器,您将需要将源代码合并到您的项目中。那篇SO帖子中提到的更改已经被进行了,并在此处重新发布:https://github.com/strangecargo/metadata-extractor

注意:较新版本的metadata-extractor可直接在Android上使用,无需修改,并可通过Maven获得。

然而,在第一部分中,当我用从takePicture获取的字节数组运行它时,解析器返回0个标记。我开始担心字节数组中没有我需要的数据。我将继续研究这个问题,但欢迎任何进一步的见解。


我已经为Android开发了一个Java元数据库,可以提取和插入像JPEG、TIFF、PNG、GIF等图像的元数据(Exif、XMP、IPTC、ICC_Profile、Photoshop IRB等)。请在这里查看。 - dragon66
有关在Android图像文件中读写EXIF的文章 - Shailendra
那个 metadata-extractor 的链接需要加上 .com:https://drewnoakes.com/code/exif/ - user4851
8个回答

34

要从图像的 byte[] 中读取元数据/EXIF(对于 Camera.takePicture() 非常有用),使用 Drew NoakesJava 元数据提取库 2.9.1 版本:

try
{
    // Extract metadata.
    Metadata metadata = ImageMetadataReader.readMetadata(new BufferedInputStream(new ByteArrayInputStream(imageData)), imageData.length);

    // Log each directory.
    for(Directory directory : metadata.getDirectories())
    {
        Log.d("LOG", "Directory: " + directory.getName());

        // Log all errors.
        for(String error : directory.getErrors())
        {
            Log.d("LOG", "> error: " + error);
        }

        // Log all tags.
        for(Tag tag : directory.getTags())
        {
            Log.d("LOG", "> tag: " + tag.getTagName() + " = " + tag.getDescription());
        }
    }
}
catch(Exception e)
{
    // TODO: handle exception
}

要读取图像的EXIF 方向(而不是缩略图的方向):

try
{
    // Get the EXIF orientation.
    final ExifIFD0Directory exifIFD0Directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
    if(exifIFD0Directory.containsTag(ExifIFD0Directory.TAG_ORIENTATION))
    {
        final int exifOrientation = exifIFD0Directory.getInt(ExifIFD0Directory.TAG_ORIENTATION);

        /* Work on exifOrientation */
    }
    else
    {
        /* Not found */
    }
}
catch(Exception e)
{
    // TODO: handle exception
}

定位从1到8。请参见这里, 这里, 这里这里

根据EXIF方向信息转换位图:

try
{
    final Matrix bitmapMatrix = new Matrix();
    switch(exifOrientation)
    {
        case 1:                                                                                     break;  // top left
        case 2:                                                 bitmapMatrix.postScale(-1, 1);      break;  // top right
        case 3:         bitmapMatrix.postRotate(180);                                               break;  // bottom right
        case 4:         bitmapMatrix.postRotate(180);           bitmapMatrix.postScale(-1, 1);      break;  // bottom left
        case 5:         bitmapMatrix.postRotate(90);            bitmapMatrix.postScale(-1, 1);      break;  // left top
        case 6:         bitmapMatrix.postRotate(90);                                                break;  // right top
        case 7:         bitmapMatrix.postRotate(270);           bitmapMatrix.postScale(-1, 1);      break;  // right bottom
        case 8:         bitmapMatrix.postRotate(270);                                               break;  // left bottom
        default:                                                                                    break;  // Unknown
    }

    // Create new bitmap.
    final Bitmap transformedBitmap = Bitmap.createBitmap(imageBitmap, 0, 0, imageBitmap.getWidth(), imageBitmap.getHeight(), bitmapMatrix, false);
}
catch(Exception e)
{
    // TODO: handle exception
}

1
更多更新版本可以在Git存储库中找到:https://github.com/drewfarris/metadata-extractor - dwbrito
1
@Pang 这是针对主摄像头的,前置摄像头的旋转怎么处理? - Muhammad Umar
4
@dwbrito,实际上,这个项目的原始作者(也就是我)已经将项目迁移到了GitHub。你提供的链接已经数年没有更新,缺少了许多修复和改进。请前往https://github.com/drewnoakes/metadata-extractor/查看最新版本。 - Drew Noakes

20

坏消息:

Android Api遗憾地不允许您从 Stream 读取exif数据,只能从 File 中读取。
ExifInterface没有带有InputStream的构造函数。因此,您必须自己解析jpeg内容。

好消息:

在纯Java中存在此API。 您可以使用这个: https://drewnoakes.com/code/exif/
它是开源的,根据Apache Licence 2发布,并作为Maven包提供。

有一个带有InputStream的构造函数: public ExifReader(java.io.InputStream is)

您可以使用像这样的ByteArrayInputStream构建基于byte[]支持的InputStream

InputStream is = new ByteArrayInputStream(decodedBytes);

1
@AndrewG 这个库可以直接在Android上使用。之前有一些旧版本不兼容,但那已经是一段时间以前的事情了(就像你的问题一样--我为其他浏览这些内容的人提到这一点)。 - Drew Noakes
3
Android API 24 的 ExifInterface 新增了对 InputStream 的支持。来源:https://developer.android.com/reference/android/media/ExifInterface.html#ExifInterface(java.io.InputStream) - Ivo Renkema
2
链接到库已经失效。 - Display name
ExifInterface现在有一个InputStream构造函数,以及FileString文件名和FileDescriptor - hippietrail
ExifInterface从androidx适用于API 24以下的版本。 - Tamim Attafi
显示剩余4条评论

7
AndroidX ExifInterface支持从输入流读取EXIF信息:
implementation "androidx.exifinterface:exifinterface:1.1.0"

您可以将输入流直接传递到构造函数中,就像这样:
val exif = ExifInterface(inputStream)
val orientation =
        exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)

这是2020年的正确答案。不再需要外部库了。只需你的Java代码看起来有点像JavaScript... - CaptainCrunch
是的,它是可行的解决方案! val inputStream: InputStream? = contentResolver.openInputStream(uri) inputStream?.run { exif = ExifInterface(inputStream) } - Oleksandr Bodashko

4
如果您想要一种不那么依赖于URI来源的读取EXIF数据的方法,可以使用exif支持库并从流中读取。例如,这是我获取图像方向的方式。
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转换为真实文件路径(除了保留临时文件路径)。

4

使用我的修改和pcans的建议,我得到了图片数据,但这并不是我预期的结果。具体来说,不是所有设备都会给出方向信息。如果你选择跟随这个路径,请注意:

  • The "Android fixed" ExifReader library I point to is actually the edited 2.3.1 which is a few releasees old. The new examples on the website and in the source pertain to the newest 2.6.x where he changes the API significantly. Using the 2.3.1 interface, you can dump all EXIF data from a byte[] by doing the following:

            Metadata header;    
            try {
                ByteArrayInputStream bais= new ByteArrayInputStream(data);
                ExifReader reader = new ExifReader(bais);
                header = reader.extract();
                Iterator<Directory> iter = header.getDirectoryIterator();
                while(iter.hasNext()){
                   Directory d = iter.next();
                   Iterator<Tag> iterTag = d.getTagIterator();
                   while(iterTag.hasNext()){
                      Tag t = iterTag.next();
                      Log.e("DEBUG", "TAG: " + t.getTagName() + " : " + t.getDescription());
                   }
                }
            } catch (JpegProcessingException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (MetadataException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
    

如果你想要数字标签值,只需替换

t.getDescription()

使用

d.getInt(t.getTagType())
  • 尽管ExifReader有一个使用byte[]的构造函数,但我必须误解了它所期望的内容,因为如果我尝试直接使用数据数组,我只能在返回的目录中获得标签。

我的答案并没有多加什么,所以我接受pcans的答案。


3
如果您正在使用Glide库,您可以从InputStream中获取Exif方向信息:
InputStream is=getActivity().getContentResolver().openInputStream(originalUri);
int orientation=new ImageHeaderParser(is).getOrientation();

1
这种方法对我来说不起作用,因为当通过mCamera.takePicture(null, null, mPictureCallback)捕获图像时,在Camera.PictureCallback.onPictureTaken返回的字节数组中,方向为-1表示失败。 - Andrew

0
如果您有一个content://类型的Uri,Android提供了通过ContentResolver的API,无需使用外部库:
public static int getExifAngle(Context context, Uri uri) {
    int angle = 0;
    Cursor c = context.getContentResolver().query(uri,
            new String[] { MediaStore.Images.ImageColumns.ORIENTATION },
            null,
            null,
            null);

    if (c != null && c.moveToFirst()) {
        int col = c.getColumnIndex( MediaStore.Images.ImageColumns.ORIENTATION );
        angle = c.getInt(col);
        c.close();
    }
    return angle;
}

您还可以读取MediaStore.Images.ImageColumns中的任何其他值,例如纬度和经度。

目前这不能与file:/// URI一起使用,但可以轻松调整。


在一些设备上无法工作。对我来说返回 col = -1 - Aleks N.
@AleksN 这只有在图片以正确的标签添加到MediaStore中时才能工作。此外,它必须是内容URI而不是文件URI。无论如何,我已经改变了主意,我建议使用Exif阅读器并避免使用内容解析器查询。这适用于文件和内容URI。 - natario

0

对于任何可能感兴趣的人,以下是如何使用https://github.com/strangecargo/metadata-extractor中的2.3.1接口获取方向标签的方法。

Metadata header;
try {
    ByteArrayInputStream bais= new ByteArrayInputStream(data);
    ExifReader reader = new ExifReader(bais);
    header = reader.extract();
    Directory dir = header.getDirectory(ExifDirectory.class);
    if (dir.containsTag(ExifDirectory.TAG_ORIENTATION)) {
        Log.v(TAG, "tag_orientation exists: " + dir.getInt(ExifDirectory.TAG_ORIENTATION));
    }
    else {
        Log.v(TAG, "tag_orietation doesn't exist");
    }


} catch (JpegProcessingException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (MetadataException e) {
    e.printStackTrace();
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接