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.graphics.RectF;
32 import android.media.ExifInterface;
33 import android.net.Uri;
34 import android.provider.MediaStore;
35 import android.util.Log;
37 import com.adobe.xmp.XMPException;
38 import com.adobe.xmp.XMPMeta;
40 import com.android.gallery3d.R;
41 import com.android.gallery3d.common.Utils;
42 import com.android.gallery3d.exif.ExifInvalidFormatException;
43 import com.android.gallery3d.exif.ExifParser;
44 import com.android.gallery3d.exif.ExifTag;
45 import com.android.gallery3d.filtershow.CropExtras;
46 import com.android.gallery3d.filtershow.FilterShowActivity;
47 import com.android.gallery3d.filtershow.HistoryAdapter;
48 import com.android.gallery3d.filtershow.imageshow.GeometryMetadata;
49 import com.android.gallery3d.filtershow.imageshow.ImageCrop;
50 import com.android.gallery3d.filtershow.imageshow.ImageShow;
51 import com.android.gallery3d.filtershow.imageshow.MasterImage;
52 import com.android.gallery3d.filtershow.presets.ImagePreset;
53 import com.android.gallery3d.filtershow.tools.BitmapTask;
54 import com.android.gallery3d.filtershow.tools.SaveCopyTask;
55 import com.android.gallery3d.util.InterruptableOutputStream;
56 import com.android.gallery3d.util.XmpUtilHelper;
58 import java.io.Closeable;
60 import java.io.FileInputStream;
61 import java.io.FileNotFoundException;
62 import java.io.IOException;
63 import java.io.InputStream;
64 import java.io.OutputStream;
65 import java.util.Vector;
66 import java.util.concurrent.locks.ReentrantLock;
69 // TODO: this class has waaaay to much bitmap copying. Cleanup.
70 public class ImageLoader {
72 private static final String LOGTAG = "ImageLoader";
73 private final Vector<ImageShow> mListeners = new Vector<ImageShow>();
74 private Bitmap mOriginalBitmapSmall = null;
75 private Bitmap mOriginalBitmapLarge = null;
76 private Bitmap mBackgroundBitmap = null;
78 private final ZoomCache mZoomCache = new ZoomCache();
80 private int mOrientation = 0;
81 private HistoryAdapter mAdapter = null;
83 private FilterShowActivity mActivity = null;
85 public static final String JPEG_MIME_TYPE = "image/jpeg";
87 public static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos";
88 public static final int DEFAULT_COMPRESS_QUALITY = 95;
90 public static final int ORI_NORMAL = ExifInterface.ORIENTATION_NORMAL;
91 public static final int ORI_ROTATE_90 = ExifInterface.ORIENTATION_ROTATE_90;
92 public static final int ORI_ROTATE_180 = ExifInterface.ORIENTATION_ROTATE_180;
93 public static final int ORI_ROTATE_270 = ExifInterface.ORIENTATION_ROTATE_270;
94 public static final int ORI_FLIP_HOR = ExifInterface.ORIENTATION_FLIP_HORIZONTAL;
95 public static final int ORI_FLIP_VERT = ExifInterface.ORIENTATION_FLIP_VERTICAL;
96 public static final int ORI_TRANSPOSE = ExifInterface.ORIENTATION_TRANSPOSE;
97 public static final int ORI_TRANSVERSE = ExifInterface.ORIENTATION_TRANSVERSE;
99 private static final int BITMAP_LOAD_BACKOUT_ATTEMPTS = 5;
100 private Context mContext = null;
101 private Uri mUri = null;
103 private Rect mOriginalBounds = null;
104 private static int mZoomOrientation = ORI_NORMAL;
106 private ReentrantLock mLoadingLock = new ReentrantLock();
108 public ImageLoader(FilterShowActivity activity, Context context) {
109 mActivity = activity;
113 public static int getZoomOrientation() {
114 return mZoomOrientation;
117 public FilterShowActivity getActivity() {
121 public boolean loadBitmap(Uri uri, int size) {
124 mOrientation = getOrientation(mContext, uri);
125 mOriginalBitmapSmall = loadScaledBitmap(uri, 160);
126 if (mOriginalBitmapSmall == null) {
127 // Couldn't read the bitmap, let's exit
128 mLoadingLock.unlock();
131 mOriginalBitmapLarge = loadScaledBitmap(uri, size);
132 if (mOriginalBitmapLarge == null) {
133 mLoadingLock.unlock();
138 GeometryMetadata geo = MasterImage.getImage().getPreset().mGeoData;
139 float w = mOriginalBitmapLarge.getWidth();
140 float h = mOriginalBitmapLarge.getHeight();
141 RectF r = new RectF(0, 0, w, h);
142 geo.setPhotoBounds(r);
143 geo.setCropBounds(r);
144 MasterImage.getImage().getPreset().setGeometry(geo);
145 MasterImage.getImage().notifyGeometryChange();
146 mLoadingLock.unlock();
150 public Uri getUri() {
154 public Rect getOriginalBounds() {
155 return mOriginalBounds;
158 public static int getOrientation(Context context, Uri uri) {
159 if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
160 String mimeType = context.getContentResolver().getType(uri);
161 if (mimeType != ImageLoader.JPEG_MIME_TYPE) {
164 String path = uri.getPath();
165 int orientation = -1;
166 InputStream is = null;
168 is = new FileInputStream(path);
169 ExifParser parser = ExifParser.parse(is, ExifParser.OPTION_IFD_0);
170 int event = parser.next();
171 while (event != ExifParser.EVENT_END) {
172 if (event == ExifParser.EVENT_NEW_TAG) {
173 ExifTag tag = parser.getTag();
174 if (tag.getTagId() == ExifTag.TAG_ORIENTATION) {
175 orientation = (int) tag.getValueAt(0);
179 event = parser.next();
181 } catch (IOException e) {
183 } catch (ExifInvalidFormatException e) {
186 Utils.closeSilently(is);
190 Cursor cursor = null;
192 cursor = context.getContentResolver().query(uri,
194 MediaStore.Images.ImageColumns.ORIENTATION
197 if (cursor.moveToNext()) {
198 int ori = cursor.getInt(0);
204 return ORI_ROTATE_90;
206 return ORI_ROTATE_270;
208 return ORI_ROTATE_180;
215 } catch (SQLiteException e) {
216 return ExifInterface.ORIENTATION_UNDEFINED;
217 } catch (IllegalArgumentException e) {
218 return ExifInterface.ORIENTATION_UNDEFINED;
220 Utils.closeSilently(cursor);
224 private void updateBitmaps() {
225 if (mOrientation > 1) {
226 mOriginalBitmapSmall = rotateToPortrait(mOriginalBitmapSmall, mOrientation);
227 mOriginalBitmapLarge = rotateToPortrait(mOriginalBitmapLarge, mOrientation);
229 mZoomOrientation = mOrientation;
233 public Bitmap decodeImage(int id, BitmapFactory.Options options) {
234 return BitmapFactory.decodeResource(mContext.getResources(), id, options);
237 public static Bitmap rotateToPortrait(Bitmap bitmap, int ori) {
238 Matrix matrix = new Matrix();
239 int w = bitmap.getWidth();
240 int h = bitmap.getHeight();
241 if (ori == ORI_ROTATE_90 ||
242 ori == ORI_ROTATE_270 ||
243 ori == ORI_TRANSPOSE ||
244 ori == ORI_TRANSVERSE) {
251 matrix.setRotate(90, w / 2f, h / 2f);
254 matrix.setRotate(180, w / 2f, h / 2f);
257 matrix.setRotate(270, w / 2f, h / 2f);
260 matrix.preScale(-1, 1);
263 matrix.preScale(1, -1);
266 matrix.setRotate(90, w / 2f, h / 2f);
267 matrix.preScale(1, -1);
270 matrix.setRotate(270, w / 2f, h / 2f);
271 matrix.preScale(1, -1);
278 return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
279 bitmap.getHeight(), matrix, true);
282 private Bitmap loadRegionBitmap(Uri uri, BitmapFactory.Options options, Rect bounds) {
283 InputStream is = null;
285 is = mContext.getContentResolver().openInputStream(uri);
286 BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
287 return decoder.decodeRegion(bounds, options);
288 } catch (FileNotFoundException e) {
289 Log.e(LOGTAG, "FileNotFoundException: " + uri);
290 } catch (Exception e) {
293 Utils.closeSilently(is);
298 static final int MAX_BITMAP_DIM = 900;
300 private Bitmap loadScaledBitmap(Uri uri, int size) {
301 InputStream is = null;
303 is = mContext.getContentResolver().openInputStream(uri);
304 Log.v(LOGTAG, "loading uri " + uri.getPath() + " input stream: "
306 BitmapFactory.Options o = new BitmapFactory.Options();
307 o.inJustDecodeBounds = true;
308 BitmapFactory.decodeStream(is, null, o);
310 int width_tmp = o.outWidth;
311 int height_tmp = o.outHeight;
313 mOriginalBounds = new Rect(0, 0, width_tmp, height_tmp);
317 if (width_tmp <= MAX_BITMAP_DIM && height_tmp <= MAX_BITMAP_DIM) {
318 if (width_tmp / 2 < size || height_tmp / 2 < size) {
327 // decode with inSampleSize
328 BitmapFactory.Options o2 = new BitmapFactory.Options();
329 o2.inSampleSize = scale;
332 Utils.closeSilently(is);
333 is = mContext.getContentResolver().openInputStream(uri);
334 return BitmapFactory.decodeStream(is, null, o2);
335 } catch (FileNotFoundException e) {
336 Log.e(LOGTAG, "FileNotFoundException: " + uri);
337 } catch (Exception e) {
340 Utils.closeSilently(is);
345 public Bitmap getBackgroundBitmap(Resources resources) {
346 if (mBackgroundBitmap == null) {
347 mBackgroundBitmap = BitmapFactory.decodeResource(resources,
348 R.drawable.filtershow_background);
350 return mBackgroundBitmap;
354 public Bitmap getOriginalBitmapSmall() {
355 return mOriginalBitmapSmall;
358 public Bitmap getOriginalBitmapLarge() {
359 return mOriginalBitmapLarge;
362 public void addListener(ImageShow imageShow) {
364 if (!mListeners.contains(imageShow)) {
365 mListeners.add(imageShow);
367 mLoadingLock.unlock();
370 private void warnListeners() {
371 mActivity.runOnUiThread(mWarnListenersRunnable);
374 private Runnable mWarnListenersRunnable = new Runnable() {
378 for (int i = 0; i < mListeners.size(); i++) {
379 ImageShow imageShow = mListeners.elementAt(i);
380 imageShow.imageLoaded();
385 // FIXME: this currently does the loading + filtering on the UI thread --
386 // need to move this to a background thread.
387 public Bitmap getScaleOneImageForPreset(ImageShow caller, ImagePreset imagePreset, Rect bounds,
388 Rect destination, boolean force) {
390 Bitmap bmp = mZoomCache.getImage(imagePreset, bounds);
391 if (force || bmp == null) {
392 BitmapFactory.Options options = new BitmapFactory.Options();
393 options.inMutable = true;
394 if (destination != null) {
395 if (bounds.width() > destination.width()) {
397 int w = bounds.width();
398 while (w > destination.width()) {
402 options.inSampleSize = sampleSize;
405 bmp = loadRegionBitmap(mUri, options, bounds);
406 if (destination != null) {
407 mLoadingLock.unlock();
411 float scaleFactor = imagePreset.getScaleFactor();
412 imagePreset.setScaleFactor(1.0f);
413 bmp = imagePreset.apply(bmp);
414 imagePreset.setScaleFactor(scaleFactor);
415 mZoomCache.setImage(imagePreset, bounds, bmp);
416 mLoadingLock.unlock();
420 mLoadingLock.unlock();
424 public void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity,
426 preset.setQuality(ImagePreset.QUALITY_FINAL);
427 preset.setScaleFactor(1.0f);
428 new SaveCopyTask(mContext, mUri, destination, new SaveCopyTask.Callback() {
431 public void onComplete(Uri result) {
432 filterShowActivity.completeSaveImage(result);
438 public static Bitmap loadMutableBitmap(Context context, Uri sourceUri) {
439 BitmapFactory.Options options = new BitmapFactory.Options();
440 return loadMutableBitmap(context, sourceUri, options);
443 public static Bitmap loadMutableBitmap(Context context, Uri sourceUri,
444 BitmapFactory.Options options) {
445 // TODO: on <3.x we need a copy of the bitmap (inMutable doesn't
447 options.inMutable = true;
449 Bitmap bitmap = decodeUriWithBackouts(context, sourceUri, options);
450 if (bitmap == null) {
453 int orientation = ImageLoader.getOrientation(context, sourceUri);
454 bitmap = ImageLoader.rotateToPortrait(bitmap, orientation);
458 public static Bitmap decodeUriWithBackouts(Context context, Uri sourceUri,
459 BitmapFactory.Options options) {
460 boolean noBitmap = true;
462 InputStream is = getInputStream(context, sourceUri);
464 if (options.inSampleSize < 1) {
465 options.inSampleSize = 1;
467 // Stopgap fix for low-memory devices.
474 // Try to decode, downsample if low-memory.
475 bmap = BitmapFactory.decodeStream(is, null, options);
477 } catch (java.lang.OutOfMemoryError e) {
478 // Try 5 times before failing for good.
479 if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) {
485 is = getInputStream(context, sourceUri);
486 options.inSampleSize *= 2;
489 Utils.closeSilently(is);
493 private static InputStream getInputStream(Context context, Uri sourceUri) {
494 InputStream is = null;
496 is = context.getContentResolver().openInputStream(sourceUri);
497 } catch (FileNotFoundException e) {
498 Log.w(LOGTAG, "could not load bitmap ", e);
499 Utils.closeSilently(is);
505 public static Bitmap decodeResourceWithBackouts(Resources res, BitmapFactory.Options options,
507 boolean noBitmap = true;
509 if (options.inSampleSize < 1) {
510 options.inSampleSize = 1;
512 // Stopgap fix for low-memory devices.
516 // Try to decode, downsample if low-memory.
517 bmap = BitmapFactory.decodeResource(
520 } catch (java.lang.OutOfMemoryError e) {
521 // Try 5 times before failing for good.
522 if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) {
527 options.inSampleSize *= 2;
533 public void returnFilteredResult(ImagePreset preset,
534 final FilterShowActivity filterShowActivity) {
535 preset.setQuality(ImagePreset.QUALITY_FINAL);
536 preset.setScaleFactor(1.0f);
538 BitmapTask.Callbacks<ImagePreset> cb = new BitmapTask.Callbacks<ImagePreset>() {
541 public void onComplete(Bitmap result) {
542 filterShowActivity.onFilteredResult(result);
546 public void onCancel() {
550 public Bitmap onExecute(ImagePreset param) {
551 if (param == null || mUri == null) {
554 BitmapFactory.Options options = new BitmapFactory.Options();
555 boolean noBitmap = true;
557 if (options.inSampleSize < 1) {
558 options.inSampleSize = 1;
560 Bitmap bitmap = null;
561 // Stopgap fix for low-memory devices.
564 // Try to do bitmap operations, downsample if low-memory
565 bitmap = loadMutableBitmap(mContext, mUri, options);
566 if (bitmap == null) {
567 Log.w(LOGTAG, "Failed to save image!");
570 bitmap = param.applyGeometry(bitmap);
571 bitmap = param.apply(bitmap);
573 } catch (java.lang.OutOfMemoryError e) {
574 // Try 5 times before failing for good.
575 if (++num_tries >= 5) {
580 options.inSampleSize *= 2;
587 (new BitmapTask<ImagePreset>(cb)).execute(preset);
590 private String getFileExtension(String requestFormat) {
591 String outputFormat = (requestFormat == null)
594 outputFormat = outputFormat.toLowerCase();
595 return (outputFormat.equals("png") || outputFormat.equals("gif"))
596 ? "png" // We don't support gif compression.
600 private CompressFormat convertExtensionToCompressFormat(String extension) {
601 return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG;
604 public void saveToUri(Bitmap bmap, Uri uri, final String outputFormat,
605 final FilterShowActivity filterShowActivity) {
607 OutputStream out = null;
609 out = filterShowActivity.getContentResolver().openOutputStream(uri);
610 } catch (FileNotFoundException e) {
611 Log.w(LOGTAG, "cannot write output", e);
614 if (bmap == null || out == null) {
619 final InterruptableOutputStream ios = new InterruptableOutputStream(out);
621 BitmapTask.Callbacks<Bitmap> cb = new BitmapTask.Callbacks<Bitmap>() {
624 public void onComplete(Bitmap result) {
625 filterShowActivity.done();
629 public void onCancel() {
634 public Bitmap onExecute(Bitmap param) {
635 CompressFormat cf = convertExtensionToCompressFormat(getFileExtension(outputFormat));
636 param.compress(cf, DEFAULT_COMPRESS_QUALITY, ios);
637 Utils.closeSilently(ios);
642 (new BitmapTask<Bitmap>(cb)).execute(bmap);
645 public void setAdapter(HistoryAdapter adapter) {
649 public HistoryAdapter getHistory() {
653 public XMPMeta getXmpObject() {
655 InputStream is = mContext.getContentResolver().openInputStream(getUri());
656 return XmpUtilHelper.extractXMPMeta(is);
657 } catch (FileNotFoundException e) {
663 * Determine if this is a light cycle 360 image
665 * @return true if it is a light Cycle image that is full 360
667 public boolean queryLightCycle360() {
668 InputStream is = null;
670 is = mContext.getContentResolver().openInputStream(getUri());
671 XMPMeta meta = XmpUtilHelper.extractXMPMeta(is);
675 String name = meta.getPacketHeader();
676 String namespace = "http://ns.google.com/photos/1.0/panorama/";
677 String cropWidthName = "GPano:CroppedAreaImageWidthPixels";
678 String fullWidthName = "GPano:FullPanoWidthPixels";
680 if (!meta.doesPropertyExist(namespace, cropWidthName)) {
683 if (!meta.doesPropertyExist(namespace, fullWidthName)) {
687 Integer cropValue = meta.getPropertyInteger(namespace, cropWidthName);
688 Integer fullValue = meta.getPropertyInteger(namespace, fullWidthName);
690 // Definition of a 360:
691 // GFullPanoWidthPixels == CroppedAreaImageWidthPixels
692 if (cropValue != null && fullValue != null) {
693 return cropValue.equals(fullValue);
697 } catch (FileNotFoundException e) {
699 } catch (XMPException e) {
702 Utils.closeSilently(is);