/* * 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.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.Canvas; import android.graphics.Matrix; import android.graphics.Paint; 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.common.Utils; import com.android.gallery3d.exif.ExifInterface; import com.android.gallery3d.exif.ExifTag; import com.android.gallery3d.filtershow.imageshow.MasterImage; import com.android.gallery3d.filtershow.pipeline.FilterEnvironment; import com.android.gallery3d.filtershow.tools.XmpPresets; import com.android.gallery3d.util.XmpUtilHelper; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.List; 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 static final float OVERDRAW_ZOOM = 1.2f; 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; } public static String getLocalPathFromUri(Context context, Uri uri) { Cursor cursor = context.getContentResolver().query(uri, new String[]{MediaStore.Images.Media.DATA}, null, null, null); if (cursor == null) { return null; } int index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); cursor.moveToFirst(); return cursor.getString(index); } /** * 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.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.ORIENTATION); 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 } catch (IllegalStateException e) { // Do nothing } finally { Utils.closeSilently(cursor); } ExifInterface exif = new ExifInterface(); InputStream is = null; // Fall back to checking EXIF tags in file or input stream. try { if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { String mimeType = getMimeType(uri); if (!JPEG_MIME_TYPE.equals(mimeType)) { return ORI_NORMAL; } String path = uri.getPath(); exif.readExif(path); } else { is = context.getContentResolver().openInputStream(uri); exif.readExif(is); } return parseExif(exif); } catch (IOException e) { Log.w(LOGTAG, "Failed to read EXIF orientation", e); } catch (NullPointerException e) { Log.w(LOGTAG, "Invalid EXIF data", e); } finally { try { if (is != null) { is.close(); } } catch (IOException e) { Log.w(LOGTAG, "Failed to close InputStream", e); } } return ORI_NORMAL; } private static int parseExif(ExifInterface exif){ 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; } } 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, BitmapCache cache, Uri uri, BitmapFactory.Options options, Rect bounds) { InputStream is = null; int w = 0; int h = 0; if (options.inSampleSize != 0) { return null; } try { is = context.getContentResolver().openInputStream(uri); BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false); Rect r = new Rect(0, 0, decoder.getWidth(), decoder.getHeight()); w = decoder.getWidth(); h = decoder.getHeight(); Rect imageBounds = new Rect(bounds); // return null if bounds are not entirely within the bitmap if (!r.contains(imageBounds)) { imageBounds.intersect(r); bounds.left = imageBounds.left; bounds.top = imageBounds.top; } Bitmap reuse = cache.getBitmap(imageBounds.width(), imageBounds.height(), BitmapCache.REGION); options.inBitmap = reuse; Bitmap bitmap = decoder.decodeRegion(imageBounds, options); if (bitmap != reuse) { cache.cache(reuse); // not reused, put back in cache } return bitmap; } catch (FileNotFoundException e) { Log.e(LOGTAG, "FileNotFoundException for " + uri, e); } catch (IOException e) { Log.e(LOGTAG, "FileNotFoundException for " + uri, e); } catch (IllegalArgumentException e) { Log.e(LOGTAG, "exc, image decoded " + w + " x " + h + " bounds: " + bounds.left + "," + bounds.top + " - " + bounds.width() + "x" + bounds.height() + " exc: " + 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(); o.inJustDecodeBounds = true; 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); if (bmap.getConfig()!= Bitmap.Config.ARGB_8888){ bmap = bmap.copy( Bitmap.Config.ARGB_8888,true); } } return bmap; } public static Bitmap getScaleOneImageForPreset(Context context, BitmapCache cache, Uri uri, Rect bounds, Rect destination) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inMutable = true; if (destination != null) { int thresholdWidth = (int) (destination.width() * OVERDRAW_ZOOM); if (bounds.width() > thresholdWidth) { int sampleSize = 1; int w = bounds.width(); while (w > thresholdWidth) { sampleSize *= 2; w /= sampleSize; } options.inSampleSize = sampleSize; } } return loadRegionBitmap(context, cache, uri, options, bounds); } /** * 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); } } public static List getExif(Context context, Uri uri) { String path = getLocalPathFromUri(context, uri); if (path != null) { Uri localUri = Uri.parse(path); String mimeType = getMimeType(localUri); if (!JPEG_MIME_TYPE.equals(mimeType)) { return null; } try { ExifInterface exif = new ExifInterface(); exif.readExif(path); List taglist = exif.getAllTags(); return taglist; } catch (IOException e) { Log.w(LOGTAG, "Failed to read EXIF tags", e); } catch (NullPointerException e) { Log.e(LOGTAG, "Failed to read EXIF tags", e); } } return null; } }