OSDN Git Service

Merge "Fix Fx filters" into gb-ub-photos-bryce
[android-x86/packages-apps-Gallery2.git] / src / com / android / gallery3d / filtershow / cache / ImageLoader.java
1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package com.android.gallery3d.filtershow.cache;
18
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;
35
36 import com.adobe.xmp.XMPException;
37 import com.adobe.xmp.XMPMeta;
38
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;
54
55 import java.io.Closeable;
56 import java.io.File;
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;
64
65 public class ImageLoader {
66
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;
72
73     private final ZoomCache mZoomCache = new ZoomCache();
74
75     private int mOrientation = 0;
76     private HistoryAdapter mAdapter = null;
77
78     private FilterShowActivity mActivity = null;
79
80     public static final String JPEG_MIME_TYPE = "image/jpeg";
81
82     public static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos";
83     public static final int DEFAULT_COMPRESS_QUALITY = 95;
84
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;
93
94     private Context mContext = null;
95     private Uri mUri = null;
96
97     private Rect mOriginalBounds = null;
98     private static int mZoomOrientation = ORI_NORMAL;
99
100     private ReentrantLock mLoadingLock = new ReentrantLock();
101
102     public ImageLoader(FilterShowActivity activity, Context context) {
103         mActivity = activity;
104         mContext = context;
105     }
106
107     public static int getZoomOrientation() {
108         return mZoomOrientation;
109     }
110
111     public FilterShowActivity getActivity() {
112         return mActivity;
113     }
114
115     public boolean loadBitmap(Uri uri, int size) {
116         mLoadingLock.lock();
117         mUri = uri;
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();
123             return false;
124         }
125         mOriginalBitmapLarge = loadScaledBitmap(uri, size);
126         if (mOriginalBitmapLarge == null) {
127             mLoadingLock.unlock();
128             return false;
129         }
130         updateBitmaps();
131         mLoadingLock.unlock();
132         return true;
133     }
134
135     public Uri getUri() {
136         return mUri;
137     }
138
139     public Rect getOriginalBounds() {
140         return mOriginalBounds;
141     }
142
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) {
147                 return -1;
148             }
149             String path = uri.getPath();
150             int orientation = -1;
151             InputStream is = null;
152             try {
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);
161                             break;
162                         }
163                     }
164                     event = parser.next();
165                 }
166             } catch (IOException e) {
167                 e.printStackTrace();
168             } catch (ExifInvalidFormatException e) {
169                 e.printStackTrace();
170             } finally {
171                 Utils.closeSilently(is);
172             }
173             return orientation;
174         }
175         Cursor cursor = null;
176         try {
177             cursor = context.getContentResolver().query(uri,
178                     new String[] {
179                         MediaStore.Images.ImageColumns.ORIENTATION
180                     },
181                     null, null, null);
182             if (cursor.moveToNext()) {
183                 int ori = cursor.getInt(0);
184
185                 switch (ori) {
186                     case 0:
187                         return ORI_NORMAL;
188                     case 90:
189                         return ORI_ROTATE_90;
190                     case 270:
191                         return ORI_ROTATE_270;
192                     case 180:
193                         return ORI_ROTATE_180;
194                     default:
195                         return -1;
196                 }
197             } else {
198                 return -1;
199             }
200         } catch (SQLiteException e) {
201             return ExifInterface.ORIENTATION_UNDEFINED;
202         } catch (IllegalArgumentException e) {
203             return ExifInterface.ORIENTATION_UNDEFINED;
204         } finally {
205             Utils.closeSilently(cursor);
206         }
207     }
208
209     private void updateBitmaps() {
210         if (mOrientation > 1) {
211             mOriginalBitmapSmall = rotateToPortrait(mOriginalBitmapSmall, mOrientation);
212             mOriginalBitmapLarge = rotateToPortrait(mOriginalBitmapLarge, mOrientation);
213         }
214         mZoomOrientation = mOrientation;
215         warnListeners();
216     }
217
218     public Bitmap decodeImage(int id, BitmapFactory.Options options) {
219         return BitmapFactory.decodeResource(mContext.getResources(), id, options);
220     }
221
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) {
230             int tmp = w;
231             w = h;
232             h = tmp;
233         }
234         switch (ori) {
235             case ORI_ROTATE_90:
236                 matrix.setRotate(90, w / 2f, h / 2f);
237                 break;
238             case ORI_ROTATE_180:
239                 matrix.setRotate(180, w / 2f, h / 2f);
240                 break;
241             case ORI_ROTATE_270:
242                 matrix.setRotate(270, w / 2f, h / 2f);
243                 break;
244             case ORI_FLIP_HOR:
245                 matrix.preScale(-1, 1);
246                 break;
247             case ORI_FLIP_VERT:
248                 matrix.preScale(1, -1);
249                 break;
250             case ORI_TRANSPOSE:
251                 matrix.setRotate(90, w / 2f, h / 2f);
252                 matrix.preScale(1, -1);
253                 break;
254             case ORI_TRANSVERSE:
255                 matrix.setRotate(270, w / 2f, h / 2f);
256                 matrix.preScale(1, -1);
257                 break;
258             case ORI_NORMAL:
259             default:
260                 return bitmap;
261         }
262
263         return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
264                 bitmap.getHeight(), matrix, true);
265     }
266
267     private Bitmap loadRegionBitmap(Uri uri, Rect bounds) {
268         InputStream is = null;
269         try {
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) {
276             e.printStackTrace();
277         } finally {
278             Utils.closeSilently(is);
279         }
280         return null;
281     }
282
283     static final int MAX_BITMAP_DIM = 2048;
284
285     private Bitmap loadScaledBitmap(Uri uri, int size) {
286         InputStream is = null;
287         try {
288             is = mContext.getContentResolver().openInputStream(uri);
289             Log.v(LOGTAG, "loading uri " + uri.getPath() + " input stream: "
290                     + is);
291             BitmapFactory.Options o = new BitmapFactory.Options();
292             o.inJustDecodeBounds = true;
293             BitmapFactory.decodeStream(is, null, o);
294
295             int width_tmp = o.outWidth;
296             int height_tmp = o.outHeight;
297
298             mOriginalBounds = new Rect(0, 0, width_tmp, height_tmp);
299
300             int scale = 1;
301             while (true) {
302                 if (width_tmp <= MAX_BITMAP_DIM && height_tmp <= MAX_BITMAP_DIM) {
303                     if (width_tmp / 2 < size || height_tmp / 2 < size) {
304                         break;
305                     }
306                 }
307                 width_tmp /= 2;
308                 height_tmp /= 2;
309                 scale *= 2;
310             }
311
312             // decode with inSampleSize
313             BitmapFactory.Options o2 = new BitmapFactory.Options();
314             o2.inSampleSize = scale;
315
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) {
322             e.printStackTrace();
323         } finally {
324             Utils.closeSilently(is);
325         }
326         return null;
327     }
328
329     public Bitmap getBackgroundBitmap(Resources resources) {
330         if (mBackgroundBitmap == null) {
331             mBackgroundBitmap = BitmapFactory.decodeResource(resources,
332                     R.drawable.filtershow_background);
333         }
334         return mBackgroundBitmap;
335
336     }
337
338     public Bitmap getOriginalBitmapSmall() {
339         return mOriginalBitmapSmall;
340     }
341
342     public Bitmap getOriginalBitmapLarge() {
343         return mOriginalBitmapLarge;
344     }
345
346     public void addListener(ImageShow imageShow) {
347         mLoadingLock.lock();
348         if (!mListeners.contains(imageShow)) {
349             mListeners.add(imageShow);
350         }
351         mLoadingLock.unlock();
352     }
353
354     private void warnListeners() {
355         mActivity.runOnUiThread(mWarnListenersRunnable);
356     }
357
358     private Runnable mWarnListenersRunnable = new Runnable() {
359
360         @Override
361         public void run() {
362             for (int i = 0; i < mListeners.size(); i++) {
363                 ImageShow imageShow = mListeners.elementAt(i);
364                 imageShow.imageLoaded();
365             }
366         }
367     };
368
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,
372             boolean force) {
373         mLoadingLock.lock();
374         Bitmap bmp = mZoomCache.getImage(imagePreset, bounds);
375         if (force || bmp == null) {
376             bmp = loadRegionBitmap(mUri, bounds);
377             if (bmp != null) {
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();
386                 return bmp2;
387             }
388         }
389         mLoadingLock.unlock();
390         return bmp;
391     }
392
393     public void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity,
394             File destination) {
395         preset.setIsHighQuality(true);
396         preset.setScaleFactor(1.0f);
397         new SaveCopyTask(mContext, mUri, destination, new SaveCopyTask.Callback() {
398
399             @Override
400             public void onComplete(Uri result) {
401                 filterShowActivity.completeSaveImage(result);
402             }
403
404         }).execute(preset);
405     }
406
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
410         // exist)
411         options.inMutable = true;
412
413         InputStream is = null;
414         Bitmap bitmap = null;
415         try {
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);
420             is = null;
421             bitmap = null;
422         } finally {
423             Utils.closeSilently(is);
424         }
425         if (bitmap == null) {
426             return null;
427         }
428         int orientation = ImageLoader.getOrientation(context, sourceUri);
429         bitmap = ImageLoader.rotateToPortrait(bitmap, orientation);
430         return bitmap;
431     }
432
433     public void returnFilteredResult(ImagePreset preset,
434             final FilterShowActivity filterShowActivity) {
435         preset.setIsHighQuality(true);
436         preset.setScaleFactor(1.0f);
437
438         BitmapTask.Callbacks<ImagePreset> cb = new BitmapTask.Callbacks<ImagePreset>() {
439
440             @Override
441             public void onComplete(Bitmap result) {
442                 filterShowActivity.onFilteredResult(result);
443             }
444
445             @Override
446             public void onCancel() {
447             }
448
449             @Override
450             public Bitmap onExecute(ImagePreset param) {
451                 if (param == null) {
452                     return null;
453                 }
454                 Bitmap bitmap = loadMutableBitmap(mContext, mUri);
455                 if (bitmap == null) {
456                     Log.w(LOGTAG, "Failed to save image!");
457                     return null;
458                 }
459                 return param.apply(bitmap);
460             }
461         };
462
463         (new BitmapTask<ImagePreset>(cb)).execute(preset);
464     }
465
466     private String getFileExtension(String requestFormat) {
467         String outputFormat = (requestFormat == null)
468                 ? "jpg"
469                 : requestFormat;
470         outputFormat = outputFormat.toLowerCase();
471         return (outputFormat.equals("png") || outputFormat.equals("gif"))
472                 ? "png" // We don't support gif compression.
473                 : "jpg";
474     }
475
476     private CompressFormat convertExtensionToCompressFormat(String extension) {
477         return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG;
478     }
479
480     public void saveToUri(Bitmap bmap, Uri uri, final String outputFormat,
481             final FilterShowActivity filterShowActivity) {
482
483         OutputStream out = null;
484         try {
485             out = filterShowActivity.getContentResolver().openOutputStream(uri);
486         } catch (FileNotFoundException e) {
487             Log.w(LOGTAG, "cannot write output", e);
488             out = null;
489         } finally {
490             if (bmap == null || out == null) {
491                 return;
492             }
493         }
494
495         final InterruptableOutputStream ios = new InterruptableOutputStream(out);
496
497         BitmapTask.Callbacks<Bitmap> cb = new BitmapTask.Callbacks<Bitmap>() {
498
499             @Override
500             public void onComplete(Bitmap result) {
501                 filterShowActivity.done();
502             }
503
504             @Override
505             public void onCancel() {
506                 ios.interrupt();
507             }
508
509             @Override
510             public Bitmap onExecute(Bitmap param) {
511                 CompressFormat cf = convertExtensionToCompressFormat(getFileExtension(outputFormat));
512                 param.compress(cf, DEFAULT_COMPRESS_QUALITY, ios);
513                 Utils.closeSilently(ios);
514                 return null;
515             }
516         };
517
518         (new BitmapTask<Bitmap>(cb)).execute(bmap);
519     }
520
521     public void setAdapter(HistoryAdapter adapter) {
522         mAdapter = adapter;
523     }
524
525     public HistoryAdapter getHistory() {
526         return mAdapter;
527     }
528
529     public XMPMeta getXmpObject() {
530         try {
531             InputStream is = mContext.getContentResolver().openInputStream(getUri());
532             return XmpUtilHelper.extractXMPMeta(is);
533         } catch (FileNotFoundException e) {
534             return null;
535         }
536     }
537
538     /**
539      * Determine if this is a light cycle 360 image
540      *
541      * @return true if it is a light Cycle image that is full 360
542      */
543     public boolean queryLightCycle360() {
544         InputStream is = null;
545         try {
546             is = mContext.getContentResolver().openInputStream(getUri());
547             XMPMeta meta = XmpUtilHelper.extractXMPMeta(is);
548             if (meta == null) {
549                 return false;
550             }
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";
555
556             if (!meta.doesPropertyExist(namespace, cropWidthName)) {
557                 return false;
558             }
559             if (!meta.doesPropertyExist(namespace, fullWidthName)) {
560                 return false;
561             }
562
563             Integer cropValue = meta.getPropertyInteger(namespace, cropWidthName);
564             Integer fullValue = meta.getPropertyInteger(namespace, fullWidthName);
565
566             // Definition of a 360:
567             // GFullPanoWidthPixels == CroppedAreaImageWidthPixels
568             if (cropValue != null && fullValue != null) {
569                 return cropValue.equals(fullValue);
570             }
571
572             return false;
573         } catch (FileNotFoundException e) {
574             return false;
575         } catch (XMPException e) {
576             return false;
577         } finally {
578             Utils.closeSilently(is);
579         }
580     }
581 }