2 * Copyright (C) 2012 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.gallery3d.filtershow.cache;
19 import android.content.ContentResolver;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.res.Resources;
23 import android.database.Cursor;
24 import android.database.sqlite.SQLiteException;
25 import android.graphics.Bitmap;
26 import android.graphics.BitmapFactory;
27 import android.graphics.BitmapRegionDecoder;
28 import android.graphics.Matrix;
29 import android.graphics.Rect;
30 import android.graphics.Bitmap.CompressFormat;
31 import android.media.ExifInterface;
32 import android.net.Uri;
33 import android.provider.MediaStore;
34 import android.util.Log;
36 import com.adobe.xmp.XMPException;
37 import com.adobe.xmp.XMPMeta;
39 import com.android.gallery3d.R;
40 import com.android.gallery3d.common.Utils;
41 import com.android.gallery3d.exif.ExifInvalidFormatException;
42 import com.android.gallery3d.exif.ExifParser;
43 import com.android.gallery3d.exif.ExifTag;
44 import com.android.gallery3d.filtershow.CropExtras;
45 import com.android.gallery3d.filtershow.FilterShowActivity;
46 import com.android.gallery3d.filtershow.HistoryAdapter;
47 import com.android.gallery3d.filtershow.imageshow.ImageCrop;
48 import com.android.gallery3d.filtershow.imageshow.ImageShow;
49 import com.android.gallery3d.filtershow.presets.ImagePreset;
50 import com.android.gallery3d.filtershow.tools.BitmapTask;
51 import com.android.gallery3d.filtershow.tools.SaveCopyTask;
52 import com.android.gallery3d.util.InterruptableOutputStream;
53 import com.android.gallery3d.util.XmpUtilHelper;
55 import java.io.Closeable;
57 import java.io.FileInputStream;
58 import java.io.FileNotFoundException;
59 import java.io.IOException;
60 import java.io.InputStream;
61 import java.io.OutputStream;
62 import java.util.Vector;
63 import java.util.concurrent.locks.ReentrantLock;
65 public class ImageLoader {
67 private static final String LOGTAG = "ImageLoader";
68 private final Vector<ImageShow> mListeners = new Vector<ImageShow>();
69 private Bitmap mOriginalBitmapSmall = null;
70 private Bitmap mOriginalBitmapLarge = null;
71 private Bitmap mBackgroundBitmap = null;
73 private final ZoomCache mZoomCache = new ZoomCache();
75 private int mOrientation = 0;
76 private HistoryAdapter mAdapter = null;
78 private FilterShowActivity mActivity = null;
80 public static final String JPEG_MIME_TYPE = "image/jpeg";
82 public static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos";
83 public static final int DEFAULT_COMPRESS_QUALITY = 95;
85 public static final int ORI_NORMAL = ExifInterface.ORIENTATION_NORMAL;
86 public static final int ORI_ROTATE_90 = ExifInterface.ORIENTATION_ROTATE_90;
87 public static final int ORI_ROTATE_180 = ExifInterface.ORIENTATION_ROTATE_180;
88 public static final int ORI_ROTATE_270 = ExifInterface.ORIENTATION_ROTATE_270;
89 public static final int ORI_FLIP_HOR = ExifInterface.ORIENTATION_FLIP_HORIZONTAL;
90 public static final int ORI_FLIP_VERT = ExifInterface.ORIENTATION_FLIP_VERTICAL;
91 public static final int ORI_TRANSPOSE = ExifInterface.ORIENTATION_TRANSPOSE;
92 public static final int ORI_TRANSVERSE = ExifInterface.ORIENTATION_TRANSVERSE;
94 private Context mContext = null;
95 private Uri mUri = null;
97 private Rect mOriginalBounds = null;
98 private static int mZoomOrientation = ORI_NORMAL;
100 private ReentrantLock mLoadingLock = new ReentrantLock();
102 public ImageLoader(FilterShowActivity activity, Context context) {
103 mActivity = activity;
107 public static int getZoomOrientation() {
108 return mZoomOrientation;
111 public FilterShowActivity getActivity() {
115 public boolean loadBitmap(Uri uri, int size) {
118 mOrientation = getOrientation(mContext, uri);
119 mOriginalBitmapSmall = loadScaledBitmap(uri, 160);
120 if (mOriginalBitmapSmall == null) {
121 // Couldn't read the bitmap, let's exit
122 mLoadingLock.unlock();
125 mOriginalBitmapLarge = loadScaledBitmap(uri, size);
126 if (mOriginalBitmapLarge == null) {
127 mLoadingLock.unlock();
131 mLoadingLock.unlock();
135 public Uri getUri() {
139 public Rect getOriginalBounds() {
140 return mOriginalBounds;
143 public static int getOrientation(Context context, Uri uri) {
144 if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
145 String mimeType = context.getContentResolver().getType(uri);
146 if (mimeType != ImageLoader.JPEG_MIME_TYPE) {
149 String path = uri.getPath();
150 int orientation = -1;
151 InputStream is = null;
153 is = new FileInputStream(path);
154 ExifParser parser = ExifParser.parse(is, ExifParser.OPTION_IFD_0);
155 int event = parser.next();
156 while (event != ExifParser.EVENT_END) {
157 if (event == ExifParser.EVENT_NEW_TAG) {
158 ExifTag tag = parser.getTag();
159 if (tag.getTagId() == ExifTag.TAG_ORIENTATION) {
160 orientation = (int) tag.getValueAt(0);
164 event = parser.next();
166 } catch (IOException e) {
168 } catch (ExifInvalidFormatException e) {
171 Utils.closeSilently(is);
175 Cursor cursor = null;
177 cursor = context.getContentResolver().query(uri,
179 MediaStore.Images.ImageColumns.ORIENTATION
182 if (cursor.moveToNext()) {
183 int ori = cursor.getInt(0);
189 return ORI_ROTATE_90;
191 return ORI_ROTATE_270;
193 return ORI_ROTATE_180;
200 } catch (SQLiteException e) {
201 return ExifInterface.ORIENTATION_UNDEFINED;
202 } catch (IllegalArgumentException e) {
203 return ExifInterface.ORIENTATION_UNDEFINED;
205 Utils.closeSilently(cursor);
209 private void updateBitmaps() {
210 if (mOrientation > 1) {
211 mOriginalBitmapSmall = rotateToPortrait(mOriginalBitmapSmall, mOrientation);
212 mOriginalBitmapLarge = rotateToPortrait(mOriginalBitmapLarge, mOrientation);
214 mZoomOrientation = mOrientation;
218 public Bitmap decodeImage(int id, BitmapFactory.Options options) {
219 return BitmapFactory.decodeResource(mContext.getResources(), id, options);
222 public static Bitmap rotateToPortrait(Bitmap bitmap, int ori) {
223 Matrix matrix = new Matrix();
224 int w = bitmap.getWidth();
225 int h = bitmap.getHeight();
226 if (ori == ORI_ROTATE_90 ||
227 ori == ORI_ROTATE_270 ||
228 ori == ORI_TRANSPOSE ||
229 ori == ORI_TRANSVERSE) {
236 matrix.setRotate(90, w / 2f, h / 2f);
239 matrix.setRotate(180, w / 2f, h / 2f);
242 matrix.setRotate(270, w / 2f, h / 2f);
245 matrix.preScale(-1, 1);
248 matrix.preScale(1, -1);
251 matrix.setRotate(90, w / 2f, h / 2f);
252 matrix.preScale(1, -1);
255 matrix.setRotate(270, w / 2f, h / 2f);
256 matrix.preScale(1, -1);
263 return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
264 bitmap.getHeight(), matrix, true);
267 private Bitmap loadRegionBitmap(Uri uri, Rect bounds) {
268 InputStream is = null;
270 is = mContext.getContentResolver().openInputStream(uri);
271 BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
272 return decoder.decodeRegion(bounds, null);
273 } catch (FileNotFoundException e) {
274 Log.e(LOGTAG, "FileNotFoundException: " + uri);
275 } catch (Exception e) {
278 Utils.closeSilently(is);
283 static final int MAX_BITMAP_DIM = 2048;
285 private Bitmap loadScaledBitmap(Uri uri, int size) {
286 InputStream is = null;
288 is = mContext.getContentResolver().openInputStream(uri);
289 Log.v(LOGTAG, "loading uri " + uri.getPath() + " input stream: "
291 BitmapFactory.Options o = new BitmapFactory.Options();
292 o.inJustDecodeBounds = true;
293 BitmapFactory.decodeStream(is, null, o);
295 int width_tmp = o.outWidth;
296 int height_tmp = o.outHeight;
298 mOriginalBounds = new Rect(0, 0, width_tmp, height_tmp);
302 if (width_tmp <= MAX_BITMAP_DIM && height_tmp <= MAX_BITMAP_DIM) {
303 if (width_tmp / 2 < size || height_tmp / 2 < size) {
312 // decode with inSampleSize
313 BitmapFactory.Options o2 = new BitmapFactory.Options();
314 o2.inSampleSize = scale;
316 Utils.closeSilently(is);
317 is = mContext.getContentResolver().openInputStream(uri);
318 return BitmapFactory.decodeStream(is, null, o2);
319 } catch (FileNotFoundException e) {
320 Log.e(LOGTAG, "FileNotFoundException: " + uri);
321 } catch (Exception e) {
324 Utils.closeSilently(is);
329 public Bitmap getBackgroundBitmap(Resources resources) {
330 if (mBackgroundBitmap == null) {
331 mBackgroundBitmap = BitmapFactory.decodeResource(resources,
332 R.drawable.filtershow_background);
334 return mBackgroundBitmap;
338 public Bitmap getOriginalBitmapSmall() {
339 return mOriginalBitmapSmall;
342 public Bitmap getOriginalBitmapLarge() {
343 return mOriginalBitmapLarge;
346 public void addListener(ImageShow imageShow) {
348 if (!mListeners.contains(imageShow)) {
349 mListeners.add(imageShow);
351 mLoadingLock.unlock();
354 private void warnListeners() {
355 mActivity.runOnUiThread(mWarnListenersRunnable);
358 private Runnable mWarnListenersRunnable = new Runnable() {
362 for (int i = 0; i < mListeners.size(); i++) {
363 ImageShow imageShow = mListeners.elementAt(i);
364 imageShow.imageLoaded();
369 // FIXME: this currently does the loading + filtering on the UI thread --
370 // need to move this to a background thread.
371 public Bitmap getScaleOneImageForPreset(ImageShow caller, ImagePreset imagePreset, Rect bounds,
374 Bitmap bmp = mZoomCache.getImage(imagePreset, bounds);
375 if (force || bmp == null) {
376 bmp = loadRegionBitmap(mUri, bounds);
378 // TODO: this workaround for RS might not be needed ultimately
379 Bitmap bmp2 = bmp.copy(Bitmap.Config.ARGB_8888, true);
380 float scaleFactor = imagePreset.getScaleFactor();
381 imagePreset.setScaleFactor(1.0f);
382 bmp2 = imagePreset.apply(bmp2);
383 imagePreset.setScaleFactor(scaleFactor);
384 mZoomCache.setImage(imagePreset, bounds, bmp2);
385 mLoadingLock.unlock();
389 mLoadingLock.unlock();
393 public void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity,
395 preset.setIsHighQuality(true);
396 preset.setScaleFactor(1.0f);
397 new SaveCopyTask(mContext, mUri, destination, new SaveCopyTask.Callback() {
400 public void onComplete(Uri result) {
401 filterShowActivity.completeSaveImage(result);
407 public static Bitmap loadMutableBitmap(Context context, Uri sourceUri) {
408 BitmapFactory.Options options = new BitmapFactory.Options();
409 // TODO: on <3.x we need a copy of the bitmap (inMutable doesn't
411 options.inMutable = true;
413 InputStream is = null;
414 Bitmap bitmap = null;
416 is = context.getContentResolver().openInputStream(sourceUri);
417 bitmap = BitmapFactory.decodeStream(is, null, options);
418 } catch (FileNotFoundException e) {
419 Log.w(LOGTAG, "could not load bitmap ", e);
423 Utils.closeSilently(is);
425 if (bitmap == null) {
428 int orientation = ImageLoader.getOrientation(context, sourceUri);
429 bitmap = ImageLoader.rotateToPortrait(bitmap, orientation);
433 public void returnFilteredResult(ImagePreset preset,
434 final FilterShowActivity filterShowActivity) {
435 preset.setIsHighQuality(true);
436 preset.setScaleFactor(1.0f);
438 BitmapTask.Callbacks<ImagePreset> cb = new BitmapTask.Callbacks<ImagePreset>() {
441 public void onComplete(Bitmap result) {
442 filterShowActivity.onFilteredResult(result);
446 public void onCancel() {
450 public Bitmap onExecute(ImagePreset param) {
454 Bitmap bitmap = loadMutableBitmap(mContext, mUri);
455 if (bitmap == null) {
456 Log.w(LOGTAG, "Failed to save image!");
459 return param.apply(bitmap);
463 (new BitmapTask<ImagePreset>(cb)).execute(preset);
466 private String getFileExtension(String requestFormat) {
467 String outputFormat = (requestFormat == null)
470 outputFormat = outputFormat.toLowerCase();
471 return (outputFormat.equals("png") || outputFormat.equals("gif"))
472 ? "png" // We don't support gif compression.
476 private CompressFormat convertExtensionToCompressFormat(String extension) {
477 return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG;
480 public void saveToUri(Bitmap bmap, Uri uri, final String outputFormat,
481 final FilterShowActivity filterShowActivity) {
483 OutputStream out = null;
485 out = filterShowActivity.getContentResolver().openOutputStream(uri);
486 } catch (FileNotFoundException e) {
487 Log.w(LOGTAG, "cannot write output", e);
490 if (bmap == null || out == null) {
495 final InterruptableOutputStream ios = new InterruptableOutputStream(out);
497 BitmapTask.Callbacks<Bitmap> cb = new BitmapTask.Callbacks<Bitmap>() {
500 public void onComplete(Bitmap result) {
501 filterShowActivity.done();
505 public void onCancel() {
510 public Bitmap onExecute(Bitmap param) {
511 CompressFormat cf = convertExtensionToCompressFormat(getFileExtension(outputFormat));
512 param.compress(cf, DEFAULT_COMPRESS_QUALITY, ios);
513 Utils.closeSilently(ios);
518 (new BitmapTask<Bitmap>(cb)).execute(bmap);
521 public void setAdapter(HistoryAdapter adapter) {
525 public HistoryAdapter getHistory() {
529 public XMPMeta getXmpObject() {
531 InputStream is = mContext.getContentResolver().openInputStream(getUri());
532 return XmpUtilHelper.extractXMPMeta(is);
533 } catch (FileNotFoundException e) {
539 * Determine if this is a light cycle 360 image
541 * @return true if it is a light Cycle image that is full 360
543 public boolean queryLightCycle360() {
544 InputStream is = null;
546 is = mContext.getContentResolver().openInputStream(getUri());
547 XMPMeta meta = XmpUtilHelper.extractXMPMeta(is);
551 String name = meta.getPacketHeader();
552 String namespace = "http://ns.google.com/photos/1.0/panorama/";
553 String cropWidthName = "GPano:CroppedAreaImageWidthPixels";
554 String fullWidthName = "GPano:FullPanoWidthPixels";
556 if (!meta.doesPropertyExist(namespace, cropWidthName)) {
559 if (!meta.doesPropertyExist(namespace, fullWidthName)) {
563 Integer cropValue = meta.getPropertyInteger(namespace, cropWidthName);
564 Integer fullValue = meta.getPropertyInteger(namespace, fullWidthName);
566 // Definition of a 360:
567 // GFullPanoWidthPixels == CroppedAreaImageWidthPixels
568 if (cropValue != null && fullValue != null) {
569 return cropValue.equals(fullValue);
573 } catch (FileNotFoundException e) {
575 } catch (XMPException e) {
578 Utils.closeSilently(is);