blob: 3040e0e0b3bc31a506301b0beb8ac875382a9fa7 [file] [log] [blame]
nicolasroard19ab7252013-09-18 16:54:05 -07001/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Sascha Haeberling7cb8c792014-03-11 09:14:53 -070017package com.android.camera.util;
nicolasroard19ab7252013-09-18 16:54:05 -070018
19import android.content.ContentResolver;
20import android.content.Context;
21import android.content.res.Resources;
22import android.database.Cursor;
23import android.database.sqlite.SQLiteException;
24import android.graphics.Bitmap;
25import android.graphics.BitmapFactory;
26import android.graphics.Matrix;
27import android.graphics.Rect;
28import android.net.Uri;
29import android.provider.MediaStore;
nicolasroard19ab7252013-09-18 16:54:05 -070030import android.webkit.MimeTypeMap;
31
Angus Kong2bca2102014-03-11 16:27:30 -070032import com.android.camera.debug.Log;
nicolasroard19ab7252013-09-18 16:54:05 -070033import com.android.camera.exif.ExifInterface;
34import com.android.camera.exif.ExifTag;
35
Sascha Haeberling7cb8c792014-03-11 09:14:53 -070036import java.io.Closeable;
nicolasroard19ab7252013-09-18 16:54:05 -070037import java.io.FileNotFoundException;
38import java.io.IOException;
39import java.io.InputStream;
40import java.util.List;
41
42public final class ImageLoader {
43
Angus Kong2bca2102014-03-11 16:27:30 -070044 private static final Log.Tag TAG = new Log.Tag("ImageLoader");
nicolasroard19ab7252013-09-18 16:54:05 -070045
46 public static final String JPEG_MIME_TYPE = "image/jpeg";
47 public static final int DEFAULT_COMPRESS_QUALITY = 95;
48
49 public static final int ORI_NORMAL = ExifInterface.Orientation.TOP_LEFT;
50 public static final int ORI_ROTATE_90 = ExifInterface.Orientation.RIGHT_TOP;
51 public static final int ORI_ROTATE_180 = ExifInterface.Orientation.BOTTOM_LEFT;
52 public static final int ORI_ROTATE_270 = ExifInterface.Orientation.RIGHT_BOTTOM;
53 public static final int ORI_FLIP_HOR = ExifInterface.Orientation.TOP_RIGHT;
54 public static final int ORI_FLIP_VERT = ExifInterface.Orientation.BOTTOM_RIGHT;
55 public static final int ORI_TRANSPOSE = ExifInterface.Orientation.LEFT_TOP;
56 public static final int ORI_TRANSVERSE = ExifInterface.Orientation.LEFT_BOTTOM;
57
58 private static final int BITMAP_LOAD_BACKOUT_ATTEMPTS = 5;
nicolasroard19ab7252013-09-18 16:54:05 -070059 private ImageLoader() {}
60
61 /**
62 * Returns the Mime type for a Url. Safe to use with Urls that do not
63 * come from Gallery's content provider.
64 */
65 public static String getMimeType(Uri src) {
66 String postfix = MimeTypeMap.getFileExtensionFromUrl(src.toString());
67 String ret = null;
68 if (postfix != null) {
69 ret = MimeTypeMap.getSingleton().getMimeTypeFromExtension(postfix);
70 }
71 return ret;
72 }
73
Sascha Haeberlinga63dbb62013-11-22 11:55:32 -080074 public static String getLocalPathFromUri(ContentResolver resolver, Uri uri) {
75 Cursor cursor = resolver.query(uri,
nicolasroard19ab7252013-09-18 16:54:05 -070076 new String[]{MediaStore.Images.Media.DATA}, null, null, null);
77 if (cursor == null) {
78 return null;
79 }
80 int index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
81 cursor.moveToFirst();
82 return cursor.getString(index);
83 }
84
85 /**
86 * Returns the image's orientation flag. Defaults to ORI_NORMAL if no valid
87 * orientation was found.
88 */
89 public static int getMetadataOrientation(Context context, Uri uri) {
90 if (uri == null || context == null) {
91 throw new IllegalArgumentException("bad argument to getOrientation");
92 }
93
94 // First try to find orientation data in Gallery's ContentProvider.
95 Cursor cursor = null;
96 try {
97 cursor = context.getContentResolver().query(uri,
98 new String[] { MediaStore.Images.ImageColumns.ORIENTATION },
99 null, null, null);
100 if (cursor != null && cursor.moveToNext()) {
101 int ori = cursor.getInt(0);
102 switch (ori) {
103 case 90:
104 return ORI_ROTATE_90;
105 case 270:
106 return ORI_ROTATE_270;
107 case 180:
108 return ORI_ROTATE_180;
109 default:
110 return ORI_NORMAL;
111 }
112 }
113 } catch (SQLiteException e) {
114 // Do nothing
115 } catch (IllegalArgumentException e) {
116 // Do nothing
117 } catch (IllegalStateException e) {
118 // Do nothing
119 } finally {
Sascha Haeberling7cb8c792014-03-11 09:14:53 -0700120 closeSilently(cursor);
nicolasroard19ab7252013-09-18 16:54:05 -0700121 }
122
123 // Fall back to checking EXIF tags in file.
124 if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
125 String mimeType = getMimeType(uri);
126 if (!JPEG_MIME_TYPE.equals(mimeType)) {
127 return ORI_NORMAL;
128 }
129 String path = uri.getPath();
130 ExifInterface exif = new ExifInterface();
131 try {
132 exif.readExif(path);
133 Integer tagval = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION);
134 if (tagval != null) {
135 int orientation = tagval;
136 switch(orientation) {
137 case ORI_NORMAL:
138 case ORI_ROTATE_90:
139 case ORI_ROTATE_180:
140 case ORI_ROTATE_270:
141 case ORI_FLIP_HOR:
142 case ORI_FLIP_VERT:
143 case ORI_TRANSPOSE:
144 case ORI_TRANSVERSE:
145 return orientation;
146 default:
147 return ORI_NORMAL;
148 }
149 }
150 } catch (IOException e) {
Sascha Haeberling7cb8c792014-03-11 09:14:53 -0700151 Log.w(TAG, "Failed to read EXIF orientation", e);
nicolasroard19ab7252013-09-18 16:54:05 -0700152 }
153 }
154 return ORI_NORMAL;
155 }
156
157 /**
158 * Returns the rotation of image at the given URI as one of 0, 90, 180,
159 * 270. Defaults to 0.
160 */
161 public static int getMetadataRotation(Context context, Uri uri) {
162 int orientation = getMetadataOrientation(context, uri);
163 switch(orientation) {
164 case ORI_ROTATE_90:
165 return 90;
166 case ORI_ROTATE_180:
167 return 180;
168 case ORI_ROTATE_270:
169 return 270;
170 default:
171 return 0;
172 }
173 }
174
175 /**
176 * Takes an orientation and a bitmap, and returns the bitmap transformed
177 * to that orientation.
178 */
179 public static Bitmap orientBitmap(Bitmap bitmap, int ori) {
180 Matrix matrix = new Matrix();
181 int w = bitmap.getWidth();
182 int h = bitmap.getHeight();
183 if (ori == ORI_ROTATE_90 ||
184 ori == ORI_ROTATE_270 ||
185 ori == ORI_TRANSPOSE ||
186 ori == ORI_TRANSVERSE) {
187 int tmp = w;
188 w = h;
189 h = tmp;
190 }
191 switch (ori) {
192 case ORI_ROTATE_90:
193 matrix.setRotate(90, w / 2f, h / 2f);
194 break;
195 case ORI_ROTATE_180:
196 matrix.setRotate(180, w / 2f, h / 2f);
197 break;
198 case ORI_ROTATE_270:
199 matrix.setRotate(270, w / 2f, h / 2f);
200 break;
201 case ORI_FLIP_HOR:
202 matrix.preScale(-1, 1);
203 break;
204 case ORI_FLIP_VERT:
205 matrix.preScale(1, -1);
206 break;
207 case ORI_TRANSPOSE:
208 matrix.setRotate(90, w / 2f, h / 2f);
209 matrix.preScale(1, -1);
210 break;
211 case ORI_TRANSVERSE:
212 matrix.setRotate(270, w / 2f, h / 2f);
213 matrix.preScale(1, -1);
214 break;
215 case ORI_NORMAL:
216 default:
217 return bitmap;
218 }
219 return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
220 bitmap.getHeight(), matrix, true);
221 }
222
223 /**
224 * Returns the bounds of the bitmap stored at a given Url.
225 */
226 public static Rect loadBitmapBounds(Context context, Uri uri) {
227 BitmapFactory.Options o = new BitmapFactory.Options();
228 loadBitmap(context, uri, o);
229 return new Rect(0, 0, o.outWidth, o.outHeight);
230 }
231
232 /**
233 * Loads a bitmap that has been downsampled using sampleSize from a given url.
234 */
235 public static Bitmap loadDownsampledBitmap(Context context, Uri uri, int sampleSize) {
236 BitmapFactory.Options options = new BitmapFactory.Options();
237 options.inMutable = true;
238 options.inSampleSize = sampleSize;
239 return loadBitmap(context, uri, options);
240 }
241
242 /**
243 * Returns the bitmap from the given uri loaded using the given options.
244 * Returns null on failure.
245 */
246 public static Bitmap loadBitmap(Context context, Uri uri, BitmapFactory.Options o) {
247 if (uri == null || context == null) {
248 throw new IllegalArgumentException("bad argument to loadBitmap");
249 }
250 InputStream is = null;
251 try {
252 is = context.getContentResolver().openInputStream(uri);
253 return BitmapFactory.decodeStream(is, null, o);
254 } catch (FileNotFoundException e) {
Sascha Haeberling7cb8c792014-03-11 09:14:53 -0700255 Log.e(TAG, "FileNotFoundException for " + uri, e);
nicolasroard19ab7252013-09-18 16:54:05 -0700256 } finally {
Sascha Haeberling7cb8c792014-03-11 09:14:53 -0700257 closeSilently(is);
nicolasroard19ab7252013-09-18 16:54:05 -0700258 }
259 return null;
260 }
261
262 /**
263 * Loads a bitmap at a given URI that is downsampled so that both sides are
264 * smaller than maxSideLength. The Bitmap's original dimensions are stored
265 * in the rect originalBounds.
266 *
267 * @param uri URI of image to open.
268 * @param context context whose ContentResolver to use.
269 * @param maxSideLength max side length of returned bitmap.
270 * @param originalBounds If not null, set to the actual bounds of the stored bitmap.
271 * @param useMin use min or max side of the original image
272 * @return downsampled bitmap or null if this operation failed.
273 */
274 public static Bitmap loadConstrainedBitmap(Uri uri, Context context, int maxSideLength,
275 Rect originalBounds, boolean useMin) {
276 if (maxSideLength <= 0 || uri == null || context == null) {
277 throw new IllegalArgumentException("bad argument to getScaledBitmap");
278 }
279 // Get width and height of stored bitmap
280 Rect storedBounds = loadBitmapBounds(context, uri);
281 if (originalBounds != null) {
282 originalBounds.set(storedBounds);
283 }
284 int w = storedBounds.width();
285 int h = storedBounds.height();
286
287 // If bitmap cannot be decoded, return null
288 if (w <= 0 || h <= 0) {
289 return null;
290 }
291
292 // Find best downsampling size
293 int imageSide = 0;
294 if (useMin) {
295 imageSide = Math.min(w, h);
296 } else {
297 imageSide = Math.max(w, h);
298 }
299 int sampleSize = 1;
300 while (imageSide > maxSideLength) {
301 imageSide >>>= 1;
302 sampleSize <<= 1;
303 }
304
305 // Make sure sample size is reasonable
306 if (sampleSize <= 0 ||
Sascha Haeberlinga63dbb62013-11-22 11:55:32 -0800307 0 >= (Math.min(w, h) / sampleSize)) {
nicolasroard19ab7252013-09-18 16:54:05 -0700308 return null;
309 }
310 return loadDownsampledBitmap(context, uri, sampleSize);
311 }
312
313 /**
314 * Loads a bitmap at a given URI that is downsampled so that both sides are
315 * smaller than maxSideLength. The Bitmap's original dimensions are stored
316 * in the rect originalBounds. The output is also transformed to the given
317 * orientation.
318 *
319 * @param uri URI of image to open.
320 * @param context context whose ContentResolver to use.
321 * @param maxSideLength max side length of returned bitmap.
322 * @param orientation the orientation to transform the bitmap to.
323 * @param originalBounds set to the actual bounds of the stored bitmap.
324 * @return downsampled bitmap or null if this operation failed.
325 */
326 public static Bitmap loadOrientedConstrainedBitmap(Uri uri, Context context, int maxSideLength,
327 int orientation, Rect originalBounds) {
328 Bitmap bmap = loadConstrainedBitmap(uri, context, maxSideLength, originalBounds, false);
329 if (bmap != null) {
330 bmap = orientBitmap(bmap, orientation);
331 if (bmap.getConfig()!= Bitmap.Config.ARGB_8888){
332 bmap = bmap.copy( Bitmap.Config.ARGB_8888,true);
333 }
334 }
335 return bmap;
336 }
337
338 /**
339 * Loads a bitmap that is downsampled by at least the input sample size. In
340 * low-memory situations, the bitmap may be downsampled further.
341 */
342 public static Bitmap loadBitmapWithBackouts(Context context, Uri sourceUri, int sampleSize) {
343 boolean noBitmap = true;
344 int num_tries = 0;
345 if (sampleSize <= 0) {
346 sampleSize = 1;
347 }
348 Bitmap bmap = null;
349 while (noBitmap) {
350 try {
351 // Try to decode, downsample if low-memory.
352 bmap = loadDownsampledBitmap(context, sourceUri, sampleSize);
353 noBitmap = false;
354 } catch (java.lang.OutOfMemoryError e) {
355 // Try with more downsampling before failing for good.
356 if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) {
357 throw e;
358 }
359 bmap = null;
360 System.gc();
361 sampleSize *= 2;
362 }
363 }
364 return bmap;
365 }
366
367 /**
368 * Loads an oriented bitmap that is downsampled by at least the input sample
369 * size. In low-memory situations, the bitmap may be downsampled further.
370 */
371 public static Bitmap loadOrientedBitmapWithBackouts(Context context, Uri sourceUri,
372 int sampleSize) {
373 Bitmap bitmap = loadBitmapWithBackouts(context, sourceUri, sampleSize);
374 if (bitmap == null) {
375 return null;
376 }
377 int orientation = getMetadataOrientation(context, sourceUri);
378 bitmap = orientBitmap(bitmap, orientation);
379 return bitmap;
380 }
381
382 /**
383 * Loads bitmap from a resource that may be downsampled in low-memory situations.
384 */
385 public static Bitmap decodeResourceWithBackouts(Resources res, BitmapFactory.Options options,
386 int id) {
387 boolean noBitmap = true;
388 int num_tries = 0;
389 if (options.inSampleSize < 1) {
390 options.inSampleSize = 1;
391 }
392 // Stopgap fix for low-memory devices.
393 Bitmap bmap = null;
394 while (noBitmap) {
395 try {
396 // Try to decode, downsample if low-memory.
397 bmap = BitmapFactory.decodeResource(
398 res, id, options);
399 noBitmap = false;
400 } catch (java.lang.OutOfMemoryError e) {
401 // Retry before failing for good.
402 if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) {
403 throw e;
404 }
405 bmap = null;
406 System.gc();
407 options.inSampleSize *= 2;
408 }
409 }
410 return bmap;
411 }
412
Sascha Haeberlinga63dbb62013-11-22 11:55:32 -0800413 public static List<ExifTag> getExif(ContentResolver resolver, Uri uri) {
414 String path = getLocalPathFromUri(resolver, uri);
nicolasroard19ab7252013-09-18 16:54:05 -0700415 if (path != null) {
416 Uri localUri = Uri.parse(path);
417 String mimeType = getMimeType(localUri);
418 if (!JPEG_MIME_TYPE.equals(mimeType)) {
419 return null;
420 }
421 try {
422 ExifInterface exif = new ExifInterface();
423 exif.readExif(path);
424 List<ExifTag> taglist = exif.getAllTags();
425 return taglist;
426 } catch (IOException e) {
Sascha Haeberling7cb8c792014-03-11 09:14:53 -0700427 Log.w(TAG, "Failed to read EXIF tags", e);
nicolasroard19ab7252013-09-18 16:54:05 -0700428 }
429 }
430 return null;
431 }
Sascha Haeberling7cb8c792014-03-11 09:14:53 -0700432
433 private static void closeSilently(Closeable c) {
434 if (c == null) return;
435 try {
436 c.close();
437 } catch (IOException t) {
438 Log.w(TAG, "close fail ", t);
439 }
440 }
441
nicolasroard19ab7252013-09-18 16:54:05 -0700442}