ImageUtils.java revision ca8fdcd23fcc53e0429abf8d4ef80e8d6d8fe6fa
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     *
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        try {
231            opts.inJustDecodeBounds = true;
232            inputStream = openInputStream(resolver, uri);
233            if (inputStream == null) {
234                return null;
235            }
236            decodeStream(inputStream, null, opts);
237
238            return new Point(opts.outWidth, opts.outHeight);
239        } finally {
240            try {
241                if (inputStream != null) {
242                    inputStream.close();
243                }
244            } catch (IOException ignore) {
245            }
246        }
247    }
248
249    private static InputStream openInputStream(ContentResolver resolver, Uri uri) throws
250            FileNotFoundException {
251        String scheme = uri.getScheme();
252        if ("http".equals(scheme) || "https".equals(scheme)) {
253            try {
254                return new URL(uri.toString()).openStream();
255            } catch (MalformedURLException e) {
256                // Fall-back to the previous behaviour, just in case
257                Log.w(TAG, "Could not convert the uri to url: " + uri.toString());
258                return resolver.openInputStream(uri);
259            } catch (IOException e) {
260                Log.w(TAG, "Could not open input stream for uri: " + uri.toString());
261                return null;
262            }
263        } else if ("data".equals(scheme)) {
264            byte[] data = parseDataUri(uri);
265            if (data != null) {
266                return new ByteArrayInputStream(data);
267            }
268        }
269        return resolver.openInputStream(uri);
270    }
271
272    private static byte[] parseDataUri(Uri uri) {
273        String ssp = uri.getSchemeSpecificPart();
274        try {
275            if (ssp.startsWith(BASE64_URI_PREFIX)) {
276                String base64 = ssp.substring(BASE64_URI_PREFIX.length());
277                return Base64.decode(base64, Base64.URL_SAFE);
278            } else if (BASE64_IMAGE_URI_PATTERN.matcher(ssp).matches()){
279                String base64 = ssp.substring(
280                        ssp.indexOf(BASE64_URI_PREFIX) + BASE64_URI_PREFIX.length());
281                return Base64.decode(base64, Base64.DEFAULT);
282            } else {
283                return null;
284            }
285        } catch (IllegalArgumentException ex) {
286            Log.e(TAG, "Mailformed data URI: " + ex);
287            return null;
288        }
289    }
290}
291