blob: 52c296c78f9304fd530e59cffe33cc6168225f2e [file] [log] [blame]
/*
* 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.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
} 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);
} 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<ExifTag> 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<ExifTag> taglist = exif.getAllTags();
return taglist;
} catch (IOException e) {
Log.w(LOGTAG, "Failed to read EXIF tags", e);
}
}
return null;
}
}