ImageLoader.java revision a0d689f180db4c995d5be39a326b1fb5b21ff5c0
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
17package com.android.gallery3d.filtershow.cache;
18
19import android.content.ContentResolver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.res.Resources;
23import android.database.Cursor;
24import android.database.sqlite.SQLiteException;
25import android.graphics.Bitmap;
26import android.graphics.BitmapFactory;
27import android.graphics.BitmapRegionDecoder;
28import android.graphics.Matrix;
29import android.graphics.Rect;
30import android.graphics.Bitmap.CompressFormat;
31import android.media.ExifInterface;
32import android.net.Uri;
33import android.provider.MediaStore;
34import android.util.Log;
35
36import com.adobe.xmp.XMPException;
37import com.adobe.xmp.XMPMeta;
38
39import com.android.gallery3d.R;
40import com.android.gallery3d.common.Utils;
41import com.android.gallery3d.exif.ExifInvalidFormatException;
42import com.android.gallery3d.exif.ExifParser;
43import com.android.gallery3d.exif.ExifTag;
44import com.android.gallery3d.filtershow.CropExtras;
45import com.android.gallery3d.filtershow.FilterShowActivity;
46import com.android.gallery3d.filtershow.HistoryAdapter;
47import com.android.gallery3d.filtershow.imageshow.ImageCrop;
48import com.android.gallery3d.filtershow.imageshow.ImageShow;
49import com.android.gallery3d.filtershow.presets.ImagePreset;
50import com.android.gallery3d.filtershow.tools.BitmapTask;
51import com.android.gallery3d.filtershow.tools.SaveCopyTask;
52import com.android.gallery3d.util.InterruptableOutputStream;
53import com.android.gallery3d.util.XmpUtilHelper;
54
55import java.io.Closeable;
56import java.io.File;
57import java.io.FileInputStream;
58import java.io.FileNotFoundException;
59import java.io.IOException;
60import java.io.InputStream;
61import java.io.OutputStream;
62import java.util.Vector;
63import java.util.concurrent.locks.ReentrantLock;
64
65public 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 = 900;
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.setQuality(ImagePreset.QUALITY_FINAL);
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.setQuality(ImagePreset.QUALITY_FINAL);
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 || mUri == 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}
582