diff options
Diffstat (limited to 'src/com/android/gallery3d/filtershow/cache/ImageLoader.java')
-rw-r--r-- | src/com/android/gallery3d/filtershow/cache/ImageLoader.java | 502 |
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); + } + } +} |