月度归档:2015年04月

Exif在Android上的应用

项目碰到了一个问题:在iOS设备上上传的图片,在Android设备上查看会发现被旋转。经过调查发现原因是iOS上传的照片包含Exif信息,正常的显示应该是在原图片的基础上,根据Exif旋转信息先进行调整再显示即可。

那么,什么是Exif呢?

维基百科:

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

具体可查阅:Exif

Exif一个针对图片文件的跨平台标准,包含了一些列图片的信息,插入到JPEG等文件头部。Exif信息中包含Orientation (rotation)这一项,即图片的方向信息,上面提到的问题产生根源就在于这个属性。

不同的端或者系统对Exif的支持情况

文章开头提到的问题产生的原因就是,iOS上传了包含旋转信息的图片到服务端,而Android端从服务端下载下来图片后,如果没根据Exif中的方向信息进行旋转,就会发生旋转问题,也即:在Android上头像是被旋转的,反而恰恰是因为Android 客户端在显示的时候没有根据Exif的方向信息进行旋转校正。

经过一些简单的测试和反馈发现,iOS设备图片浏览器、Mac端Chrome浏览器等均可显示正确方向的图片,也即这些应用根据Exif信息对图片显示进行了处理。

Android端的话,经过确认如果使用BitmapFactory的一系列decode方法、或者使用Bitmap的一系列createBitmap方法,得到的bitmap并没有根据图片的Exif旋转信息进行校正。Android提供的API(ImageView、BitmapFactory、Bitmap等)或者Android系统层并没有处理Exif的方向信息,所以Android客户端直接decode从服务端下载来的图片byte[]数据为bitmap,会发生头像“被旋转”的现象。

Android项目对Exif方向信息进行支持(解决旋转问题)

Android API提供了ExifInterface获取图片信息的Exif方向信息,使用如下:

ExifInterface exifInterface = new ExifInterface(filePath);   
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);

获取方向后可以进行适当的角度转换:

// Constants used for the Orientation Exif tag.
public static final int ORIENTATION_UNDEFINED = 0;
public static final int ORIENTATION_NORMAL = 1;
public static final int ORIENTATION_FLIP_HORIZONTAL = 2;  // left right reversed mirror
public static final int ORIENTATION_ROTATE_180 = 3;
public static final int ORIENTATION_FLIP_VERTICAL = 4;  // upside down mirror
public static final int ORIENTATION_TRANSPOSE = 5;  // flipped about top-left <--> bottom-right axis
public static final int ORIENTATION_ROTATE_90 = 6;  // rotate 90 cw to right it
public static final int ORIENTATION_TRANSVERSE = 7;  // flipped about top-right <--> bottom-left axis
public static final int ORIENTATION_ROTATE_270 = 8;  // rotate 270 to right it

然后再根据转换后的角度将bitmap方向校正了,这一步不赘述。

Matrix matrix = new Matrix();   
matrix.setRotate(degree);   
Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);

这里是存在一个问题的:即图片必须是手机本地已存储的文件,传入文件路径才可以使用ExifInterface。那么问题就来了,怎么直接从网下下载下来的文件流中读取Exif信息?

答案是:非常不幸,API没提供相关接口。

从图片文件流中读取Exif信息

的确是有一些方法。

1. 使用第三方库metadata-extractor

这个库算是非常符合要求,从文件byte[]读取到了以下信息:

04-13 15:50:39.759: D/EXIF LOG(5830): Directory: Jpeg
04-13 15:50:39.759: D/EXIF LOG(5830): > tag: Compression Type = Baseline
04-13 15:50:39.759: D/EXIF LOG(5830): > tag: Data Precision = 8 bits
04-13 15:50:39.759: D/EXIF LOG(5830): > tag: Image Height = 2448 pixels
04-13 15:50:39.759: D/EXIF LOG(5830): > tag: Image Width = 2449 pixels
04-13 15:50:39.759: D/EXIF LOG(5830): > tag: Number of Components = 3
04-13 15:50:39.760: D/EXIF LOG(5830): > tag: Component 1 = Y component: Quantization table 0, Sampling factors 2 horiz/2 vert
04-13 15:50:39.761: D/EXIF LOG(5830): > tag: Component 2 = Cb component: Quantization table 1, Sampling factors 1 horiz/1 vert
04-13 15:50:39.761: D/EXIF LOG(5830): > tag: Component 3 = Cr component: Quantization table 1, Sampling factors 1 horiz/1 vert
04-13 15:50:39.761: D/EXIF LOG(5830): Directory: Jfif
04-13 15:50:39.761: D/EXIF LOG(5830): > tag: Version = 1.1
04-13 15:50:39.761: D/EXIF LOG(5830): > tag: Resolution Units = none
04-13 15:50:39.762: D/EXIF LOG(5830): > tag: X Resolution = 65535 dots
04-13 15:50:39.762: D/EXIF LOG(5830): > tag: Y Resolution = 65535 dots
04-13 15:50:39.762: D/EXIF LOG(5830): Directory: Exif SubIFD
04-13 15:50:39.762: D/EXIF LOG(5830): > tag: Color Space = sRGB
04-13 15:50:39.762: D/EXIF LOG(5830): > tag: Exif Image Width = 2449 pixels
04-13 15:50:39.762: D/EXIF LOG(5830): > tag: Exif Image Height = 2448 pixels
04-13 15:50:39.762: D/EXIF LOG(5830): Directory: Exif IFD0
04-13 15:50:39.762: D/EXIF LOG(5830): > tag: Orientation = Right side, top (Rotate 90 CW)
04-13 15:50:39.762: D/EXIF LOG(5830): > tag: X Resolution = 2147483647 dots per inch
04-13 15:50:39.762: D/EXIF LOG(5830): > tag: Y Resolution = 2147483647 dots per inch
04-13 15:50:39.762: D/EXIF LOG(5830): > tag: Resolution Unit = Inch
04-13 15:50:39.762: D/EXIF LOG(5830): Directory: Photoshop
04-13 15:50:39.762: D/EXIF LOG(5830): > tag: IPTC-NAA Record = 0 bytes binary data
04-13 15:50:39.762: D/EXIF LOG(5830): > tag: Caption Digest = -44 29 -116 -39 -113 0 -78 4 -23 -128 9 -104 -20 -8 66 126
04-13 15:50:39.762: D/EXIF LOG(5830): Directory: Iptc

