summaryrefslogtreecommitdiffstats
path: root/photoviewer
diff options
context:
space:
mode:
authorPaul Westbrook <pwestbro@google.com>2012-09-20 09:38:32 -0700
committerPaul Westbrook <pwestbro@google.com>2012-09-25 01:08:22 -0700
commit5211e4682a26be787e60b1c56f56b113a2fac26c (patch)
treef50c4eedac25acc310c31b3828aaf346ff004479 /photoviewer
parentb6641931653e19766025820fb78c7142e597545e (diff)
downloadandroid_frameworks_ex-5211e4682a26be787e60b1c56f56b113a2fac26c.tar.gz
android_frameworks_ex-5211e4682a26be787e60b1c56f56b113a2fac26c.tar.bz2
android_frameworks_ex-5211e4682a26be787e60b1c56f56b113a2fac26c.zip
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
Diffstat (limited to 'photoviewer')
-rw-r--r--photoviewer/src/com/android/ex/photo/util/Exif.java137
-rw-r--r--photoviewer/src/com/android/ex/photo/util/ImageUtils.java376
2 files changed, 343 insertions, 170 deletions
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) {
+ }
+ }
+ }
+}