Home | History | Annotate | Download | only in util
      1 /*
      2  * Copyright (C) 2011 Google Inc.
      3  * Licensed to The Android Open Source Project.
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.android.ex.photo.util;
     19 
     20 import android.content.ContentResolver;
     21 import android.graphics.Bitmap;
     22 import android.graphics.BitmapFactory;
     23 import android.graphics.Matrix;
     24 import android.graphics.Point;
     25 import android.graphics.Rect;
     26 import android.net.Uri;
     27 import android.os.Build;
     28 import android.util.Base64;
     29 import android.util.Log;
     30 
     31 import com.android.ex.photo.PhotoViewController;
     32 import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult;
     33 
     34 import java.io.ByteArrayInputStream;
     35 import java.io.FileNotFoundException;
     36 import java.io.IOException;
     37 import java.io.InputStream;
     38 import java.util.regex.Pattern;
     39 
     40 
     41 /**
     42  * Image utilities
     43  */
     44 public class ImageUtils {
     45     // Logging
     46     private static final String TAG = "ImageUtils";
     47 
     48     /** Minimum class memory class to use full-res photos */
     49     private final static long MIN_NORMAL_CLASS = 32;
     50     /** Minimum class memory class to use small photos */
     51     private final static long MIN_SMALL_CLASS = 24;
     52 
     53     private static final String BASE64_URI_PREFIX = "base64,";
     54     private static final Pattern BASE64_IMAGE_URI_PATTERN = Pattern.compile("^(?:.*;)?base64,.*");
     55 
     56     public static enum ImageSize {
     57         EXTRA_SMALL,
     58         SMALL,
     59         NORMAL,
     60     }
     61 
     62     public static final ImageSize sUseImageSize;
     63     static {
     64         // On HC and beyond, assume devices are more capable
     65         if (Build.VERSION.SDK_INT >= 11) {
     66             sUseImageSize = ImageSize.NORMAL;
     67         } else {
     68             if (PhotoViewController.sMemoryClass >= MIN_NORMAL_CLASS) {
     69                 // We have plenty of memory; use full sized photos
     70                 sUseImageSize = ImageSize.NORMAL;
     71             } else if (PhotoViewController.sMemoryClass >= MIN_SMALL_CLASS) {
     72                 // We have slight less memory; use smaller sized photos
     73                 sUseImageSize = ImageSize.SMALL;
     74             } else {
     75                 // We have little memory; use very small sized photos
     76                 sUseImageSize = ImageSize.EXTRA_SMALL;
     77             }
     78         }
     79     }
     80 
     81     /**
     82      * @return true if the MimeType type is image
     83      */
     84     public static boolean isImageMimeType(String mimeType) {
     85         return mimeType != null && mimeType.startsWith("image/");
     86     }
     87 
     88     /**
     89      * Create a bitmap from a local URI
     90      *
     91      * @param resolver The ContentResolver
     92      * @param uri      The local URI
     93      * @param maxSize  The maximum size (either width or height)
     94      * @return The new bitmap or null
     95      */
     96     public static BitmapResult createLocalBitmap(final ContentResolver resolver, final Uri uri,
     97             final int maxSize) {
     98         final BitmapResult result = new BitmapResult();
     99         final InputStreamFactory factory = createInputStreamFactory(resolver, uri);
    100         try {
    101             final Point bounds = getImageBounds(factory);
    102             if (bounds == null) {
    103                 result.status = BitmapResult.STATUS_EXCEPTION;
    104                 return result;
    105             }
    106 
    107             final BitmapFactory.Options opts = new BitmapFactory.Options();
    108             opts.inSampleSize = Math.max(bounds.x / maxSize, bounds.y / maxSize);
    109             result.bitmap = decodeStream(factory, null, opts);
    110             result.status = BitmapResult.STATUS_SUCCESS;
    111             return result;
    112 
    113         } catch (FileNotFoundException exception) {
    114             // Do nothing - the photo will appear to be missing
    115         } catch (IOException exception) {
    116             result.status = BitmapResult.STATUS_EXCEPTION;
    117         } catch (IllegalArgumentException exception) {
    118             // Do nothing - the photo will appear to be missing
    119         } catch (SecurityException exception) {
    120             result.status = BitmapResult.STATUS_EXCEPTION;
    121         }
    122         return result;
    123     }
    124 
    125     /**
    126      * Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect,
    127      * BitmapFactory.Options)} that returns {@code null} on {@link
    128      * OutOfMemoryError}.
    129      *
    130      * @param factory    Used to create input streams that holds the raw data to be decoded into a
    131      *                   bitmap.
    132      * @param outPadding If not null, return the padding rect for the bitmap if
    133      *                   it exists, otherwise set padding to [-1,-1,-1,-1]. If
    134      *                   no bitmap is returned (null) then padding is
    135      *                   unchanged.
    136      * @param opts       null-ok; Options that control downsampling and whether the
    137      *                   image should be completely decoded, or just is size returned.
    138      * @return The decoded bitmap, or null if the image data could not be
    139      * decoded, or, if opts is non-null, if opts requested only the
    140      * size be returned (in opts.outWidth and opts.outHeight)
    141      */
    142     public static Bitmap decodeStream(final InputStreamFactory factory, final Rect outPadding,
    143             final BitmapFactory.Options opts) throws FileNotFoundException {
    144         InputStream is = null;
    145         try {
    146             // Determine the orientation for this image
    147             is = factory.createInputStream();
    148             final int orientation = Exif.getOrientation(is, -1);
    149             if (is != null) {
    150                 is.close();
    151             }
    152 
    153             // Decode the bitmap
    154             is = factory.createInputStream();
    155             final Bitmap originalBitmap = BitmapFactory.decodeStream(is, outPadding, opts);
    156 
    157             if (is != null && originalBitmap == null && !opts.inJustDecodeBounds) {
    158                 Log.w(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options): "
    159                         + "Image bytes cannot be decoded into a Bitmap");
    160                 throw new UnsupportedOperationException(
    161                         "Image bytes cannot be decoded into a Bitmap.");
    162             }
    163 
    164             // Rotate the Bitmap based on the orientation
    165             if (originalBitmap != null && orientation != 0) {
    166                 final Matrix matrix = new Matrix();
    167                 matrix.postRotate(orientation);
    168                 return Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(),
    169                         originalBitmap.getHeight(), matrix, true);
    170             }
    171             return originalBitmap;
    172         } catch (OutOfMemoryError oome) {
    173             Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome);
    174             return null;
    175         } catch (IOException ioe) {
    176             Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an IOE", ioe);
    177             return null;
    178         } finally {
    179             if (is != null) {
    180                 try {
    181                     is.close();
    182                 } catch (IOException e) {
    183                     // Do nothing
    184                 }
    185             }
    186         }
    187     }
    188 
    189     /**
    190      * Gets the image bounds
    191      *
    192      * @param factory Used to create the InputStream.
    193      *
    194      * @return The image bounds
    195      */
    196     private static Point getImageBounds(final InputStreamFactory factory)
    197             throws IOException {
    198         final BitmapFactory.Options opts = new BitmapFactory.Options();
    199         opts.inJustDecodeBounds = true;
    200         decodeStream(factory, null, opts);
    201 
    202         return new Point(opts.outWidth, opts.outHeight);
    203     }
    204 
    205     private static InputStreamFactory createInputStreamFactory(final ContentResolver resolver,
    206             final Uri uri) {
    207         final String scheme = uri.getScheme();
    208         if ("data".equals(scheme)) {
    209             return new DataInputStreamFactory(resolver, uri);
    210         }
    211         return new BaseInputStreamFactory(resolver, uri);
    212     }
    213 
    214     /**
    215      * Utility class for when an InputStream needs to be read multiple times. For example, one pass
    216      * may load EXIF orientation, and the second pass may do the actual Bitmap decode.
    217      */
    218     public interface InputStreamFactory {
    219 
    220         /**
    221          * Create a new InputStream. The caller of this method must be able to read the input
    222          * stream starting from the beginning.
    223          * @return
    224          */
    225         InputStream createInputStream() throws FileNotFoundException;
    226     }
    227 
    228     private static class BaseInputStreamFactory implements InputStreamFactory {
    229         protected final ContentResolver mResolver;
    230         protected final Uri mUri;
    231 
    232         public BaseInputStreamFactory(final ContentResolver resolver, final Uri uri) {
    233             mResolver = resolver;
    234             mUri = uri;
    235         }
    236 
    237         @Override
    238         public InputStream createInputStream() throws FileNotFoundException {
    239             return mResolver.openInputStream(mUri);
    240         }
    241     }
    242 
    243     private static class DataInputStreamFactory extends BaseInputStreamFactory {
    244         private byte[] mData;
    245 
    246         public DataInputStreamFactory(final ContentResolver resolver, final Uri uri) {
    247             super(resolver, uri);
    248         }
    249 
    250         @Override
    251         public InputStream createInputStream() throws FileNotFoundException {
    252             if (mData == null) {
    253                 mData = parseDataUri(mUri);
    254                 if (mData == null) {
    255                     return super.createInputStream();
    256                 }
    257             }
    258             return new ByteArrayInputStream(mData);
    259         }
    260 
    261         private byte[] parseDataUri(final Uri uri) {
    262             final String ssp = uri.getSchemeSpecificPart();
    263             try {
    264                 if (ssp.startsWith(BASE64_URI_PREFIX)) {
    265                     final String base64 = ssp.substring(BASE64_URI_PREFIX.length());
    266                     return Base64.decode(base64, Base64.URL_SAFE);
    267                 } else if (BASE64_IMAGE_URI_PATTERN.matcher(ssp).matches()){
    268                     final String base64 = ssp.substring(
    269                             ssp.indexOf(BASE64_URI_PREFIX) + BASE64_URI_PREFIX.length());
    270                     return Base64.decode(base64, Base64.DEFAULT);
    271                 } else {
    272                     return null;
    273                 }
    274             } catch (IllegalArgumentException ex) {
    275                 Log.e(TAG, "Mailformed data URI: " + ex);
    276                 return null;
    277             }
    278         }
    279     }
    280 }
    281