ImageLoader.java revision 860af325f2030a03c526e8551a85230d17df7b15
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.res.Resources;
22import android.database.Cursor;
23import android.database.sqlite.SQLiteException;
24import android.graphics.Bitmap;
25import android.graphics.BitmapFactory;
26import android.graphics.BitmapRegionDecoder;
27import android.graphics.Matrix;
28import android.graphics.Rect;
29import android.net.Uri;
30import android.provider.MediaStore;
31import android.util.Log;
32import android.webkit.MimeTypeMap;
33
34import com.adobe.xmp.XMPException;
35import com.adobe.xmp.XMPMeta;
36import com.android.gallery3d.common.Utils;
37import com.android.gallery3d.exif.ExifInterface;
38import com.android.gallery3d.exif.ExifTag;
39import com.android.gallery3d.filtershow.imageshow.MasterImage;
40import com.android.gallery3d.filtershow.tools.XmpPresets;
41import com.android.gallery3d.util.XmpUtilHelper;
42
43import java.io.FileNotFoundException;
44import java.io.IOException;
45import java.io.InputStream;
46import java.util.List;
47
48public final class ImageLoader {
49
50    private static final String LOGTAG = "ImageLoader";
51
52    public static final String JPEG_MIME_TYPE = "image/jpeg";
53    public static final int DEFAULT_COMPRESS_QUALITY = 95;
54
55    public static final int ORI_NORMAL = ExifInterface.Orientation.TOP_LEFT;
56    public static final int ORI_ROTATE_90 = ExifInterface.Orientation.RIGHT_TOP;
57    public static final int ORI_ROTATE_180 = ExifInterface.Orientation.BOTTOM_LEFT;
58    public static final int ORI_ROTATE_270 = ExifInterface.Orientation.RIGHT_BOTTOM;
59    public static final int ORI_FLIP_HOR = ExifInterface.Orientation.TOP_RIGHT;
60    public static final int ORI_FLIP_VERT = ExifInterface.Orientation.BOTTOM_RIGHT;
61    public static final int ORI_TRANSPOSE = ExifInterface.Orientation.LEFT_TOP;
62    public static final int ORI_TRANSVERSE = ExifInterface.Orientation.LEFT_BOTTOM;
63
64    private static final int BITMAP_LOAD_BACKOUT_ATTEMPTS = 5;
65
66    private ImageLoader() {}
67
68    /**
69     * Returns the Mime type for a Url.  Safe to use with Urls that do not
70     * come from Gallery's content provider.
71     */
72    public static String getMimeType(Uri src) {
73        String postfix = MimeTypeMap.getFileExtensionFromUrl(src.toString());
74        String ret = null;
75        if (postfix != null) {
76            ret = MimeTypeMap.getSingleton().getMimeTypeFromExtension(postfix);
77        }
78        return ret;
79    }
80
81    public static String getLocalPathFromUri(Context context, Uri uri) {
82        Cursor cursor = context.getContentResolver().query(uri,
83                new String[]{MediaStore.Images.Media.DATA}, null, null, null);
84        int index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
85        cursor.moveToFirst();
86        return cursor.getString(index);
87    }
88
89    /**
90     * Returns the image's orientation flag.  Defaults to ORI_NORMAL if no valid
91     * orientation was found.
92     */
93    public static int getMetadataOrientation(Context context, Uri uri) {
94        if (uri == null || context == null) {
95            throw new IllegalArgumentException("bad argument to getOrientation");
96        }
97
98        // First try to find orientation data in Gallery's ContentProvider.
99        Cursor cursor = null;
100        try {
101            cursor = context.getContentResolver().query(uri,
102                    new String[] { MediaStore.Images.ImageColumns.ORIENTATION },
103                    null, null, null);
104            if (cursor != null && cursor.moveToNext()) {
105                int ori = cursor.getInt(0);
106                switch (ori) {
107                    case 90:
108                        return ORI_ROTATE_90;
109                    case 270:
110                        return ORI_ROTATE_270;
111                    case 180:
112                        return ORI_ROTATE_180;
113                    default:
114                        return ORI_NORMAL;
115                }
116            }
117        } catch (SQLiteException e) {
118            // Do nothing
119        } catch (IllegalArgumentException e) {
120            // Do nothing
121        } finally {
122            Utils.closeSilently(cursor);
123        }
124
125        // Fall back to checking EXIF tags in file.
126        if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
127            String mimeType = getMimeType(uri);
128            if (!JPEG_MIME_TYPE.equals(mimeType)) {
129                return ORI_NORMAL;
130            }
131            String path = uri.getPath();
132            ExifInterface exif = new ExifInterface();
133            try {
134                exif.readExif(path);
135                Integer tagval = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION);
136                if (tagval != null) {
137                    int orientation = tagval;
138                    switch(orientation) {
139                        case ORI_NORMAL:
140                        case ORI_ROTATE_90:
141                        case ORI_ROTATE_180:
142                        case ORI_ROTATE_270:
143                        case ORI_FLIP_HOR:
144                        case ORI_FLIP_VERT:
145                        case ORI_TRANSPOSE:
146                        case ORI_TRANSVERSE:
147                            return orientation;
148                        default:
149                            return ORI_NORMAL;
150                    }
151                }
152            } catch (IOException e) {
153                Log.w(LOGTAG, "Failed to read EXIF orientation", e);
154            }
155        }
156        return ORI_NORMAL;
157    }
158
159    /**
160     * Returns the rotation of image at the given URI as one of 0, 90, 180,
161     * 270.  Defaults to 0.
162     */
163    public static int getMetadataRotation(Context context, Uri uri) {
164        int orientation = getMetadataOrientation(context, uri);
165        switch(orientation) {
166            case ORI_ROTATE_90:
167                return 90;
168            case ORI_ROTATE_180:
169                return 180;
170            case ORI_ROTATE_270:
171                return 270;
172            default:
173                return 0;
174        }
175    }
176
177    /**
178     * Takes an orientation and a bitmap, and returns the bitmap transformed
179     * to that orientation.
180     */
181    public static Bitmap orientBitmap(Bitmap bitmap, int ori) {
182        Matrix matrix = new Matrix();
183        int w = bitmap.getWidth();
184        int h = bitmap.getHeight();
185        if (ori == ORI_ROTATE_90 ||
186                ori == ORI_ROTATE_270 ||
187                ori == ORI_TRANSPOSE ||
188                ori == ORI_TRANSVERSE) {
189            int tmp = w;
190            w = h;
191            h = tmp;
192        }
193        switch (ori) {
194            case ORI_ROTATE_90:
195                matrix.setRotate(90, w / 2f, h / 2f);
196                break;
197            case ORI_ROTATE_180:
198                matrix.setRotate(180, w / 2f, h / 2f);
199                break;
200            case ORI_ROTATE_270:
201                matrix.setRotate(270, w / 2f, h / 2f);
202                break;
203            case ORI_FLIP_HOR:
204                matrix.preScale(-1, 1);
205                break;
206            case ORI_FLIP_VERT:
207                matrix.preScale(1, -1);
208                break;
209            case ORI_TRANSPOSE:
210                matrix.setRotate(90, w / 2f, h / 2f);
211                matrix.preScale(1, -1);
212                break;
213            case ORI_TRANSVERSE:
214                matrix.setRotate(270, w / 2f, h / 2f);
215                matrix.preScale(1, -1);
216                break;
217            case ORI_NORMAL:
218            default:
219                return bitmap;
220        }
221        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
222                bitmap.getHeight(), matrix, true);
223    }
224
225    /**
226     * Returns the bitmap for the rectangular region given by "bounds"
227     * if it is a subset of the bitmap stored at uri.  Otherwise returns
228     * null.
229     */
230    public static Bitmap loadRegionBitmap(Context context, Uri uri, BitmapFactory.Options options,
231            Rect bounds) {
232        InputStream is = null;
233        try {
234            is = context.getContentResolver().openInputStream(uri);
235            BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
236            Rect r = new Rect(0, 0, decoder.getWidth(), decoder.getHeight());
237            // return null if bounds are not entirely within the bitmap
238            if (!r.contains(bounds)) {
239                return null;
240            }
241            return decoder.decodeRegion(bounds, options);
242        } catch (FileNotFoundException e) {
243            Log.e(LOGTAG, "FileNotFoundException for " + uri, e);
244        } catch (IOException e) {
245            Log.e(LOGTAG, "FileNotFoundException for " + uri, e);
246        } finally {
247            Utils.closeSilently(is);
248        }
249        return null;
250    }
251
252    /**
253     * Returns the bounds of the bitmap stored at a given Url.
254     */
255    public static Rect loadBitmapBounds(Context context, Uri uri) {
256        BitmapFactory.Options o = new BitmapFactory.Options();
257        loadBitmap(context, uri, o);
258        return new Rect(0, 0, o.outWidth, o.outHeight);
259    }
260
261    /**
262     * Loads a bitmap that has been downsampled using sampleSize from a given url.
263     */
264    public static Bitmap loadDownsampledBitmap(Context context, Uri uri, int sampleSize) {
265        BitmapFactory.Options options = new BitmapFactory.Options();
266        options.inMutable = true;
267        options.inSampleSize = sampleSize;
268        return loadBitmap(context, uri, options);
269    }
270
271
272    /**
273     * Returns the bitmap from the given uri loaded using the given options.
274     * Returns null on failure.
275     */
276    public static Bitmap loadBitmap(Context context, Uri uri, BitmapFactory.Options o) {
277        if (uri == null || context == null) {
278            throw new IllegalArgumentException("bad argument to loadBitmap");
279        }
280        InputStream is = null;
281        try {
282            is = context.getContentResolver().openInputStream(uri);
283            return BitmapFactory.decodeStream(is, null, o);
284        } catch (FileNotFoundException e) {
285            Log.e(LOGTAG, "FileNotFoundException for " + uri, e);
286        } finally {
287            Utils.closeSilently(is);
288        }
289        return null;
290    }
291
292    /**
293     * Loads a bitmap at a given URI that is downsampled so that both sides are
294     * smaller than maxSideLength. The Bitmap's original dimensions are stored
295     * in the rect originalBounds.
296     *
297     * @param uri URI of image to open.
298     * @param context context whose ContentResolver to use.
299     * @param maxSideLength max side length of returned bitmap.
300     * @param originalBounds If not null, set to the actual bounds of the stored bitmap.
301     * @param useMin use min or max side of the original image
302     * @return downsampled bitmap or null if this operation failed.
303     */
304    public static Bitmap loadConstrainedBitmap(Uri uri, Context context, int maxSideLength,
305            Rect originalBounds, boolean useMin) {
306        if (maxSideLength <= 0 || uri == null || context == null) {
307            throw new IllegalArgumentException("bad argument to getScaledBitmap");
308        }
309        // Get width and height of stored bitmap
310        Rect storedBounds = loadBitmapBounds(context, uri);
311        if (originalBounds != null) {
312            originalBounds.set(storedBounds);
313        }
314        int w = storedBounds.width();
315        int h = storedBounds.height();
316
317        // If bitmap cannot be decoded, return null
318        if (w <= 0 || h <= 0) {
319            return null;
320        }
321
322        // Find best downsampling size
323        int imageSide = 0;
324        if (useMin) {
325            imageSide = Math.min(w, h);
326        } else {
327            imageSide = Math.max(w, h);
328        }
329        int sampleSize = 1;
330        while (imageSide > maxSideLength) {
331            imageSide >>>= 1;
332            sampleSize <<= 1;
333        }
334
335        // Make sure sample size is reasonable
336        if (sampleSize <= 0 ||
337                0 >= (int) (Math.min(w, h) / sampleSize)) {
338            return null;
339        }
340        return loadDownsampledBitmap(context, uri, sampleSize);
341    }
342
343    /**
344     * Loads a bitmap at a given URI that is downsampled so that both sides are
345     * smaller than maxSideLength. The Bitmap's original dimensions are stored
346     * in the rect originalBounds.  The output is also transformed to the given
347     * orientation.
348     *
349     * @param uri URI of image to open.
350     * @param context context whose ContentResolver to use.
351     * @param maxSideLength max side length of returned bitmap.
352     * @param orientation  the orientation to transform the bitmap to.
353     * @param originalBounds set to the actual bounds of the stored bitmap.
354     * @return downsampled bitmap or null if this operation failed.
355     */
356    public static Bitmap loadOrientedConstrainedBitmap(Uri uri, Context context, int maxSideLength,
357            int orientation, Rect originalBounds) {
358        Bitmap bmap = loadConstrainedBitmap(uri, context, maxSideLength, originalBounds, false);
359        if (bmap != null) {
360            bmap = orientBitmap(bmap, orientation);
361        }
362        return bmap;
363    }
364
365    public static Bitmap getScaleOneImageForPreset(Context context, Uri uri, Rect bounds,
366            Rect destination) {
367        BitmapFactory.Options options = new BitmapFactory.Options();
368        options.inMutable = true;
369        if (destination != null) {
370            if (bounds.width() > destination.width()) {
371                int sampleSize = 1;
372                int w = bounds.width();
373                while (w > destination.width()) {
374                    sampleSize *= 2;
375                    w /= sampleSize;
376                }
377                options.inSampleSize = sampleSize;
378            }
379        }
380        Bitmap bmp = loadRegionBitmap(context, uri, options, bounds);
381        return bmp;
382    }
383
384    /**
385     * Loads a bitmap that is downsampled by at least the input sample size. In
386     * low-memory situations, the bitmap may be downsampled further.
387     */
388    public static Bitmap loadBitmapWithBackouts(Context context, Uri sourceUri, int sampleSize) {
389        boolean noBitmap = true;
390        int num_tries = 0;
391        if (sampleSize <= 0) {
392            sampleSize = 1;
393        }
394        Bitmap bmap = null;
395        while (noBitmap) {
396            try {
397                // Try to decode, downsample if low-memory.
398                bmap = loadDownsampledBitmap(context, sourceUri, sampleSize);
399                noBitmap = false;
400            } catch (java.lang.OutOfMemoryError e) {
401                // Try with more downsampling before failing for good.
402                if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) {
403                    throw e;
404                }
405                bmap = null;
406                System.gc();
407                sampleSize *= 2;
408            }
409        }
410        return bmap;
411    }
412
413    /**
414     * Loads an oriented bitmap that is downsampled by at least the input sample
415     * size. In low-memory situations, the bitmap may be downsampled further.
416     */
417    public static Bitmap loadOrientedBitmapWithBackouts(Context context, Uri sourceUri,
418            int sampleSize) {
419        Bitmap bitmap = loadBitmapWithBackouts(context, sourceUri, sampleSize);
420        if (bitmap == null) {
421            return null;
422        }
423        int orientation = getMetadataOrientation(context, sourceUri);
424        bitmap = orientBitmap(bitmap, orientation);
425        return bitmap;
426    }
427
428    /**
429     * Loads bitmap from a resource that may be downsampled in low-memory situations.
430     */
431    public static Bitmap decodeResourceWithBackouts(Resources res, BitmapFactory.Options options,
432            int id) {
433        boolean noBitmap = true;
434        int num_tries = 0;
435        if (options.inSampleSize < 1) {
436            options.inSampleSize = 1;
437        }
438        // Stopgap fix for low-memory devices.
439        Bitmap bmap = null;
440        while (noBitmap) {
441            try {
442                // Try to decode, downsample if low-memory.
443                bmap = BitmapFactory.decodeResource(
444                        res, id, options);
445                noBitmap = false;
446            } catch (java.lang.OutOfMemoryError e) {
447                // Retry before failing for good.
448                if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) {
449                    throw e;
450                }
451                bmap = null;
452                System.gc();
453                options.inSampleSize *= 2;
454            }
455        }
456        return bmap;
457    }
458
459    public static XMPMeta getXmpObject(Context context) {
460        try {
461            InputStream is = context.getContentResolver().openInputStream(
462                    MasterImage.getImage().getUri());
463            return XmpUtilHelper.extractXMPMeta(is);
464        } catch (FileNotFoundException e) {
465            return null;
466        }
467    }
468
469    /**
470     * Determine if this is a light cycle 360 image
471     *
472     * @return true if it is a light Cycle image that is full 360
473     */
474    public static boolean queryLightCycle360(Context context) {
475        InputStream is = null;
476        try {
477            is = context.getContentResolver().openInputStream(MasterImage.getImage().getUri());
478            XMPMeta meta = XmpUtilHelper.extractXMPMeta(is);
479            if (meta == null) {
480                return false;
481            }
482            String namespace = "http://ns.google.com/photos/1.0/panorama/";
483            String cropWidthName = "GPano:CroppedAreaImageWidthPixels";
484            String fullWidthName = "GPano:FullPanoWidthPixels";
485
486            if (!meta.doesPropertyExist(namespace, cropWidthName)) {
487                return false;
488            }
489            if (!meta.doesPropertyExist(namespace, fullWidthName)) {
490                return false;
491            }
492
493            Integer cropValue = meta.getPropertyInteger(namespace, cropWidthName);
494            Integer fullValue = meta.getPropertyInteger(namespace, fullWidthName);
495
496            // Definition of a 360:
497            // GFullPanoWidthPixels == CroppedAreaImageWidthPixels
498            if (cropValue != null && fullValue != null) {
499                return cropValue.equals(fullValue);
500            }
501
502            return false;
503        } catch (FileNotFoundException e) {
504            return false;
505        } catch (XMPException e) {
506            return false;
507        } finally {
508            Utils.closeSilently(is);
509        }
510    }
511
512    public static List<ExifTag> getExif(Context context, Uri uri) {
513        String path = getLocalPathFromUri(context, uri);
514        if (path != null) {
515            Uri localUri = Uri.parse(path);
516            String mimeType = getMimeType(localUri);
517            if (!JPEG_MIME_TYPE.equals(mimeType)) {
518                return null;
519            }
520            try {
521                ExifInterface exif = new ExifInterface();
522                exif.readExif(path);
523                List<ExifTag> taglist = exif.getAllTags();
524                return taglist;
525            } catch (IOException e) {
526                Log.w(LOGTAG, "Failed to read EXIF tags", e);
527            }
528        }
529        return null;
530    }
531}
532