ImageUtils.java revision 274afea9b667b551d9582a3088fc27012d0e9a29
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
18package com.android.ex.photo.util;
19
20import android.content.ContentResolver;
21import android.graphics.Bitmap;
22import android.graphics.BitmapFactory;
23import android.graphics.Matrix;
24import android.graphics.Point;
25import android.graphics.Rect;
26import android.net.Uri;
27import android.os.Build;
28import android.util.Base64;
29import android.util.Log;
30
31import com.android.ex.photo.PhotoViewActivity;
32import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult;
33
34import java.io.ByteArrayInputStream;
35import java.io.ByteArrayOutputStream;
36import java.io.FileNotFoundException;
37import java.io.IOException;
38import java.io.InputStream;
39import java.net.MalformedURLException;
40import java.net.URL;
41import java.util.regex.Pattern;
42
43
44/**
45 * Image utilities
46 */
47public 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     * @return The new bitmap or null
98     */
99    public static BitmapResult createLocalBitmap(final ContentResolver resolver, final Uri uri,
100            final int maxSize) {
101        final BitmapResult result = new BitmapResult();
102        final InputStreamFactory factory = createInputStreamFactory(resolver, uri);
103        try {
104            final Point bounds = getImageBounds(factory);
105            if (bounds == null) {
106                result.status = BitmapResult.STATUS_EXCEPTION;
107                return result;
108            }
109
110            final BitmapFactory.Options opts = new BitmapFactory.Options();
111            opts.inSampleSize = Math.max(bounds.x / maxSize, bounds.y / maxSize);
112            result.bitmap = decodeStream(factory, null, opts);
113            result.status = BitmapResult.STATUS_SUCCESS;
114            return result;
115
116        } catch (FileNotFoundException exception) {
117            // Do nothing - the photo will appear to be missing
118        } catch (IOException exception) {
119            result.status = BitmapResult.STATUS_EXCEPTION;
120        } catch (IllegalArgumentException exception) {
121            // Do nothing - the photo will appear to be missing
122        } catch (SecurityException exception) {
123            result.status = BitmapResult.STATUS_EXCEPTION;
124        }
125        return result;
126    }
127
128    /**
129     * Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect,
130     * BitmapFactory.Options)} that returns {@code null} on {@link
131     * OutOfMemoryError}.
132     *
133     * @param factory    Used to create input streams that holds the raw data to be decoded into a
134     *                   bitmap.
135     * @param outPadding If not null, return the padding rect for the bitmap if
136     *                   it exists, otherwise set padding to [-1,-1,-1,-1]. If
137     *                   no bitmap is returned (null) then padding is
138     *                   unchanged.
139     * @param opts       null-ok; Options that control downsampling and whether the
140     *                   image should be completely decoded, or just is size returned.
141     * @return The decoded bitmap, or null if the image data could not be
142     * decoded, or, if opts is non-null, if opts requested only the
143     * size be returned (in opts.outWidth and opts.outHeight)
144     */
145    public static Bitmap decodeStream(final InputStreamFactory factory, final Rect outPadding,
146            final BitmapFactory.Options opts) throws FileNotFoundException {
147        InputStream is = null;
148        try {
149            // Determine the orientation for this image
150            is = factory.createInputStream();
151            final int orientation = Exif.getOrientation(is, -1);
152            if (is != null) {
153                is.close();
154            }
155
156            // Decode the bitmap
157            is = factory.createInputStream();
158            final Bitmap originalBitmap = BitmapFactory.decodeStream(is, outPadding, opts);
159
160            if (is != null && originalBitmap == null && !opts.inJustDecodeBounds) {
161                Log.w(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options): "
162                        + "Image bytes cannot be decoded into a Bitmap");
163                throw new UnsupportedOperationException(
164                        "Image bytes cannot be decoded into a Bitmap.");
165            }
166
167            // Rotate the Bitmap based on the orientation
168            if (originalBitmap != null && orientation != 0) {
169                final Matrix matrix = new Matrix();
170                matrix.postRotate(orientation);
171                return Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(),
172                        originalBitmap.getHeight(), matrix, true);
173            }
174            return originalBitmap;
175        } catch (OutOfMemoryError oome) {
176            Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome);
177            return null;
178        } catch (IOException ioe) {
179            Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an IOE", ioe);
180            return null;
181        } finally {
182            if (is != null) {
183                try {
184                    is.close();
185                } catch (IOException e) {
186                    // Do nothing
187                }
188            }
189        }
190    }
191
192    /**
193     * Gets the image bounds
194     *
195     * @param factory Used to create the InputStream.
196     *
197     * @return The image bounds
198     */
199    private static Point getImageBounds(final InputStreamFactory factory)
200            throws IOException {
201        final BitmapFactory.Options opts = new BitmapFactory.Options();
202        opts.inJustDecodeBounds = true;
203        decodeStream(factory, null, opts);
204
205        return new Point(opts.outWidth, opts.outHeight);
206    }
207
208    private static InputStreamFactory createInputStreamFactory(final ContentResolver resolver,
209            final Uri uri) {
210        final String scheme = uri.getScheme();
211        if ("data".equals(scheme)) {
212            return new DataInputStreamFactory(resolver, uri);
213        }
214        return new BaseInputStreamFactory(resolver, uri);
215    }
216
217    /**
218     * Utility class for when an InputStream needs to be read multiple times. For example, one pass
219     * may load EXIF orientation, and the second pass may do the actual Bitmap decode.
220     */
221    public interface InputStreamFactory {
222
223        /**
224         * Create a new InputStream. The caller of this method must be able to read the input
225         * stream starting from the beginning.
226         * @return
227         */
228        InputStream createInputStream() throws FileNotFoundException;
229    }
230
231    private static class BaseInputStreamFactory implements InputStreamFactory {
232        protected final ContentResolver mResolver;
233        protected final Uri mUri;
234
235        public BaseInputStreamFactory(final ContentResolver resolver, final Uri uri) {
236            mResolver = resolver;
237            mUri = uri;
238        }
239
240        @Override
241        public InputStream createInputStream() throws FileNotFoundException {
242            return mResolver.openInputStream(mUri);
243        }
244    }
245
246    private static class DataInputStreamFactory extends BaseInputStreamFactory {
247        private byte[] mData;
248
249        public DataInputStreamFactory(final ContentResolver resolver, final Uri uri) {
250            super(resolver, uri);
251        }
252
253        @Override
254        public InputStream createInputStream() throws FileNotFoundException {
255            if (mData == null) {
256                mData = parseDataUri(mUri);
257                if (mData == null) {
258                    return super.createInputStream();
259                }
260            }
261            return new ByteArrayInputStream(mData);
262        }
263
264        private byte[] parseDataUri(final Uri uri) {
265            final String ssp = uri.getSchemeSpecificPart();
266            try {
267                if (ssp.startsWith(BASE64_URI_PREFIX)) {
268                    final String base64 = ssp.substring(BASE64_URI_PREFIX.length());
269                    return Base64.decode(base64, Base64.URL_SAFE);
270                } else if (BASE64_IMAGE_URI_PATTERN.matcher(ssp).matches()){
271                    final String base64 = ssp.substring(
272                            ssp.indexOf(BASE64_URI_PREFIX) + BASE64_URI_PREFIX.length());
273                    return Base64.decode(base64, Base64.DEFAULT);
274                } else {
275                    return null;
276                }
277            } catch (IllegalArgumentException ex) {
278                Log.e(TAG, "Mailformed data URI: " + ex);
279                return null;
280            }
281        }
282    }
283}
284