summaryrefslogtreecommitdiffstats
path: root/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/gallery3d/filtershow/cache/ImageLoader.java')
-rw-r--r--src/com/android/gallery3d/filtershow/cache/ImageLoader.java502
1 files changed, 502 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
new file mode 100644
index 000000000..b6c72fd9d
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
@@ -0,0 +1,502 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.filtershow.cache;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import com.adobe.xmp.XMPException;
+import com.adobe.xmp.XMPMeta;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.exif.ExifInterface;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.util.XmpUtilHelper;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+public final class ImageLoader {
+
+ private static final String LOGTAG = "ImageLoader";
+
+ public static final String JPEG_MIME_TYPE = "image/jpeg";
+ public static final int DEFAULT_COMPRESS_QUALITY = 95;
+
+ public static final int ORI_NORMAL = ExifInterface.Orientation.TOP_LEFT;
+ public static final int ORI_ROTATE_90 = ExifInterface.Orientation.RIGHT_TOP;
+ public static final int ORI_ROTATE_180 = ExifInterface.Orientation.BOTTOM_LEFT;
+ public static final int ORI_ROTATE_270 = ExifInterface.Orientation.RIGHT_BOTTOM;
+ public static final int ORI_FLIP_HOR = ExifInterface.Orientation.TOP_RIGHT;
+ public static final int ORI_FLIP_VERT = ExifInterface.Orientation.BOTTOM_RIGHT;
+ public static final int ORI_TRANSPOSE = ExifInterface.Orientation.LEFT_TOP;
+ public static final int ORI_TRANSVERSE = ExifInterface.Orientation.LEFT_BOTTOM;
+
+ private static final int BITMAP_LOAD_BACKOUT_ATTEMPTS = 5;
+
+ private ImageLoader() {}
+
+ /**
+ * Returns the Mime type for a Url. Safe to use with Urls that do not
+ * come from Gallery's content provider.
+ */
+ public static String getMimeType(Uri src) {
+ String postfix = MimeTypeMap.getFileExtensionFromUrl(src.toString());
+ String ret = null;
+ if (postfix != null) {
+ ret = MimeTypeMap.getSingleton().getMimeTypeFromExtension(postfix);
+ }
+ return ret;
+ }
+
+ /**
+ * Returns the image's orientation flag. Defaults to ORI_NORMAL if no valid
+ * orientation was found.
+ */
+ public static int getMetadataOrientation(Context context, Uri uri) {
+ if (uri == null || context == null) {
+ throw new IllegalArgumentException("bad argument to getOrientation");
+ }
+
+ // First try to find orientation data in Gallery's ContentProvider.
+ Cursor cursor = null;
+ try {
+ cursor = context.getContentResolver().query(uri,
+ new String[] { MediaStore.Images.ImageColumns.ORIENTATION },
+ null, null, null);
+ if (cursor != null && cursor.moveToNext()) {
+ int ori = cursor.getInt(0);
+ switch (ori) {
+ case 90:
+ return ORI_ROTATE_90;
+ case 270:
+ return ORI_ROTATE_270;
+ case 180:
+ return ORI_ROTATE_180;
+ default:
+ return ORI_NORMAL;
+ }
+ }
+ } catch (SQLiteException e) {
+ // Do nothing
+ } catch (IllegalArgumentException e) {
+ // Do nothing
+ } finally {
+ Utils.closeSilently(cursor);
+ }
+
+ // Fall back to checking EXIF tags in file.
+ if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
+ String mimeType = getMimeType(uri);
+ if (!JPEG_MIME_TYPE.equals(mimeType)) {
+ return ORI_NORMAL;
+ }
+ String path = uri.getPath();
+ ExifInterface exif = new ExifInterface();
+ try {
+ exif.readExif(path);
+ Integer tagval = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION);
+ if (tagval != null) {
+ int orientation = tagval;
+ switch(orientation) {
+ case ORI_NORMAL:
+ case ORI_ROTATE_90:
+ case ORI_ROTATE_180:
+ case ORI_ROTATE_270:
+ case ORI_FLIP_HOR:
+ case ORI_FLIP_VERT:
+ case ORI_TRANSPOSE:
+ case ORI_TRANSVERSE:
+ return orientation;
+ default:
+ return ORI_NORMAL;
+ }
+ }
+ } catch (IOException e) {
+ Log.w(LOGTAG, "Failed to read EXIF orientation", e);
+ }
+ }
+ return ORI_NORMAL;
+ }
+
+ /**
+ * Returns the rotation of image at the given URI as one of 0, 90, 180,
+ * 270. Defaults to 0.
+ */
+ public static int getMetadataRotation(Context context, Uri uri) {
+ int orientation = getMetadataOrientation(context, uri);
+ switch(orientation) {
+ case ORI_ROTATE_90:
+ return 90;
+ case ORI_ROTATE_180:
+ return 180;
+ case ORI_ROTATE_270:
+ return 270;
+ default:
+ return 0;
+ }
+ }
+
+ /**
+ * Takes an orientation and a bitmap, and returns the bitmap transformed
+ * to that orientation.
+ */
+ public static Bitmap orientBitmap(Bitmap bitmap, int ori) {
+ Matrix matrix = new Matrix();
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ if (ori == ORI_ROTATE_90 ||
+ ori == ORI_ROTATE_270 ||
+ ori == ORI_TRANSPOSE ||
+ ori == ORI_TRANSVERSE) {
+ int tmp = w;
+ w = h;
+ h = tmp;
+ }
+ switch (ori) {
+ case ORI_ROTATE_90:
+ matrix.setRotate(90, w / 2f, h / 2f);
+ break;
+ case ORI_ROTATE_180:
+ matrix.setRotate(180, w / 2f, h / 2f);
+ break;
+ case ORI_ROTATE_270:
+ matrix.setRotate(270, w / 2f, h / 2f);
+ break;
+ case ORI_FLIP_HOR:
+ matrix.preScale(-1, 1);
+ break;
+ case ORI_FLIP_VERT:
+ matrix.preScale(1, -1);
+ break;
+ case ORI_TRANSPOSE:
+ matrix.setRotate(90, w / 2f, h / 2f);
+ matrix.preScale(1, -1);
+ break;
+ case ORI_TRANSVERSE:
+ matrix.setRotate(270, w / 2f, h / 2f);
+ matrix.preScale(1, -1);
+ break;
+ case ORI_NORMAL:
+ default:
+ return bitmap;
+ }
+ return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
+ bitmap.getHeight(), matrix, true);
+ }
+
+ /**
+ * Returns the bitmap for the rectangular region given by "bounds"
+ * if it is a subset of the bitmap stored at uri. Otherwise returns
+ * null.
+ */
+ public static Bitmap loadRegionBitmap(Context context, Uri uri, BitmapFactory.Options options,
+ Rect bounds) {
+ InputStream is = null;
+ try {
+ is = context.getContentResolver().openInputStream(uri);
+ BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
+ Rect r = new Rect(0, 0, decoder.getWidth(), decoder.getHeight());
+ // return null if bounds are not entirely within the bitmap
+ if (!r.contains(bounds)) {
+ return null;
+ }
+ return decoder.decodeRegion(bounds, options);
+ } catch (FileNotFoundException e) {
+ Log.e(LOGTAG, "FileNotFoundException for " + uri, e);
+ } catch (IOException e) {
+ Log.e(LOGTAG, "FileNotFoundException for " + uri, e);
+ } finally {
+ Utils.closeSilently(is);
+ }
+ return null;
+ }
+
+ /**
+ * Returns the bounds of the bitmap stored at a given Url.
+ */
+ public static Rect loadBitmapBounds(Context context, Uri uri) {
+ BitmapFactory.Options o = new BitmapFactory.Options();
+ loadBitmap(context, uri, o);
+ return new Rect(0, 0, o.outWidth, o.outHeight);
+ }
+
+ /**
+ * Loads a bitmap that has been downsampled using sampleSize from a given url.
+ */
+ public static Bitmap loadDownsampledBitmap(Context context, Uri uri, int sampleSize) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inMutable = true;
+ options.inSampleSize = sampleSize;
+ return loadBitmap(context, uri, options);
+ }
+
+
+ /**
+ * Returns the bitmap from the given uri loaded using the given options.
+ * Returns null on failure.
+ */
+ public static Bitmap loadBitmap(Context context, Uri uri, BitmapFactory.Options o) {
+ if (uri == null || context == null) {
+ throw new IllegalArgumentException("bad argument to loadBitmap");
+ }
+ InputStream is = null;
+ try {
+ is = context.getContentResolver().openInputStream(uri);
+ return BitmapFactory.decodeStream(is, null, o);
+ } catch (FileNotFoundException e) {
+ Log.e(LOGTAG, "FileNotFoundException for " + uri, e);
+ } finally {
+ Utils.closeSilently(is);
+ }
+ return null;
+ }
+
+ /**
+ * Loads a bitmap at a given URI that is downsampled so that both sides are
+ * smaller than maxSideLength. The Bitmap's original dimensions are stored
+ * in the rect originalBounds.
+ *
+ * @param uri URI of image to open.
+ * @param context context whose ContentResolver to use.
+ * @param maxSideLength max side length of returned bitmap.
+ * @param originalBounds If not null, set to the actual bounds of the stored bitmap.
+ * @param useMin use min or max side of the original image
+ * @return downsampled bitmap or null if this operation failed.
+ */
+ public static Bitmap loadConstrainedBitmap(Uri uri, Context context, int maxSideLength,
+ Rect originalBounds, boolean useMin) {
+ if (maxSideLength <= 0 || uri == null || context == null) {
+ throw new IllegalArgumentException("bad argument to getScaledBitmap");
+ }
+ // Get width and height of stored bitmap
+ Rect storedBounds = loadBitmapBounds(context, uri);
+ if (originalBounds != null) {
+ originalBounds.set(storedBounds);
+ }
+ int w = storedBounds.width();
+ int h = storedBounds.height();
+
+ // If bitmap cannot be decoded, return null
+ if (w <= 0 || h <= 0) {
+ return null;
+ }
+
+ // Find best downsampling size
+ int imageSide = 0;
+ if (useMin) {
+ imageSide = Math.min(w, h);
+ } else {
+ imageSide = Math.max(w, h);
+ }
+ int sampleSize = 1;
+ while (imageSide > maxSideLength) {
+ imageSide >>>= 1;
+ sampleSize <<= 1;
+ }
+
+ // Make sure sample size is reasonable
+ if (sampleSize <= 0 ||
+ 0 >= (int) (Math.min(w, h) / sampleSize)) {
+ return null;
+ }
+ return loadDownsampledBitmap(context, uri, sampleSize);
+ }
+
+ /**
+ * Loads a bitmap at a given URI that is downsampled so that both sides are
+ * smaller than maxSideLength. The Bitmap's original dimensions are stored
+ * in the rect originalBounds. The output is also transformed to the given
+ * orientation.
+ *
+ * @param uri URI of image to open.
+ * @param context context whose ContentResolver to use.
+ * @param maxSideLength max side length of returned bitmap.
+ * @param orientation the orientation to transform the bitmap to.
+ * @param originalBounds set to the actual bounds of the stored bitmap.
+ * @return downsampled bitmap or null if this operation failed.
+ */
+ public static Bitmap loadOrientedConstrainedBitmap(Uri uri, Context context, int maxSideLength,
+ int orientation, Rect originalBounds) {
+ Bitmap bmap = loadConstrainedBitmap(uri, context, maxSideLength, originalBounds, false);
+ if (bmap != null) {
+ bmap = orientBitmap(bmap, orientation);
+ }
+ return bmap;
+ }
+
+ public static Bitmap getScaleOneImageForPreset(Context context, Uri uri, Rect bounds,
+ Rect destination) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inMutable = true;
+ if (destination != null) {
+ if (bounds.width() > destination.width()) {
+ int sampleSize = 1;
+ int w = bounds.width();
+ while (w > destination.width()) {
+ sampleSize *= 2;
+ w /= sampleSize;
+ }
+ options.inSampleSize = sampleSize;
+ }
+ }
+ Bitmap bmp = loadRegionBitmap(context, uri, options, bounds);
+ return bmp;
+ }
+
+ /**
+ * Loads a bitmap that is downsampled by at least the input sample size. In
+ * low-memory situations, the bitmap may be downsampled further.
+ */
+ public static Bitmap loadBitmapWithBackouts(Context context, Uri sourceUri, int sampleSize) {
+ boolean noBitmap = true;
+ int num_tries = 0;
+ if (sampleSize <= 0) {
+ sampleSize = 1;
+ }
+ Bitmap bmap = null;
+ while (noBitmap) {
+ try {
+ // Try to decode, downsample if low-memory.
+ bmap = loadDownsampledBitmap(context, sourceUri, sampleSize);
+ noBitmap = false;
+ } catch (java.lang.OutOfMemoryError e) {
+ // Try with more downsampling before failing for good.
+ if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) {
+ throw e;
+ }
+ bmap = null;
+ System.gc();
+ sampleSize *= 2;
+ }
+ }
+ return bmap;
+ }
+
+ /**
+ * Loads an oriented bitmap that is downsampled by at least the input sample
+ * size. In low-memory situations, the bitmap may be downsampled further.
+ */
+ public static Bitmap loadOrientedBitmapWithBackouts(Context context, Uri sourceUri,
+ int sampleSize) {
+ Bitmap bitmap = loadBitmapWithBackouts(context, sourceUri, sampleSize);
+ if (bitmap == null) {
+ return null;
+ }
+ int orientation = getMetadataOrientation(context, sourceUri);
+ bitmap = orientBitmap(bitmap, orientation);
+ return bitmap;
+ }
+
+ /**
+ * Loads bitmap from a resource that may be downsampled in low-memory situations.
+ */
+ public static Bitmap decodeResourceWithBackouts(Resources res, BitmapFactory.Options options,
+ int id) {
+ boolean noBitmap = true;
+ int num_tries = 0;
+ if (options.inSampleSize < 1) {
+ options.inSampleSize = 1;
+ }
+ // Stopgap fix for low-memory devices.
+ Bitmap bmap = null;
+ while (noBitmap) {
+ try {
+ // Try to decode, downsample if low-memory.
+ bmap = BitmapFactory.decodeResource(
+ res, id, options);
+ noBitmap = false;
+ } catch (java.lang.OutOfMemoryError e) {
+ // Retry before failing for good.
+ if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) {
+ throw e;
+ }
+ bmap = null;
+ System.gc();
+ options.inSampleSize *= 2;
+ }
+ }
+ return bmap;
+ }
+
+ public static XMPMeta getXmpObject(Context context) {
+ try {
+ InputStream is = context.getContentResolver().openInputStream(
+ MasterImage.getImage().getUri());
+ return XmpUtilHelper.extractXMPMeta(is);
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Determine if this is a light cycle 360 image
+ *
+ * @return true if it is a light Cycle image that is full 360
+ */
+ public static boolean queryLightCycle360(Context context) {
+ InputStream is = null;
+ try {
+ is = context.getContentResolver().openInputStream(MasterImage.getImage().getUri());
+ XMPMeta meta = XmpUtilHelper.extractXMPMeta(is);
+ if (meta == null) {
+ return false;
+ }
+ String namespace = "http://ns.google.com/photos/1.0/panorama/";
+ String cropWidthName = "GPano:CroppedAreaImageWidthPixels";
+ String fullWidthName = "GPano:FullPanoWidthPixels";
+
+ if (!meta.doesPropertyExist(namespace, cropWidthName)) {
+ return false;
+ }
+ if (!meta.doesPropertyExist(namespace, fullWidthName)) {
+ return false;
+ }
+
+ Integer cropValue = meta.getPropertyInteger(namespace, cropWidthName);
+ Integer fullValue = meta.getPropertyInteger(namespace, fullWidthName);
+
+ // Definition of a 360:
+ // GFullPanoWidthPixels == CroppedAreaImageWidthPixels
+ if (cropValue != null && fullValue != null) {
+ return cropValue.equals(fullValue);
+ }
+
+ return false;
+ } catch (FileNotFoundException e) {
+ return false;
+ } catch (XMPException e) {
+ return false;
+ } finally {
+ Utils.closeSilently(is);
+ }
+ }
+}