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.util.Exif;
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 Bitmap createLocalBitmap(ContentResolver resolver, Uri uri, int maxSize) {
101        // TODO: make this method not download the image for both getImageBounds and decodeStream
102        InputStream inputStream = null;
103        try {
104            final BitmapFactory.Options opts = new BitmapFactory.Options();
105            final Point bounds = getImageBounds(resolver, uri);
106            inputStream = openInputStream(resolver, uri);
107            if (bounds == null || inputStream == null) {
108                return null;
109            }
110            opts.inSampleSize = Math.max(bounds.x / maxSize, bounds.y / maxSize);
111
112            final Bitmap decodedBitmap = decodeStream(inputStream, null, opts);
113
114            // Correct thumbnail orientation as necessary
115            // TODO: Fix rotation if it's actually a problem
116            //return rotateBitmap(resolver, uri, decodedBitmap);
117            return decodedBitmap;
118
119        } catch (FileNotFoundException exception) {
120            // Do nothing - the photo will appear to be missing
121        } catch (IOException exception) {
122            // Do nothing - the photo will appear to be missing
123        } catch (IllegalArgumentException exception) {
124            // Do nothing - the photo will appear to be missing
125        } finally {
126            try {
127                if (inputStream != null) {
128                    inputStream.close();
129                }
130            } catch (IOException ignore) {
131            }
132        }
133        return null;
134    }
135
136    /**
137     * Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect,
138     * BitmapFactory.Options)} that returns {@code null} on {@link
139     * OutOfMemoryError}.
140     *
141     * @param is The input stream that holds the raw data to be decoded into a
142     *           bitmap.
143     * @param outPadding If not null, return the padding rect for the bitmap if
144     *                   it exists, otherwise set padding to [-1,-1,-1,-1]. If
145     *                   no bitmap is returned (null) then padding is
146     *                   unchanged.
147     * @param opts null-ok; Options that control downsampling and whether the
148     *             image should be completely decoded, or just is size returned.
149     * @return The decoded bitmap, or null if the image data could not be
150     *         decoded, or, if opts is non-null, if opts requested only the
151     *         size be returned (in opts.outWidth and opts.outHeight)
152     */
153    public static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) {
154        ByteArrayOutputStream out = null;
155        InputStream byteStream = null;
156        try {
157            out = new ByteArrayOutputStream();
158            final byte[] buffer = new byte[4096];
159            int n = is.read(buffer);
160            while (n >= 0) {
161                out.write(buffer, 0, n);
162                n = is.read(buffer);
163            }
164
165            final byte[] bitmapBytes = out.toByteArray();
166
167            // Determine the orientation for this image
168            final int orientation = Exif.getOrientation(bitmapBytes);
169
170            // Create an InputStream from this byte array
171            byteStream = new ByteArrayInputStream(bitmapBytes);
172
173            final Bitmap originalBitmap = BitmapFactory.decodeStream(byteStream, outPadding, opts);
174
175            if (originalBitmap != null && orientation != 0) {
176                final Matrix matrix = new Matrix();
177                matrix.postRotate(orientation);
178                return Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(),
179                        originalBitmap.getHeight(), matrix, true);
180            }
181            return originalBitmap;
182        } catch (OutOfMemoryError oome) {
183            Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome);
184            return null;
185        } catch (IOException ioe) {
186            Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an IOE", ioe);
187            return null;
188        } finally {
189            if (out != null) {
190                try {
191                    out.close();
192                } catch (IOException e) {
193                    // Do nothing
194                }
195            }
196            if (byteStream != null) {
197                try {
198                    byteStream.close();
199                } catch (IOException e) {
200                    // Do nothing
201                }
202            }
203        }
204    }
205
206    /**
207     * Gets the image bounds
208     *
209     * @param resolver The ContentResolver
210     * @param uri The uri
211     *
212     * @return The image bounds
213     */
214    private static Point getImageBounds(ContentResolver resolver, Uri uri)
215            throws IOException {
216        final BitmapFactory.Options opts = new BitmapFactory.Options();
217        InputStream inputStream = null;
218        String scheme = uri.getScheme();
219        try {
220            opts.inJustDecodeBounds = true;
221            inputStream = openInputStream(resolver, uri);
222            if (inputStream == null) {
223                return null;
224            }
225            decodeStream(inputStream, null, opts);
226
227            return new Point(opts.outWidth, opts.outHeight);
228        } finally {
229            try {
230                if (inputStream != null) {
231                    inputStream.close();
232                }
233            } catch (IOException ignore) {
234            }
235        }
236    }
237
238    private static InputStream openInputStream(ContentResolver resolver, Uri uri) throws
239            FileNotFoundException {
240        String scheme = uri.getScheme();
241        if ("http".equals(scheme) || "https".equals(scheme)) {
242            try {
243                return new URL(uri.toString()).openStream();
244            } catch (MalformedURLException e) {
245                // Fall-back to the previous behaviour, just in case
246                Log.w(TAG, "Could not convert the uri to url: " + uri.toString());
247                return resolver.openInputStream(uri);
248            } catch (IOException e) {
249                Log.w(TAG, "Could not open input stream for uri: " + uri.toString());
250                return null;
251            }
252        } else if ("data".equals(scheme)) {
253            byte[] data = parseDataUri(uri);
254            if (data != null) {
255                return new ByteArrayInputStream(data);
256            }
257        }
258        return resolver.openInputStream(uri);
259    }
260
261    private static byte[] parseDataUri(Uri uri) {
262        String ssp = uri.getSchemeSpecificPart();
263        try {
264            if (ssp.startsWith(BASE64_URI_PREFIX)) {
265                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                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