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.PhotoViewController;
32import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult;
33
34import java.io.ByteArrayInputStream;
35import java.io.FileNotFoundException;
36import java.io.IOException;
37import java.io.InputStream;
38import java.util.regex.Pattern;
39
40
41/**
42 * Image utilities
43 */
44public 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