Home | History | Annotate | Download | only in cache
      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;
     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      */
     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 
     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      */
     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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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      */
    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      */
    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      */
    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 
    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      */
    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 
    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