blob: b4e98e11436452581b798fadbb335b4d173b50a2 [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.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.Bitmap.CompressFormat;
import android.net.Uri;
import android.provider.MediaStore;
import android.util.Log;
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.ExifTag;
import com.android.gallery3d.exif.ExifInterface;
import com.android.gallery3d.filtershow.FilterShowActivity;
import com.android.gallery3d.filtershow.HistoryAdapter;
import com.android.gallery3d.filtershow.filters.FiltersManager;
import com.android.gallery3d.filtershow.imageshow.ImageShow;
import com.android.gallery3d.filtershow.imageshow.MasterImage;
import com.android.gallery3d.filtershow.presets.ImagePreset;
import com.android.gallery3d.filtershow.tools.BitmapTask;
import com.android.gallery3d.filtershow.tools.SaveCopyTask;
import com.android.gallery3d.util.InterruptableOutputStream;
import com.android.gallery3d.util.XmpUtilHelper;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Vector;
import java.util.concurrent.locks.ReentrantLock;
// TODO: this class has waaaay to much bitmap copying. Cleanup.
public class ImageLoader {
private static final String LOGTAG = "ImageLoader";
private final Vector<ImageShow> mListeners = new Vector<ImageShow>();
private Bitmap mOriginalBitmapSmall = null;
private Bitmap mOriginalBitmapLarge = null;
private Bitmap mOriginalBitmapHighres = null;
private Bitmap mBackgroundBitmap = null;
private final ZoomCache mZoomCache = new ZoomCache();
private int mOrientation = 0;
private HistoryAdapter mAdapter = null;
private FilterShowActivity mActivity = null;
public static final String JPEG_MIME_TYPE = "image/jpeg";
public static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos";
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 Context mContext = null;
private Uri mUri = null;
private Rect mOriginalBounds = null;
private static int mZoomOrientation = ORI_NORMAL;
static final int MAX_BITMAP_DIM = 900;
private ReentrantLock mLoadingLock = new ReentrantLock();
public ImageLoader(FilterShowActivity activity, Context context) {
mActivity = activity;
mContext = context;
}
public static int getZoomOrientation() {
return mZoomOrientation;
}
public FilterShowActivity getActivity() {
return mActivity;
}
public boolean loadBitmap(Uri uri, int size) {
mLoadingLock.lock();
mUri = uri;
mOrientation = getOrientation(mContext, uri);
mOriginalBitmapSmall = loadScaledBitmap(uri, 160);
if (mOriginalBitmapSmall == null) {
// Couldn't read the bitmap, let's exit
mLoadingLock.unlock();
return false;
}
mOriginalBitmapLarge = loadScaledBitmap(uri, size);
if (mOriginalBitmapLarge == null) {
mLoadingLock.unlock();
return false;
}
if (MasterImage.getImage().supportsHighRes()) {
int highresPreviewSize = mOriginalBitmapLarge.getWidth() * 2;
if (highresPreviewSize > mOriginalBounds.width()) {
highresPreviewSize = mOriginalBounds.width();
}
mOriginalBitmapHighres = loadScaledBitmap(uri, highresPreviewSize, false);
}
updateBitmaps();
mLoadingLock.unlock();
return true;
}
public Uri getUri() {
return mUri;
}
public Rect getOriginalBounds() {
return mOriginalBounds;
}
public static int getOrientation(Context context, Uri uri) {
if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
String mimeType = context.getContentResolver().getType(uri);
if (mimeType != ImageLoader.JPEG_MIME_TYPE) {
return -1;
}
String path = uri.getPath();
int orientation = -1;
InputStream is = null;
ExifInterface exif = new ExifInterface();
try {
exif.readExif(path);
orientation = ExifInterface.getRotationForOrientationValue(
exif.getTagIntValue(ExifInterface.TAG_ORIENTATION).shortValue());
} catch (IOException e) {
Log.w(LOGTAG, "Failed to read EXIF orientation", e);
}
return orientation;
}
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(uri,
new String[] {
MediaStore.Images.ImageColumns.ORIENTATION
},
null, null, null);
if (cursor.moveToNext()) {
int ori = cursor.getInt(0);
switch (ori) {
case 0:
return ORI_NORMAL;
case 90:
return ORI_ROTATE_90;
case 270:
return ORI_ROTATE_270;
case 180:
return ORI_ROTATE_180;
default:
return -1;
}
} else {
return -1;
}
} catch (SQLiteException e) {
return -1;
} catch (IllegalArgumentException e) {
return -1;
} finally {
Utils.closeSilently(cursor);
}
}
private void updateBitmaps() {
if (mOrientation > 1) {
mOriginalBitmapSmall = rotateToPortrait(mOriginalBitmapSmall, mOrientation);
mOriginalBitmapLarge = rotateToPortrait(mOriginalBitmapLarge, mOrientation);
if (mOriginalBitmapHighres != null) {
mOriginalBitmapHighres = rotateToPortrait(mOriginalBitmapHighres, mOrientation);
}
}
mZoomOrientation = mOrientation;
warnListeners();
}
public Bitmap decodeImage(int id, BitmapFactory.Options options) {
return BitmapFactory.decodeResource(mContext.getResources(), id, options);
}
public static Bitmap rotateToPortrait(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);
}
private Bitmap loadRegionBitmap(Uri uri, BitmapFactory.Options options, Rect bounds) {
InputStream is = null;
try {
is = mContext.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: " + uri);
} catch (Exception e) {
e.printStackTrace();
} finally {
Utils.closeSilently(is);
}
return null;
}
private Bitmap loadScaledBitmap(Uri uri, int size) {
return loadScaledBitmap(uri, size, true);
}
private Bitmap loadScaledBitmap(Uri uri, int size, boolean enforceSize) {
InputStream is = null;
try {
is = mContext.getContentResolver().openInputStream(uri);
Log.v(LOGTAG, "loading uri " + uri.getPath() + " input stream: "
+ is);
BitmapFactory.Options o = new BitmapFactory.Options();
o.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, o);
int width_tmp = o.outWidth;
int height_tmp = o.outHeight;
mOriginalBounds = new Rect(0, 0, width_tmp, height_tmp);
int scale = 1;
while (true) {
if (width_tmp <= 2 || height_tmp <= 2) {
break;
}
if (!enforceSize
|| (width_tmp <= MAX_BITMAP_DIM
&& height_tmp <= MAX_BITMAP_DIM)) {
if (width_tmp / 2 < size || height_tmp / 2 < size) {
break;
}
}
width_tmp /= 2;
height_tmp /= 2;
scale *= 2;
}
// decode with inSampleSize
BitmapFactory.Options o2 = new BitmapFactory.Options();
o2.inSampleSize = scale;
o2.inMutable = true;
Utils.closeSilently(is);
is = mContext.getContentResolver().openInputStream(uri);
return BitmapFactory.decodeStream(is, null, o2);
} catch (FileNotFoundException e) {
Log.e(LOGTAG, "FileNotFoundException: " + uri);
} catch (Exception e) {
e.printStackTrace();
} finally {
Utils.closeSilently(is);
}
return null;
}
public Bitmap getBackgroundBitmap(Resources resources) {
if (mBackgroundBitmap == null) {
mBackgroundBitmap = BitmapFactory.decodeResource(resources,
R.drawable.filtershow_background);
}
return mBackgroundBitmap;
}
public Bitmap getOriginalBitmapSmall() {
return mOriginalBitmapSmall;
}
public Bitmap getOriginalBitmapLarge() {
return mOriginalBitmapLarge;
}
public Bitmap getOriginalBitmapHighres() {
return mOriginalBitmapHighres;
}
public void addListener(ImageShow imageShow) {
mLoadingLock.lock();
if (!mListeners.contains(imageShow)) {
mListeners.add(imageShow);
}
mLoadingLock.unlock();
}
private void warnListeners() {
mActivity.runOnUiThread(mWarnListenersRunnable);
}
private Runnable mWarnListenersRunnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < mListeners.size(); i++) {
ImageShow imageShow = mListeners.elementAt(i);
imageShow.imageLoaded();
}
}
};
public Bitmap getScaleOneImageForPreset(Rect bounds, Rect destination) {
mLoadingLock.lock();
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(mUri, options, bounds);
mLoadingLock.unlock();
return bmp;
}
public void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity,
File destination) {
new SaveCopyTask(mContext, mUri, destination, new SaveCopyTask.Callback() {
@Override
public void onComplete(Uri result) {
filterShowActivity.completeSaveImage(result);
}
}).execute(preset);
}
public static Bitmap loadMutableBitmap(Context context, Uri sourceUri) {
BitmapFactory.Options options = new BitmapFactory.Options();
return loadMutableBitmap(context, sourceUri, options);
}
public static Bitmap loadMutableBitmap(Context context, Uri sourceUri,
BitmapFactory.Options options) {
// TODO: on <3.x we need a copy of the bitmap (inMutable doesn't
// exist)
options.inMutable = true;
Bitmap bitmap = decodeUriWithBackouts(context, sourceUri, options);
if (bitmap == null) {
return null;
}
int orientation = ImageLoader.getOrientation(context, sourceUri);
bitmap = ImageLoader.rotateToPortrait(bitmap, orientation);
return bitmap;
}
public static Bitmap decodeUriWithBackouts(Context context, Uri sourceUri,
BitmapFactory.Options options) {
boolean noBitmap = true;
int num_tries = 0;
InputStream is = getInputStream(context, sourceUri);
if (options.inSampleSize < 1) {
options.inSampleSize = 1;
}
// Stopgap fix for low-memory devices.
Bitmap bmap = null;
while (noBitmap) {
if (is == null) {
return null;
}
try {
// Try to decode, downsample if low-memory.
bmap = BitmapFactory.decodeStream(is, null, options);
noBitmap = false;
} catch (java.lang.OutOfMemoryError e) {
// Try 5 times before failing for good.
if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) {
throw e;
}
is = null;
bmap = null;
System.gc();
is = getInputStream(context, sourceUri);
options.inSampleSize *= 2;
}
}
Utils.closeSilently(is);
return bmap;
}
private static InputStream getInputStream(Context context, Uri sourceUri) {
InputStream is = null;
try {
is = context.getContentResolver().openInputStream(sourceUri);
} catch (FileNotFoundException e) {
Log.w(LOGTAG, "could not load bitmap ", e);
Utils.closeSilently(is);
is = null;
}
return is;
}
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) {
// Try 5 times before failing for good.
if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) {
throw e;
}
bmap = null;
System.gc();
options.inSampleSize *= 2;
}
}
return bmap;
}
public void returnFilteredResult(ImagePreset preset,
final FilterShowActivity filterShowActivity) {
BitmapTask.Callbacks<ImagePreset> cb = new BitmapTask.Callbacks<ImagePreset>() {
@Override
public void onComplete(Bitmap result) {
filterShowActivity.onFilteredResult(result);
}
@Override
public void onCancel() {
}
@Override
public Bitmap onExecute(ImagePreset param) {
if (param == null || mUri == null) {
return null;
}
BitmapFactory.Options options = new BitmapFactory.Options();
boolean noBitmap = true;
int num_tries = 0;
if (options.inSampleSize < 1) {
options.inSampleSize = 1;
}
Bitmap bitmap = null;
// Stopgap fix for low-memory devices.
while (noBitmap) {
try {
// Try to do bitmap operations, downsample if low-memory
bitmap = loadMutableBitmap(mContext, mUri, options);
if (bitmap == null) {
Log.w(LOGTAG, "Failed to save image!");
return null;
}
CachingPipeline pipeline = new CachingPipeline(
FiltersManager.getManager(), "Saving");
bitmap = pipeline.renderFinalImage(bitmap, param);
noBitmap = false;
} catch (java.lang.OutOfMemoryError e) {
// Try 5 times before failing for good.
if (++num_tries >= 5) {
throw e;
}
bitmap = null;
System.gc();
options.inSampleSize *= 2;
}
}
return bitmap;
}
};
(new BitmapTask<ImagePreset>(cb)).execute(preset);
}
private String getFileExtension(String requestFormat) {
String outputFormat = (requestFormat == null)
? "jpg"
: requestFormat;
outputFormat = outputFormat.toLowerCase();
return (outputFormat.equals("png") || outputFormat.equals("gif"))
? "png" // We don't support gif compression.
: "jpg";
}
private CompressFormat convertExtensionToCompressFormat(String extension) {
return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG;
}
public void saveToUri(Bitmap bmap, Uri uri, final String outputFormat,
final FilterShowActivity filterShowActivity) {
OutputStream out = null;
try {
out = filterShowActivity.getContentResolver().openOutputStream(uri);
} catch (FileNotFoundException e) {
Log.w(LOGTAG, "cannot write output", e);
out = null;
} finally {
if (bmap == null || out == null) {
return;
}
}
final InterruptableOutputStream ios = new InterruptableOutputStream(out);
BitmapTask.Callbacks<Bitmap> cb = new BitmapTask.Callbacks<Bitmap>() {
@Override
public void onComplete(Bitmap result) {
filterShowActivity.done();
}
@Override
public void onCancel() {
ios.interrupt();
}
@Override
public Bitmap onExecute(Bitmap param) {
CompressFormat cf = convertExtensionToCompressFormat(getFileExtension(outputFormat));
param.compress(cf, DEFAULT_COMPRESS_QUALITY, ios);
Utils.closeSilently(ios);
return null;
}
};
(new BitmapTask<Bitmap>(cb)).execute(bmap);
}
public void setAdapter(HistoryAdapter adapter) {
mAdapter = adapter;
}
public HistoryAdapter getHistory() {
return mAdapter;
}
public XMPMeta getXmpObject() {
try {
InputStream is = mContext.getContentResolver().openInputStream(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 boolean queryLightCycle360() {
InputStream is = null;
try {
is = mContext.getContentResolver().openInputStream(getUri());
XMPMeta meta = XmpUtilHelper.extractXMPMeta(is);
if (meta == null) {
return false;
}
String name = meta.getPacketHeader();
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);
}
}
}