2. 从文件流中仅获取Exif方向信息。

第1种方法需要引入第三方库,并且大多数时候我们只是处理方向信息而已,所以还有一种简化方法,仅用于获取Exif中的方向信息,传送门(源码亦会附在本文最后):Exif.java

通过getOrientation(byte[] jpeg)方法即可轻松获得图片的方向信息,顺手校正一下再进行显示,完美解决。

附Exif.java源码:

public class Exif {
    private static final String TAG = "CameraExif";

    // Returns the degrees in clockwise. Values are 0, 90, 180, or 270.
    public static int getOrientation(byte[] jpeg) {
        if (jpeg == null) {
            return 0;
        }

        int offset = 0;
        int length = 0;

        // ISO/IEC 10918-1:1993(E)
        while (offset + 3 < jpeg.length && (jpeg[offset++] & 0xFF) == 0xFF) {
            int marker = jpeg[offset] & 0xFF;

            // Check if the marker is a padding.
            if (marker == 0xFF) {
                continue;
            }
            offset++;

            // Check if the marker is SOI or TEM.
            if (marker == 0xD8 || marker == 0x01) {
                continue;
            }
            // Check if the marker is EOI or SOS.
            if (marker == 0xD9 || marker == 0xDA) {
                break;
            }

            // Get the length and check if it is reasonable.
            length = pack(jpeg, offset, 2, false);
            if (length < 2 || offset + length > jpeg.length) {
                Log.e(TAG, "Invalid length");
                return 0;
            }

            // Break if the marker is EXIF in APP1.
            if (marker == 0xE1 && length >= 8 &&
                    pack(jpeg, offset + 2, 4, false) == 0x45786966 &&
                    pack(jpeg, offset + 6, 2, false) == 0) {
                offset += 8;
                length -= 8;
                break;
            }

            // Skip other markers.
            offset += length;
            length = 0;
        }

        // JEITA CP-3451 Exif Version 2.2
        if (length > 8) {
            // Identify the byte order.
            int tag = pack(jpeg, offset, 4, false);
            if (tag != 0x49492A00 && tag != 0x4D4D002A) {
                Log.e(TAG, "Invalid byte order");
                return 0;
            }
            boolean littleEndian = (tag == 0x49492A00);

            // Get the offset and check if it is reasonable.
            int count = pack(jpeg, offset + 4, 4, littleEndian) + 2;
            if (count < 10 || count > length) {
                Log.e(TAG, "Invalid offset");
                return 0;
            }
            offset += count;
            length -= count;

            // Get the count and go through all the elements.
            count = pack(jpeg, offset - 2, 2, littleEndian);
            while (count-- > 0 && length >= 12) {
                // Get the tag and check if it is orientation.
                tag = pack(jpeg, offset, 2, littleEndian);
                if (tag == 0x0112) {
                    // We do not really care about type and count, do we?
                    int orientation = pack(jpeg, offset + 8, 2, littleEndian);
                    switch (orientation) {
                        case 1:
                            return 0;
                        case 3:
                            return 180;
                        case 6:
                            return 90;
                        case 8:
                            return 270;
                    }
                    Log.i(TAG, "Unsupported orientation");
                    return 0;
                }
                offset += 12;
                length -= 12;
            }
        }

        Log.i(TAG, "Orientation not found");
        return 0;
    }

    private static int pack(byte[] bytes, int offset, int length,
            boolean littleEndian) {
        int step = 1;
        if (littleEndian) {
            offset += length - 1;
            step = -1;
        }

        int value = 0;
        while (length-- > 0) {
            value = (value << 8) | (bytes[offset] & 0xFF);
            offset += step;
        }
        return value;
    }
}