From 5211e4682a26be787e60b1c56f56b113a2fac26c Mon Sep 17 00:00:00 2001 From: Paul Westbrook Date: Thu, 20 Sep 2012 09:38:32 -0700 Subject: Add Exif parser Class to parse exif headers of images to detect orientation Modify ImageUtils#decodeStream to parse the exif header and respect orientation Bug: 7087307 Change-Id: I93c3a69a50ae26d3fd4e96944439f87e9d511945 --- .../src/com/android/ex/photo/util/Exif.java | 137 ++++++++ .../src/com/android/ex/photo/util/ImageUtils.java | 376 +++++++++++---------- 2 files changed, 343 insertions(+), 170 deletions(-) create mode 100644 photoviewer/src/com/android/ex/photo/util/Exif.java (limited to 'photoviewer') diff --git a/photoviewer/src/com/android/ex/photo/util/Exif.java b/photoviewer/src/com/android/ex/photo/util/Exif.java new file mode 100644 index 0000000..743b896 --- /dev/null +++ b/photoviewer/src/com/android/ex/photo/util/Exif.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ex.photo.util; + +import android.util.Log; + +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; + } +} diff --git a/photoviewer/src/com/android/ex/photo/util/ImageUtils.java b/photoviewer/src/com/android/ex/photo/util/ImageUtils.java index d852d65..7fe971a 100644 --- a/photoviewer/src/com/android/ex/photo/util/ImageUtils.java +++ b/photoviewer/src/com/android/ex/photo/util/ImageUtils.java @@ -1,175 +1,211 @@ -/* - * Copyright (C) 2011 Google Inc. - * Licensed to The Android Open Source Project. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.ex.photo.util; - -import android.content.ContentResolver; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Point; -import android.graphics.Rect; -import android.net.Uri; -import android.os.Build; -import android.util.Log; - -import com.android.ex.photo.PhotoViewActivity; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; - -/** - * Image utilities - */ -public class ImageUtils { - // Logging - private static final String TAG = "ImageUtils"; - - /** Minimum class memory class to use full-res photos */ - private final static long MIN_NORMAL_CLASS = 32; - /** Minimum class memory class to use small photos */ - private final static long MIN_SMALL_CLASS = 24; - - public static enum ImageSize { - EXTRA_SMALL, - SMALL, - NORMAL, - } - - public static final ImageSize sUseImageSize; - static { - // On HC and beyond, assume devices are more capable - if (Build.VERSION.SDK_INT >= 11) { - sUseImageSize = ImageSize.NORMAL; - } else { - if (PhotoViewActivity.sMemoryClass >= MIN_NORMAL_CLASS) { - // We have plenty of memory; use full sized photos - sUseImageSize = ImageSize.NORMAL; - } else if (PhotoViewActivity.sMemoryClass >= MIN_SMALL_CLASS) { - // We have slight less memory; use smaller sized photos - sUseImageSize = ImageSize.SMALL; - } else { - // We have little memory; use very small sized photos - sUseImageSize = ImageSize.EXTRA_SMALL; - } - } - } - - /** - * @return true if the MimeType type is image - */ - public static boolean isImageMimeType(String mimeType) { - return mimeType != null && mimeType.startsWith("image/"); - } - - /** - * Create a bitmap from a local URI - * - * @param resolver The ContentResolver - * @param uri The local URI - * @param maxSize The maximum size (either width or height) - * - * @return The new bitmap or null - */ - public static Bitmap createLocalBitmap(ContentResolver resolver, Uri uri, int maxSize) { - InputStream inputStream = null; - try { - final BitmapFactory.Options opts = new BitmapFactory.Options(); - final Point bounds = getImageBounds(resolver, uri); - - inputStream = resolver.openInputStream(uri); - opts.inSampleSize = Math.max(bounds.x / maxSize, bounds.y / maxSize); - - final Bitmap decodedBitmap = decodeStream(inputStream, null, opts); - - // Correct thumbnail orientation as necessary +/* + * Copyright (C) 2011 Google Inc. + * Licensed to The Android Open Source Project. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ex.photo.util; + +import android.content.ContentResolver; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.graphics.Point; +import android.graphics.Rect; +import android.net.Uri; +import android.os.Build; +import android.util.DisplayMetrics; +import android.util.Log; + +import com.android.ex.photo.PhotoViewActivity; +import com.android.ex.photo.util.Exif; + +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +/** + * Image utilities + */ +public class ImageUtils { + // Logging + private static final String TAG = "ImageUtils"; + + /** Minimum class memory class to use full-res photos */ + private final static long MIN_NORMAL_CLASS = 32; + /** Minimum class memory class to use small photos */ + private final static long MIN_SMALL_CLASS = 24; + + public static enum ImageSize { + EXTRA_SMALL, + SMALL, + NORMAL, + } + + public static final ImageSize sUseImageSize; + static { + // On HC and beyond, assume devices are more capable + if (Build.VERSION.SDK_INT >= 11) { + sUseImageSize = ImageSize.NORMAL; + } else { + if (PhotoViewActivity.sMemoryClass >= MIN_NORMAL_CLASS) { + // We have plenty of memory; use full sized photos + sUseImageSize = ImageSize.NORMAL; + } else if (PhotoViewActivity.sMemoryClass >= MIN_SMALL_CLASS) { + // We have slight less memory; use smaller sized photos + sUseImageSize = ImageSize.SMALL; + } else { + // We have little memory; use very small sized photos + sUseImageSize = ImageSize.EXTRA_SMALL; + } + } + } + + /** + * @return true if the MimeType type is image + */ + public static boolean isImageMimeType(String mimeType) { + return mimeType != null && mimeType.startsWith("image/"); + } + + /** + * Create a bitmap from a local URI + * + * @param resolver The ContentResolver + * @param uri The local URI + * @param maxSize The maximum size (either width or height) + * + * @return The new bitmap or null + */ + public static Bitmap createLocalBitmap(ContentResolver resolver, Uri uri, int maxSize) { + InputStream inputStream = null; + try { + final BitmapFactory.Options opts = new BitmapFactory.Options(); + final Point bounds = getImageBounds(resolver, uri); + + inputStream = resolver.openInputStream(uri); + opts.inSampleSize = Math.max(bounds.x / maxSize, bounds.y / maxSize); + + final Bitmap decodedBitmap = decodeStream(inputStream, null, opts); + + // Correct thumbnail orientation as necessary // TODO: Fix rotation if it's actually a problem //return rotateBitmap(resolver, uri, decodedBitmap); return decodedBitmap; - - } catch (FileNotFoundException exception) { - // Do nothing - the photo will appear to be missing - } catch (IOException exception) { - // Do nothing - the photo will appear to be missing + + } catch (FileNotFoundException exception) { + // Do nothing - the photo will appear to be missing + } catch (IOException exception) { + // Do nothing - the photo will appear to be missing } catch (IllegalArgumentException exception) { // Do nothing - the photo will appear to be missing - } finally { - try { - if (inputStream != null) { - inputStream.close(); - } - } catch (IOException ignore) { - } - } - return null; - } - - /** - * Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect, - * BitmapFactory.Options)} that returns {@code null} on {@link - * OutOfMemoryError}. - * - * @param is The input stream that holds the raw data to be decoded into a - * bitmap. - * @param outPadding If not null, return the padding rect for the bitmap if - * it exists, otherwise set padding to [-1,-1,-1,-1]. If - * no bitmap is returned (null) then padding is - * unchanged. - * @param opts null-ok; Options that control downsampling and whether the - * image should be completely decoded, or just is size returned. - * @return The decoded bitmap, or null if the image data could not be - * decoded, or, if opts is non-null, if opts requested only the - * size be returned (in opts.outWidth and opts.outHeight) - */ - public static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) { - try { - return BitmapFactory.decodeStream(is, outPadding, opts); - } catch (OutOfMemoryError oome) { - Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome); - return null; - } - } - - /** - * Gets the image bounds - * - * @param resolver The ContentResolver - * @param uri The uri - * - * @return The image bounds - */ - private static Point getImageBounds(ContentResolver resolver, Uri uri) - throws IOException { - final BitmapFactory.Options opts = new BitmapFactory.Options(); - InputStream inputStream = null; - - try { - opts.inJustDecodeBounds = true; - inputStream = resolver.openInputStream(uri); - decodeStream(inputStream, null, opts); - - return new Point(opts.outWidth, opts.outHeight); - } finally { - try { - if (inputStream != null) { - inputStream.close(); - } - } catch (IOException ignore) { - } - } - } -} + } finally { + try { + if (inputStream != null) { + inputStream.close(); + } + } catch (IOException ignore) { + } + } + return null; + } + + /** + * Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect, + * BitmapFactory.Options)} that returns {@code null} on {@link + * OutOfMemoryError}. + * + * @param is The input stream that holds the raw data to be decoded into a + * bitmap. + * @param outPadding If not null, return the padding rect for the bitmap if + * it exists, otherwise set padding to [-1,-1,-1,-1]. If + * no bitmap is returned (null) then padding is + * unchanged. + * @param opts null-ok; Options that control downsampling and whether the + * image should be completely decoded, or just is size returned. + * @return The decoded bitmap, or null if the image data could not be + * decoded, or, if opts is non-null, if opts requested only the + * size be returned (in opts.outWidth and opts.outHeight) + */ + public static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) { + ByteArrayOutputStream out = null; + try { + out = new ByteArrayOutputStream(); + final byte[] buffer = new byte[4096]; + int n = is.read(buffer); + while (n >= 0) { + out.write(buffer, 0, n); + n = is.read(buffer); + } + final byte[] bitmapBytes = out.toByteArray(); + + // Determine the orientation for this image + final int orientation = Exif.getOrientation(bitmapBytes); + final Bitmap originalBitmap = + BitmapFactory.decodeByteArray(bitmapBytes, 0, bitmapBytes.length, opts); + + if (originalBitmap != null && orientation != 0) { + final Matrix matrix = new Matrix(); + matrix.postRotate(orientation); + return Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(), + originalBitmap.getHeight(), matrix, true); + } + return originalBitmap; + } catch (OutOfMemoryError oome) { + Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome); + return null; + } catch (IOException ioe) { + Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an IOE", ioe); + return null; + } finally { + if (out != null) { + try { + out.close(); + } catch (IOException e) { + // Do nothing + } + } + } + } + + /** + * Gets the image bounds + * + * @param resolver The ContentResolver + * @param uri The uri + * + * @return The image bounds + */ + private static Point getImageBounds(ContentResolver resolver, Uri uri) + throws IOException { + final BitmapFactory.Options opts = new BitmapFactory.Options(); + InputStream inputStream = null; + + try { + opts.inJustDecodeBounds = true; + inputStream = resolver.openInputStream(uri); + decodeStream(inputStream, null, opts); + + return new Point(opts.outWidth, opts.outHeight); + } finally { + try { + if (inputStream != null) { + inputStream.close(); + } + } catch (IOException ignore) { + } + } + } +} -- cgit v1.2.3