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      */
isImageMimeType(String mimeType)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      */
createLocalBitmap(final ContentResolver resolver, final Uri uri, final int maxSize)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      */
decodeStream(final InputStreamFactory factory, final Rect outPadding, final BitmapFactory.Options opts)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      */
getImageBounds(final InputStreamFactory factory)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 
createInputStreamFactory(final ContentResolver resolver, final Uri uri)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          */
createInputStream()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 
BaseInputStreamFactory(final ContentResolver resolver, final Uri uri)232         public BaseInputStreamFactory(final ContentResolver resolver, final Uri uri) {
233             mResolver = resolver;
234             mUri = uri;
235         }
236 
237         @Override
createInputStream()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 
DataInputStreamFactory(final ContentResolver resolver, final Uri uri)246         public DataInputStreamFactory(final ContentResolver resolver, final Uri uri) {
247             super(resolver, uri);
248         }
249 
250         @Override
createInputStream()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 
parseDataUri(final Uri uri)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