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.PhotoViewActivity;
     32 import com.android.ex.photo.loaders.PhotoBitmapLoader.BitmapResult;
     33 
     34 import java.io.ByteArrayInputStream;
     35 import java.io.ByteArrayOutputStream;
     36 import java.io.FileNotFoundException;
     37 import java.io.IOException;
     38 import java.io.InputStream;
     39 import java.net.MalformedURLException;
     40 import java.net.URL;
     41 import java.util.regex.Pattern;
     42 
     43 
     44 /**
     45  * Image utilities
     46  */
     47 public class ImageUtils {
     48     // Logging
     49     private static final String TAG = "ImageUtils";
     50 
     51     /** Minimum class memory class to use full-res photos */
     52     private final static long MIN_NORMAL_CLASS = 32;
     53     /** Minimum class memory class to use small photos */
     54     private final static long MIN_SMALL_CLASS = 24;
     55 
     56     private static final String BASE64_URI_PREFIX = "base64,";
     57     private static final Pattern BASE64_IMAGE_URI_PATTERN = Pattern.compile("^(?:.*;)?base64,.*");
     58 
     59     public static enum ImageSize {
     60         EXTRA_SMALL,
     61         SMALL,
     62         NORMAL,
     63     }
     64 
     65     public static final ImageSize sUseImageSize;
     66     static {
     67         // On HC and beyond, assume devices are more capable
     68         if (Build.VERSION.SDK_INT >= 11) {
     69             sUseImageSize = ImageSize.NORMAL;
     70         } else {
     71             if (PhotoViewActivity.sMemoryClass >= MIN_NORMAL_CLASS) {
     72                 // We have plenty of memory; use full sized photos
     73                 sUseImageSize = ImageSize.NORMAL;
     74             } else if (PhotoViewActivity.sMemoryClass >= MIN_SMALL_CLASS) {
     75                 // We have slight less memory; use smaller sized photos
     76                 sUseImageSize = ImageSize.SMALL;
     77             } else {
     78                 // We have little memory; use very small sized photos
     79                 sUseImageSize = ImageSize.EXTRA_SMALL;
     80             }
     81         }
     82     }
     83 
     84     /**
     85      * @return true if the MimeType type is image
     86      */
     87     public static boolean isImageMimeType(String mimeType) {
     88         return mimeType != null && mimeType.startsWith("image/");
     89     }
     90 
     91     /**
     92      * Create a bitmap from a local URI
     93      *
     94      * @param resolver The ContentResolver
     95      * @param uri The local URI
     96      * @param maxSize The maximum size (either width or height)
     97      *
     98      * @return The new bitmap or null
     99      */
    100     public static BitmapResult createLocalBitmap(ContentResolver resolver, Uri uri, int maxSize) {
    101         // TODO: make this method not download the image for both getImageBounds and decodeStream
    102         BitmapResult result = new BitmapResult();
    103         InputStream inputStream = null;
    104         try {
    105             final BitmapFactory.Options opts = new BitmapFactory.Options();
    106             final Point bounds = getImageBounds(resolver, uri);
    107             inputStream = openInputStream(resolver, uri);
    108             if (bounds == null || inputStream == null) {
    109                 result.status = BitmapResult.STATUS_EXCEPTION;
    110                 return result;
    111             }
    112             opts.inSampleSize = Math.max(bounds.x / maxSize, bounds.y / maxSize);
    113 
    114             final Bitmap decodedBitmap = decodeStream(inputStream, null, opts);
    115 
    116             // Correct thumbnail orientation as necessary
    117             // TODO: Fix rotation if it's actually a problem
    118             //return rotateBitmap(resolver, uri, decodedBitmap);
    119             result.bitmap = decodedBitmap;
    120             result.status = BitmapResult.STATUS_SUCCESS;
    121             return result;
    122 
    123         } catch (FileNotFoundException exception) {
    124             // Do nothing - the photo will appear to be missing
    125         } catch (IOException exception) {
    126             result.status = BitmapResult.STATUS_EXCEPTION;
    127         } catch (IllegalArgumentException exception) {
    128             // Do nothing - the photo will appear to be missing
    129         } catch (SecurityException exception) {
    130             result.status = BitmapResult.STATUS_EXCEPTION;
    131         } finally {
    132             try {
    133                 if (inputStream != null) {
    134                     inputStream.close();
    135                 }
    136             } catch (IOException ignore) {
    137             }
    138         }
    139         return result;
    140     }
    141 
    142     /**
    143      * Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect,
    144      * BitmapFactory.Options)} that returns {@code null} on {@link
    145      * OutOfMemoryError}.
    146      *
    147      * @param is The input stream that holds the raw data to be decoded into a
    148      *           bitmap.
    149      * @param outPadding If not null, return the padding rect for the bitmap if
    150      *                   it exists, otherwise set padding to [-1,-1,-1,-1]. If
    151      *                   no bitmap is returned (null) then padding is
    152      *                   unchanged.
    153      * @param opts null-ok; Options that control downsampling and whether the
    154      *             image should be completely decoded, or just is size returned.
    155      * @return The decoded bitmap, or null if the image data could not be
    156      *         decoded, or, if opts is non-null, if opts requested only the
    157      *         size be returned (in opts.outWidth and opts.outHeight)
    158      */
    159     public static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) {
    160         ByteArrayOutputStream out = null;
    161         InputStream byteStream = null;
    162         try {
    163             out = new ByteArrayOutputStream();
    164             final byte[] buffer = new byte[4096];
    165             int n = is.read(buffer);
    166             while (n >= 0) {
    167                 out.write(buffer, 0, n);
    168                 n = is.read(buffer);
    169             }
    170 
    171             final byte[] bitmapBytes = out.toByteArray();
    172 
    173             // Determine the orientation for this image
    174             final int orientation = Exif.getOrientation(bitmapBytes);
    175 
    176             // Create an InputStream from this byte array
    177             byteStream = new ByteArrayInputStream(bitmapBytes);
    178 
    179             final Bitmap originalBitmap = BitmapFactory.decodeStream(byteStream, outPadding, opts);
    180 
    181             if (byteStream != null && originalBitmap == null && !opts.inJustDecodeBounds) {
    182                 Log.w(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options): "
    183                         + "Image bytes cannot be decoded into a Bitmap");
    184                 throw new UnsupportedOperationException(
    185                         "Image bytes cannot be decoded into a Bitmap.");
    186             }
    187             if (originalBitmap != null && orientation != 0) {
    188                 final Matrix matrix = new Matrix();
    189                 matrix.postRotate(orientation);
    190                 return Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(),
    191                         originalBitmap.getHeight(), matrix, true);
    192             }
    193             return originalBitmap;
    194         } catch (OutOfMemoryError oome) {
    195             Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome);
    196             return null;
    197         } catch (IOException ioe) {
    198             Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an IOE", ioe);
    199             return null;
    200         } finally {
    201             if (out != null) {
    202                 try {
    203                     out.close();
    204                 } catch (IOException e) {
    205                     // Do nothing
    206                 }
    207             }
    208             if (byteStream != null) {
    209                 try {
    210                     byteStream.close();
    211                 } catch (IOException e) {
    212                     // Do nothing
    213                 }
    214             }
    215         }
    216     }
    217 
    218     /**
    219      * Gets the image bounds
    220      *
    221      * @param resolver The ContentResolver
    222      * @param uri The uri
    223      *
    224      * @return The image bounds
    225      */
    226     private static Point getImageBounds(ContentResolver resolver, Uri uri)
    227             throws IOException {
    228         final BitmapFactory.Options opts = new BitmapFactory.Options();
    229         InputStream inputStream = null;
    230         String scheme = uri.getScheme();
    231         try {
    232             opts.inJustDecodeBounds = true;
    233             inputStream = openInputStream(resolver, uri);
    234             if (inputStream == null) {
    235                 return null;
    236             }
    237             decodeStream(inputStream, null, opts);
    238 
    239             return new Point(opts.outWidth, opts.outHeight);
    240         } finally {
    241             try {
    242                 if (inputStream != null) {
    243                     inputStream.close();
    244                 }
    245             } catch (IOException ignore) {
    246             }
    247         }
    248     }
    249 
    250     private static InputStream openInputStream(ContentResolver resolver, Uri uri) throws
    251             FileNotFoundException {
    252         String scheme = uri.getScheme();
    253         if ("http".equals(scheme) || "https".equals(scheme)) {
    254             try {
    255                 return new URL(uri.toString()).openStream();
    256             } catch (MalformedURLException e) {
    257                 // Fall-back to the previous behaviour, just in case
    258                 Log.w(TAG, "Could not convert the uri to url: " + uri.toString());
    259                 return resolver.openInputStream(uri);
    260             } catch (IOException e) {
    261                 Log.w(TAG, "Could not open input stream for uri: " + uri.toString());
    262                 return null;
    263             }
    264         } else if ("data".equals(scheme)) {
    265             byte[] data = parseDataUri(uri);
    266             if (data != null) {
    267                 return new ByteArrayInputStream(data);
    268             }
    269         }
    270         return resolver.openInputStream(uri);
    271     }
    272 
    273     private static byte[] parseDataUri(Uri uri) {
    274         String ssp = uri.getSchemeSpecificPart();
    275         try {
    276             if (ssp.startsWith(BASE64_URI_PREFIX)) {
    277                 String base64 = ssp.substring(BASE64_URI_PREFIX.length());
    278                 return Base64.decode(base64, Base64.URL_SAFE);
    279             } else if (BASE64_IMAGE_URI_PATTERN.matcher(ssp).matches()){
    280                 String base64 = ssp.substring(
    281                         ssp.indexOf(BASE64_URI_PREFIX) + BASE64_URI_PREFIX.length());
    282                 return Base64.decode(base64, Base64.DEFAULT);
    283             } else {
    284                 return null;
    285             }
    286         } catch (IllegalArgumentException ex) {
    287             Log.e(TAG, "Mailformed data URI: " + ex);
    288             return null;
    289         }
    290     }
    291 }
    292