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 
    133         // Fall back to checking EXIF tags in file.
    134         if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
    135             String mimeType = getMimeType(uri);
    136             if (!JPEG_MIME_TYPE.equals(mimeType)) {
    137                 return ORI_NORMAL;
    138             }
    139             String path = uri.getPath();
    140             ExifInterface exif = new ExifInterface();
    141             try {
    142                 exif.readExif(path);
    143                 Integer tagval = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION);
    144                 if (tagval != null) {
    145                     int orientation = tagval;
    146                     switch(orientation) {
    147                         case ORI_NORMAL:
    148                         case ORI_ROTATE_90:
    149                         case ORI_ROTATE_180:
    150                         case ORI_ROTATE_270:
    151                         case ORI_FLIP_HOR:
    152                         case ORI_FLIP_VERT:
    153                         case ORI_TRANSPOSE:
    154                         case ORI_TRANSVERSE:
    155                             return orientation;
    156                         default:
    157                             return ORI_NORMAL;
    158                     }
    159                 }
    160             } catch (IOException e) {
    161                 Log.w(LOGTAG, "Failed to read EXIF orientation", e);
    162             }
    163         }
    164         return ORI_NORMAL;
    165     }
    166 
    167     /**
    168      * Returns the rotation of image at the given URI as one of 0, 90, 180,
    169      * 270.  Defaults to 0.
    170      */
    171     public static int getMetadataRotation(Context context, Uri uri) {
    172         int orientation = getMetadataOrientation(context, uri);
    173         switch(orientation) {
    174             case ORI_ROTATE_90:
    175                 return 90;
    176             case ORI_ROTATE_180:
    177                 return 180;
    178             case ORI_ROTATE_270:
    179                 return 270;
    180             default:
    181                 return 0;
    182         }
    183     }
    184 
    185     /**
    186      * Takes an orientation and a bitmap, and returns the bitmap transformed
    187      * to that orientation.
    188      */
    189     public static Bitmap orientBitmap(Bitmap bitmap, int ori) {
    190         Matrix matrix = new Matrix();
    191         int w = bitmap.getWidth();
    192         int h = bitmap.getHeight();
    193         if (ori == ORI_ROTATE_90 ||
    194                 ori == ORI_ROTATE_270 ||
    195                 ori == ORI_TRANSPOSE ||
    196                 ori == ORI_TRANSVERSE) {
    197             int tmp = w;
    198             w = h;
    199             h = tmp;
    200         }
    201         switch (ori) {
    202             case ORI_ROTATE_90:
    203                 matrix.setRotate(90, w / 2f, h / 2f);
    204                 break;
    205             case ORI_ROTATE_180:
    206                 matrix.setRotate(180, w / 2f, h / 2f);
    207                 break;
    208             case ORI_ROTATE_270:
    209                 matrix.setRotate(270, w / 2f, h / 2f);
    210                 break;
    211             case ORI_FLIP_HOR:
    212                 matrix.preScale(-1, 1);
    213                 break;
    214             case ORI_FLIP_VERT:
    215                 matrix.preScale(1, -1);
    216                 break;
    217             case ORI_TRANSPOSE:
    218                 matrix.setRotate(90, w / 2f, h / 2f);
    219                 matrix.preScale(1, -1);
    220                 break;
    221             case ORI_TRANSVERSE:
    222                 matrix.setRotate(270, w / 2f, h / 2f);
    223                 matrix.preScale(1, -1);
    224                 break;
    225             case ORI_NORMAL:
    226             default:
    227                 return bitmap;
    228         }
    229         return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
    230                 bitmap.getHeight(), matrix, true);
    231     }
    232 
    233     /**
    234      * Returns the bitmap for the rectangular region given by "bounds"
    235      * if it is a subset of the bitmap stored at uri.  Otherwise returns
    236      * null.
    237      */
    238     public static Bitmap loadRegionBitmap(Context context, BitmapCache cache,
    239                                           Uri uri, BitmapFactory.Options options,
    240                                           Rect bounds) {
    241         InputStream is = null;
    242         int w = 0;
    243         int h = 0;
    244         if (options.inSampleSize != 0) {
    245             return null;
    246         }
    247         try {
    248             is = context.getContentResolver().openInputStream(uri);
    249             BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
    250             Rect r = new Rect(0, 0, decoder.getWidth(), decoder.getHeight());
    251             w = decoder.getWidth();
    252             h = decoder.getHeight();
    253             Rect imageBounds = new Rect(bounds);
    254             // return null if bounds are not entirely within the bitmap
    255             if (!r.contains(imageBounds)) {
    256                 imageBounds.intersect(r);
    257                 bounds.left = imageBounds.left;
    258                 bounds.top = imageBounds.top;
    259             }
    260             Bitmap reuse = cache.getBitmap(imageBounds.width(),
    261                     imageBounds.height(), BitmapCache.REGION);
    262             options.inBitmap = reuse;
    263             Bitmap bitmap = decoder.decodeRegion(imageBounds, options);
    264             if (bitmap != reuse) {
    265                 cache.cache(reuse); // not reused, put back in cache
    266             }
    267             return bitmap;
    268         } catch (FileNotFoundException e) {
    269             Log.e(LOGTAG, "FileNotFoundException for " + uri, e);
    270         } catch (IOException e) {
    271             Log.e(LOGTAG, "FileNotFoundException for " + uri, e);
    272         } catch (IllegalArgumentException e) {
    273             Log.e(LOGTAG, "exc, image decoded " + w + " x " + h + " bounds: "
    274                     + bounds.left + "," + bounds.top + " - "
    275                     + bounds.width() + "x" + bounds.height() + " exc: " + e);
    276         } finally {
    277             Utils.closeSilently(is);
    278         }
    279         return null;
    280     }
    281 
    282     /**
    283      * Returns the bounds of the bitmap stored at a given Url.
    284      */
    285     public static Rect loadBitmapBounds(Context context, Uri uri) {
    286         BitmapFactory.Options o = new BitmapFactory.Options();
    287         o.inJustDecodeBounds = true;
    288         loadBitmap(context, uri, o);
    289         return new Rect(0, 0, o.outWidth, o.outHeight);
    290     }
    291 
    292     /**
    293      * Loads a bitmap that has been downsampled using sampleSize from a given url.
    294      */
    295     public static Bitmap loadDownsampledBitmap(Context context, Uri uri, int sampleSize) {
    296         BitmapFactory.Options options = new BitmapFactory.Options();
    297         options.inMutable = true;
    298         options.inSampleSize = sampleSize;
    299         return loadBitmap(context, uri, options);
    300     }
    301 
    302 
    303     /**
    304      * Returns the bitmap from the given uri loaded using the given options.
    305      * Returns null on failure.
    306      */
    307     public static Bitmap loadBitmap(Context context, Uri uri, BitmapFactory.Options o) {
    308         if (uri == null || context == null) {
    309             throw new IllegalArgumentException("bad argument to loadBitmap");
    310         }
    311         InputStream is = null;
    312         try {
    313             is = context.getContentResolver().openInputStream(uri);
    314             return BitmapFactory.decodeStream(is, null, o);
    315         } catch (FileNotFoundException e) {
    316             Log.e(LOGTAG, "FileNotFoundException for " + uri, e);
    317         } finally {
    318             Utils.closeSilently(is);
    319         }
    320         return null;
    321     }
    322 
    323     /**
    324      * Loads a bitmap at a given URI that is downsampled so that both sides are
    325      * smaller than maxSideLength. The Bitmap's original dimensions are stored
    326      * in the rect originalBounds.
    327      *
    328      * @param uri URI of image to open.
    329      * @param context context whose ContentResolver to use.
    330      * @param maxSideLength max side length of returned bitmap.
    331      * @param originalBounds If not null, set to the actual bounds of the stored bitmap.
    332      * @param useMin use min or max side of the original image
    333      * @return downsampled bitmap or null if this operation failed.
    334      */
    335     public static Bitmap loadConstrainedBitmap(Uri uri, Context context, int maxSideLength,
    336             Rect originalBounds, boolean useMin) {
    337         if (maxSideLength <= 0 || uri == null || context == null) {
    338             throw new IllegalArgumentException("bad argument to getScaledBitmap");
    339         }
    340         // Get width and height of stored bitmap
    341         Rect storedBounds = loadBitmapBounds(context, uri);
    342         if (originalBounds != null) {
    343             originalBounds.set(storedBounds);
    344         }
    345         int w = storedBounds.width();
    346         int h = storedBounds.height();
    347 
    348         // If bitmap cannot be decoded, return null
    349         if (w <= 0 || h <= 0) {
    350             return null;
    351         }
    352 
    353         // Find best downsampling size
    354         int imageSide = 0;
    355         if (useMin) {
    356             imageSide = Math.min(w, h);
    357         } else {
    358             imageSide = Math.max(w, h);
    359         }
    360         int sampleSize = 1;
    361         while (imageSide > maxSideLength) {
    362             imageSide >>>= 1;
    363             sampleSize <<= 1;
    364         }
    365 
    366         // Make sure sample size is reasonable
    367         if (sampleSize <= 0 ||
    368                 0 >= (int) (Math.min(w, h) / sampleSize)) {
    369             return null;
    370         }
    371         return loadDownsampledBitmap(context, uri, sampleSize);
    372     }
    373 
    374     /**
    375      * Loads a bitmap at a given URI that is downsampled so that both sides are
    376      * smaller than maxSideLength. The Bitmap's original dimensions are stored
    377      * in the rect originalBounds.  The output is also transformed to the given
    378      * orientation.
    379      *
    380      * @param uri URI of image to open.
    381      * @param context context whose ContentResolver to use.
    382      * @param maxSideLength max side length of returned bitmap.
    383      * @param orientation  the orientation to transform the bitmap to.
    384      * @param originalBounds set to the actual bounds of the stored bitmap.
    385      * @return downsampled bitmap or null if this operation failed.
    386      */
    387     public static Bitmap loadOrientedConstrainedBitmap(Uri uri, Context context, int maxSideLength,
    388             int orientation, Rect originalBounds) {
    389         Bitmap bmap = loadConstrainedBitmap(uri, context, maxSideLength, originalBounds, false);
    390         if (bmap != null) {
    391             bmap = orientBitmap(bmap, orientation);
    392             if (bmap.getConfig()!= Bitmap.Config.ARGB_8888){
    393                 bmap = bmap.copy( Bitmap.Config.ARGB_8888,true);
    394             }
    395         }
    396         return bmap;
    397     }
    398 
    399     public static Bitmap getScaleOneImageForPreset(Context context,
    400                                                    BitmapCache cache,
    401                                                    Uri uri, Rect bounds,
    402                                                    Rect destination) {
    403         BitmapFactory.Options options = new BitmapFactory.Options();
    404         options.inMutable = true;
    405         if (destination != null) {
    406             int thresholdWidth = (int) (destination.width() * OVERDRAW_ZOOM);
    407             if (bounds.width() > thresholdWidth) {
    408                 int sampleSize = 1;
    409                 int w = bounds.width();
    410                 while (w > thresholdWidth) {
    411                     sampleSize *= 2;
    412                     w /= sampleSize;
    413                 }
    414                 options.inSampleSize = sampleSize;
    415             }
    416         }
    417         return loadRegionBitmap(context, cache, uri, options, bounds);
    418     }
    419 
    420     /**
    421      * Loads a bitmap that is downsampled by at least the input sample size. In
    422      * low-memory situations, the bitmap may be downsampled further.
    423      */
    424     public static Bitmap loadBitmapWithBackouts(Context context, Uri sourceUri, int sampleSize) {
    425         boolean noBitmap = true;
    426         int num_tries = 0;
    427         if (sampleSize <= 0) {
    428             sampleSize = 1;
    429         }
    430         Bitmap bmap = null;
    431         while (noBitmap) {
    432             try {
    433                 // Try to decode, downsample if low-memory.
    434                 bmap = loadDownsampledBitmap(context, sourceUri, sampleSize);
    435                 noBitmap = false;
    436             } catch (java.lang.OutOfMemoryError e) {
    437                 // Try with more downsampling before failing for good.
    438                 if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) {
    439                     throw e;
    440                 }
    441                 bmap = null;
    442                 System.gc();
    443                 sampleSize *= 2;
    444             }
    445         }
    446         return bmap;
    447     }
    448 
    449     /**
    450      * Loads an oriented bitmap that is downsampled by at least the input sample
    451      * size. In low-memory situations, the bitmap may be downsampled further.
    452      */
    453     public static Bitmap loadOrientedBitmapWithBackouts(Context context, Uri sourceUri,
    454             int sampleSize) {
    455         Bitmap bitmap = loadBitmapWithBackouts(context, sourceUri, sampleSize);
    456         if (bitmap == null) {
    457             return null;
    458         }
    459         int orientation = getMetadataOrientation(context, sourceUri);
    460         bitmap = orientBitmap(bitmap, orientation);
    461         return bitmap;
    462     }
    463 
    464     /**
    465      * Loads bitmap from a resource that may be downsampled in low-memory situations.
    466      */
    467     public static Bitmap decodeResourceWithBackouts(Resources res, BitmapFactory.Options options,
    468             int id) {
    469         boolean noBitmap = true;
    470         int num_tries = 0;
    471         if (options.inSampleSize < 1) {
    472             options.inSampleSize = 1;
    473         }
    474         // Stopgap fix for low-memory devices.
    475         Bitmap bmap = null;
    476         while (noBitmap) {
    477             try {
    478                 // Try to decode, downsample if low-memory.
    479                 bmap = BitmapFactory.decodeResource(
    480                         res, id, options);
    481                 noBitmap = false;
    482             } catch (java.lang.OutOfMemoryError e) {
    483                 // Retry before failing for good.
    484                 if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) {
    485                     throw e;
    486                 }
    487                 bmap = null;
    488                 System.gc();
    489                 options.inSampleSize *= 2;
    490             }
    491         }
    492         return bmap;
    493     }
    494 
    495     public static XMPMeta getXmpObject(Context context) {
    496         try {
    497             InputStream is = context.getContentResolver().openInputStream(
    498                     MasterImage.getImage().getUri());
    499             return XmpUtilHelper.extractXMPMeta(is);
    500         } catch (FileNotFoundException e) {
    501             return null;
    502         }
    503     }
    504 
    505     /**
    506      * Determine if this is a light cycle 360 image
    507      *
    508      * @return true if it is a light Cycle image that is full 360
    509      */
    510     public static boolean queryLightCycle360(Context context) {
    511         InputStream is = null;
    512         try {
    513             is = context.getContentResolver().openInputStream(MasterImage.getImage().getUri());
    514             XMPMeta meta = XmpUtilHelper.extractXMPMeta(is);
    515             if (meta == null) {
    516                 return false;
    517             }
    518             String namespace = "http://ns.google.com/photos/1.0/panorama/";
    519             String cropWidthName = "GPano:CroppedAreaImageWidthPixels";
    520             String fullWidthName = "GPano:FullPanoWidthPixels";
    521 
    522             if (!meta.doesPropertyExist(namespace, cropWidthName)) {
    523                 return false;
    524             }
    525             if (!meta.doesPropertyExist(namespace, fullWidthName)) {
    526                 return false;
    527             }
    528 
    529             Integer cropValue = meta.getPropertyInteger(namespace, cropWidthName);
    530             Integer fullValue = meta.getPropertyInteger(namespace, fullWidthName);
    531 
    532             // Definition of a 360:
    533             // GFullPanoWidthPixels == CroppedAreaImageWidthPixels
    534             if (cropValue != null && fullValue != null) {
    535                 return cropValue.equals(fullValue);
    536             }
    537 
    538             return false;
    539         } catch (FileNotFoundException e) {
    540             return false;
    541         } catch (XMPException e) {
    542             return false;
    543         } finally {
    544             Utils.closeSilently(is);
    545         }
    546     }
    547 
    548     public static List<ExifTag> getExif(Context context, Uri uri) {
    549         String path = getLocalPathFromUri(context, uri);
    550         if (path != null) {
    551             Uri localUri = Uri.parse(path);
    552             String mimeType = getMimeType(localUri);
    553             if (!JPEG_MIME_TYPE.equals(mimeType)) {
    554                 return null;
    555             }
    556             try {
    557                 ExifInterface exif = new ExifInterface();
    558                 exif.readExif(path);
    559                 List<ExifTag> taglist = exif.getAllTags();
    560                 return taglist;
    561             } catch (IOException e) {
    562                 Log.w(LOGTAG, "Failed to read EXIF tags", e);
    563             }
    564         }
    565         return null;
    566     }
    567 }
    568