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