ImageUtils.java revision 7a75fba57ed82c4b0fea362ce739ab253eac909f
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            is.close();
153
154            // Decode the bitmap
155            is = factory.createInputStream();
156            final Bitmap originalBitmap = BitmapFactory.decodeStream(is, outPadding, opts);
157
158            if (is != null && originalBitmap == null && !opts.inJustDecodeBounds) {
159                Log.w(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options): "
160                        + "Image bytes cannot be decoded into a Bitmap");
161                throw new UnsupportedOperationException(
162                        "Image bytes cannot be decoded into a Bitmap.");
163            }
164
165            // Rotate the Bitmap based on the orientation
166            if (originalBitmap != null && orientation != 0) {
167                final Matrix matrix = new Matrix();
168                matrix.postRotate(orientation);
169                return Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(),
170                        originalBitmap.getHeight(), matrix, true);
171            }
172            return originalBitmap;
173        } catch (OutOfMemoryError oome) {
174            Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome);
175            return null;
176        } catch (IOException ioe) {
177            Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an IOE", ioe);
178            return null;
179        } finally {
180            if (is != null) {
181                try {
182                    is.close();
183                } catch (IOException e) {
184                    // Do nothing
185                }
186            }
187        }
188    }
189
190    /**
191     * Gets the image bounds
192     *
193     * @param factory Used to create the InputStream.
194     *
195     * @return The image bounds
196     */
197    private static Point getImageBounds(final InputStreamFactory factory)
198            throws IOException {
199        final BitmapFactory.Options opts = new BitmapFactory.Options();
200        opts.inJustDecodeBounds = true;
201        decodeStream(factory, null, opts);
202
203        return new Point(opts.outWidth, opts.outHeight);
204    }
205
206    private static InputStreamFactory createInputStreamFactory(final ContentResolver resolver,
207            final Uri uri) {
208        final String scheme = uri.getScheme();
209        if ("data".equals(scheme)) {
210            return new DataInputStreamFactory(resolver, uri);
211        }
212        return new BaseInputStreamFactory(resolver, uri);
213    }
214
215    /**
216     * Utility class for when an InputStream needs to be read multiple times. For example, one pass
217     * may load EXIF orientation, and the second pass may do the actual Bitmap decode.
218     */
219    public interface InputStreamFactory {
220
221        /**
222         * Create a new InputStream. The caller of this method must be able to read the input
223         * stream starting from the beginning.
224         * @return
225         */
226        InputStream createInputStream() throws FileNotFoundException;
227    }
228
229    private static class BaseInputStreamFactory implements InputStreamFactory {
230        protected final ContentResolver mResolver;
231        protected final Uri mUri;
232
233        public BaseInputStreamFactory(final ContentResolver resolver, final Uri uri) {
234            mResolver = resolver;
235            mUri = uri;
236        }
237
238        @Override
239        public InputStream createInputStream() throws FileNotFoundException {
240            return mResolver.openInputStream(mUri);
241        }
242    }
243
244    private static class DataInputStreamFactory extends BaseInputStreamFactory {
245        private byte[] mData;
246
247        public DataInputStreamFactory(final ContentResolver resolver, final Uri uri) {
248            super(resolver, uri);
249        }
250
251        @Override
252        public InputStream createInputStream() throws FileNotFoundException {
253            if (mData == null) {
254                mData = parseDataUri(mUri);
255                if (mData == null) {
256                    return super.createInputStream();
257                }
258            }
259            return new ByteArrayInputStream(mData);
260        }
261
262        private byte[] parseDataUri(final Uri uri) {
263            final String ssp = uri.getSchemeSpecificPart();
264            try {
265                if (ssp.startsWith(BASE64_URI_PREFIX)) {
266                    final String base64 = ssp.substring(BASE64_URI_PREFIX.length());
267                    return Base64.decode(base64, Base64.URL_SAFE);
268                } else if (BASE64_IMAGE_URI_PATTERN.matcher(ssp).matches()){
269                    final String base64 = ssp.substring(
270                            ssp.indexOf(BASE64_URI_PREFIX) + BASE64_URI_PREFIX.length());
271                    return Base64.decode(base64, Base64.DEFAULT);
272                } else {
273                    return null;
274                }
275            } catch (IllegalArgumentException ex) {
276                Log.e(TAG, "Mailformed data URI: " + ex);
277                return null;
278            }
279        }
280    }
281}
282