ImageLoader.java revision 860af325f2030a03c526e8551a85230d17df7b15
1/* 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 17package com.android.gallery3d.filtershow.cache; 18 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.BitmapRegionDecoder; 27import android.graphics.Matrix; 28import android.graphics.Rect; 29import android.net.Uri; 30import android.provider.MediaStore; 31import android.util.Log; 32import android.webkit.MimeTypeMap; 33 34import com.adobe.xmp.XMPException; 35import com.adobe.xmp.XMPMeta; 36import com.android.gallery3d.common.Utils; 37import com.android.gallery3d.exif.ExifInterface; 38import com.android.gallery3d.exif.ExifTag; 39import com.android.gallery3d.filtershow.imageshow.MasterImage; 40import com.android.gallery3d.filtershow.tools.XmpPresets; 41import com.android.gallery3d.util.XmpUtilHelper; 42 43import java.io.FileNotFoundException; 44import java.io.IOException; 45import java.io.InputStream; 46import java.util.List; 47 48public final class ImageLoader { 49 50 private static final String LOGTAG = "ImageLoader"; 51 52 public static final String JPEG_MIME_TYPE = "image/jpeg"; 53 public static final int DEFAULT_COMPRESS_QUALITY = 95; 54 55 public static final int ORI_NORMAL = ExifInterface.Orientation.TOP_LEFT; 56 public static final int ORI_ROTATE_90 = ExifInterface.Orientation.RIGHT_TOP; 57 public static final int ORI_ROTATE_180 = ExifInterface.Orientation.BOTTOM_LEFT; 58 public static final int ORI_ROTATE_270 = ExifInterface.Orientation.RIGHT_BOTTOM; 59 public static final int ORI_FLIP_HOR = ExifInterface.Orientation.TOP_RIGHT; 60 public static final int ORI_FLIP_VERT = ExifInterface.Orientation.BOTTOM_RIGHT; 61 public static final int ORI_TRANSPOSE = ExifInterface.Orientation.LEFT_TOP; 62 public static final int ORI_TRANSVERSE = ExifInterface.Orientation.LEFT_BOTTOM; 63 64 private static final int BITMAP_LOAD_BACKOUT_ATTEMPTS = 5; 65 66 private ImageLoader() {} 67 68 /** 69 * Returns the Mime type for a Url. Safe to use with Urls that do not 70 * come from Gallery's content provider. 71 */ 72 public static String getMimeType(Uri src) { 73 String postfix = MimeTypeMap.getFileExtensionFromUrl(src.toString()); 74 String ret = null; 75 if (postfix != null) { 76 ret = MimeTypeMap.getSingleton().getMimeTypeFromExtension(postfix); 77 } 78 return ret; 79 } 80 81 public static String getLocalPathFromUri(Context context, Uri uri) { 82 Cursor cursor = context.getContentResolver().query(uri, 83 new String[]{MediaStore.Images.Media.DATA}, null, null, null); 84 int index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); 85 cursor.moveToFirst(); 86 return cursor.getString(index); 87 } 88 89 /** 90 * Returns the image's orientation flag. Defaults to ORI_NORMAL if no valid 91 * orientation was found. 92 */ 93 public static int getMetadataOrientation(Context context, Uri uri) { 94 if (uri == null || context == null) { 95 throw new IllegalArgumentException("bad argument to getOrientation"); 96 } 97 98 // First try to find orientation data in Gallery's ContentProvider. 99 Cursor cursor = null; 100 try { 101 cursor = context.getContentResolver().query(uri, 102 new String[] { MediaStore.Images.ImageColumns.ORIENTATION }, 103 null, null, null); 104 if (cursor != null && cursor.moveToNext()) { 105 int ori = cursor.getInt(0); 106 switch (ori) { 107 case 90: 108 return ORI_ROTATE_90; 109 case 270: 110 return ORI_ROTATE_270; 111 case 180: 112 return ORI_ROTATE_180; 113 default: 114 return ORI_NORMAL; 115 } 116 } 117 } catch (SQLiteException e) { 118 // Do nothing 119 } catch (IllegalArgumentException e) { 120 // Do nothing 121 } finally { 122 Utils.closeSilently(cursor); 123 } 124 125 // Fall back to checking EXIF tags in file. 126 if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { 127 String mimeType = getMimeType(uri); 128 if (!JPEG_MIME_TYPE.equals(mimeType)) { 129 return ORI_NORMAL; 130 } 131 String path = uri.getPath(); 132 ExifInterface exif = new ExifInterface(); 133 try { 134 exif.readExif(path); 135 Integer tagval = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION); 136 if (tagval != null) { 137 int orientation = tagval; 138 switch(orientation) { 139 case ORI_NORMAL: 140 case ORI_ROTATE_90: 141 case ORI_ROTATE_180: 142 case ORI_ROTATE_270: 143 case ORI_FLIP_HOR: 144 case ORI_FLIP_VERT: 145 case ORI_TRANSPOSE: 146 case ORI_TRANSVERSE: 147 return orientation; 148 default: 149 return ORI_NORMAL; 150 } 151 } 152 } catch (IOException e) { 153 Log.w(LOGTAG, "Failed to read EXIF orientation", e); 154 } 155 } 156 return ORI_NORMAL; 157 } 158 159 /** 160 * Returns the rotation of image at the given URI as one of 0, 90, 180, 161 * 270. Defaults to 0. 162 */ 163 public static int getMetadataRotation(Context context, Uri uri) { 164 int orientation = getMetadataOrientation(context, uri); 165 switch(orientation) { 166 case ORI_ROTATE_90: 167 return 90; 168 case ORI_ROTATE_180: 169 return 180; 170 case ORI_ROTATE_270: 171 return 270; 172 default: 173 return 0; 174 } 175 } 176 177 /** 178 * Takes an orientation and a bitmap, and returns the bitmap transformed 179 * to that orientation. 180 */ 181 public static Bitmap orientBitmap(Bitmap bitmap, int ori) { 182 Matrix matrix = new Matrix(); 183 int w = bitmap.getWidth(); 184 int h = bitmap.getHeight(); 185 if (ori == ORI_ROTATE_90 || 186 ori == ORI_ROTATE_270 || 187 ori == ORI_TRANSPOSE || 188 ori == ORI_TRANSVERSE) { 189 int tmp = w; 190 w = h; 191 h = tmp; 192 } 193 switch (ori) { 194 case ORI_ROTATE_90: 195 matrix.setRotate(90, w / 2f, h / 2f); 196 break; 197 case ORI_ROTATE_180: 198 matrix.setRotate(180, w / 2f, h / 2f); 199 break; 200 case ORI_ROTATE_270: 201 matrix.setRotate(270, w / 2f, h / 2f); 202 break; 203 case ORI_FLIP_HOR: 204 matrix.preScale(-1, 1); 205 break; 206 case ORI_FLIP_VERT: 207 matrix.preScale(1, -1); 208 break; 209 case ORI_TRANSPOSE: 210 matrix.setRotate(90, w / 2f, h / 2f); 211 matrix.preScale(1, -1); 212 break; 213 case ORI_TRANSVERSE: 214 matrix.setRotate(270, w / 2f, h / 2f); 215 matrix.preScale(1, -1); 216 break; 217 case ORI_NORMAL: 218 default: 219 return bitmap; 220 } 221 return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), 222 bitmap.getHeight(), matrix, true); 223 } 224 225 /** 226 * Returns the bitmap for the rectangular region given by "bounds" 227 * if it is a subset of the bitmap stored at uri. Otherwise returns 228 * null. 229 */ 230 public static Bitmap loadRegionBitmap(Context context, Uri uri, BitmapFactory.Options options, 231 Rect bounds) { 232 InputStream is = null; 233 try { 234 is = context.getContentResolver().openInputStream(uri); 235 BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false); 236 Rect r = new Rect(0, 0, decoder.getWidth(), decoder.getHeight()); 237 // return null if bounds are not entirely within the bitmap 238 if (!r.contains(bounds)) { 239 return null; 240 } 241 return decoder.decodeRegion(bounds, options); 242 } catch (FileNotFoundException e) { 243 Log.e(LOGTAG, "FileNotFoundException for " + uri, e); 244 } catch (IOException e) { 245 Log.e(LOGTAG, "FileNotFoundException for " + uri, e); 246 } finally { 247 Utils.closeSilently(is); 248 } 249 return null; 250 } 251 252 /** 253 * Returns the bounds of the bitmap stored at a given Url. 254 */ 255 public static Rect loadBitmapBounds(Context context, Uri uri) { 256 BitmapFactory.Options o = new BitmapFactory.Options(); 257 loadBitmap(context, uri, o); 258 return new Rect(0, 0, o.outWidth, o.outHeight); 259 } 260 261 /** 262 * Loads a bitmap that has been downsampled using sampleSize from a given url. 263 */ 264 public static Bitmap loadDownsampledBitmap(Context context, Uri uri, int sampleSize) { 265 BitmapFactory.Options options = new BitmapFactory.Options(); 266 options.inMutable = true; 267 options.inSampleSize = sampleSize; 268 return loadBitmap(context, uri, options); 269 } 270 271 272 /** 273 * Returns the bitmap from the given uri loaded using the given options. 274 * Returns null on failure. 275 */ 276 public static Bitmap loadBitmap(Context context, Uri uri, BitmapFactory.Options o) { 277 if (uri == null || context == null) { 278 throw new IllegalArgumentException("bad argument to loadBitmap"); 279 } 280 InputStream is = null; 281 try { 282 is = context.getContentResolver().openInputStream(uri); 283 return BitmapFactory.decodeStream(is, null, o); 284 } catch (FileNotFoundException e) { 285 Log.e(LOGTAG, "FileNotFoundException for " + uri, e); 286 } finally { 287 Utils.closeSilently(is); 288 } 289 return null; 290 } 291 292 /** 293 * Loads a bitmap at a given URI that is downsampled so that both sides are 294 * smaller than maxSideLength. The Bitmap's original dimensions are stored 295 * in the rect originalBounds. 296 * 297 * @param uri URI of image to open. 298 * @param context context whose ContentResolver to use. 299 * @param maxSideLength max side length of returned bitmap. 300 * @param originalBounds If not null, set to the actual bounds of the stored bitmap. 301 * @param useMin use min or max side of the original image 302 * @return downsampled bitmap or null if this operation failed. 303 */ 304 public static Bitmap loadConstrainedBitmap(Uri uri, Context context, int maxSideLength, 305 Rect originalBounds, boolean useMin) { 306 if (maxSideLength <= 0 || uri == null || context == null) { 307 throw new IllegalArgumentException("bad argument to getScaledBitmap"); 308 } 309 // Get width and height of stored bitmap 310 Rect storedBounds = loadBitmapBounds(context, uri); 311 if (originalBounds != null) { 312 originalBounds.set(storedBounds); 313 } 314 int w = storedBounds.width(); 315 int h = storedBounds.height(); 316 317 // If bitmap cannot be decoded, return null 318 if (w <= 0 || h <= 0) { 319 return null; 320 } 321 322 // Find best downsampling size 323 int imageSide = 0; 324 if (useMin) { 325 imageSide = Math.min(w, h); 326 } else { 327 imageSide = Math.max(w, h); 328 } 329 int sampleSize = 1; 330 while (imageSide > maxSideLength) { 331 imageSide >>>= 1; 332 sampleSize <<= 1; 333 } 334 335 // Make sure sample size is reasonable 336 if (sampleSize <= 0 || 337 0 >= (int) (Math.min(w, h) / sampleSize)) { 338 return null; 339 } 340 return loadDownsampledBitmap(context, uri, sampleSize); 341 } 342 343 /** 344 * Loads a bitmap at a given URI that is downsampled so that both sides are 345 * smaller than maxSideLength. The Bitmap's original dimensions are stored 346 * in the rect originalBounds. The output is also transformed to the given 347 * orientation. 348 * 349 * @param uri URI of image to open. 350 * @param context context whose ContentResolver to use. 351 * @param maxSideLength max side length of returned bitmap. 352 * @param orientation the orientation to transform the bitmap to. 353 * @param originalBounds set to the actual bounds of the stored bitmap. 354 * @return downsampled bitmap or null if this operation failed. 355 */ 356 public static Bitmap loadOrientedConstrainedBitmap(Uri uri, Context context, int maxSideLength, 357 int orientation, Rect originalBounds) { 358 Bitmap bmap = loadConstrainedBitmap(uri, context, maxSideLength, originalBounds, false); 359 if (bmap != null) { 360 bmap = orientBitmap(bmap, orientation); 361 } 362 return bmap; 363 } 364 365 public static Bitmap getScaleOneImageForPreset(Context context, Uri uri, Rect bounds, 366 Rect destination) { 367 BitmapFactory.Options options = new BitmapFactory.Options(); 368 options.inMutable = true; 369 if (destination != null) { 370 if (bounds.width() > destination.width()) { 371 int sampleSize = 1; 372 int w = bounds.width(); 373 while (w > destination.width()) { 374 sampleSize *= 2; 375 w /= sampleSize; 376 } 377 options.inSampleSize = sampleSize; 378 } 379 } 380 Bitmap bmp = loadRegionBitmap(context, uri, options, bounds); 381 return bmp; 382 } 383 384 /** 385 * Loads a bitmap that is downsampled by at least the input sample size. In 386 * low-memory situations, the bitmap may be downsampled further. 387 */ 388 public static Bitmap loadBitmapWithBackouts(Context context, Uri sourceUri, int sampleSize) { 389 boolean noBitmap = true; 390 int num_tries = 0; 391 if (sampleSize <= 0) { 392 sampleSize = 1; 393 } 394 Bitmap bmap = null; 395 while (noBitmap) { 396 try { 397 // Try to decode, downsample if low-memory. 398 bmap = loadDownsampledBitmap(context, sourceUri, sampleSize); 399 noBitmap = false; 400 } catch (java.lang.OutOfMemoryError e) { 401 // Try with more downsampling before failing for good. 402 if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) { 403 throw e; 404 } 405 bmap = null; 406 System.gc(); 407 sampleSize *= 2; 408 } 409 } 410 return bmap; 411 } 412 413 /** 414 * Loads an oriented bitmap that is downsampled by at least the input sample 415 * size. In low-memory situations, the bitmap may be downsampled further. 416 */ 417 public static Bitmap loadOrientedBitmapWithBackouts(Context context, Uri sourceUri, 418 int sampleSize) { 419 Bitmap bitmap = loadBitmapWithBackouts(context, sourceUri, sampleSize); 420 if (bitmap == null) { 421 return null; 422 } 423 int orientation = getMetadataOrientation(context, sourceUri); 424 bitmap = orientBitmap(bitmap, orientation); 425 return bitmap; 426 } 427 428 /** 429 * Loads bitmap from a resource that may be downsampled in low-memory situations. 430 */ 431 public static Bitmap decodeResourceWithBackouts(Resources res, BitmapFactory.Options options, 432 int id) { 433 boolean noBitmap = true; 434 int num_tries = 0; 435 if (options.inSampleSize < 1) { 436 options.inSampleSize = 1; 437 } 438 // Stopgap fix for low-memory devices. 439 Bitmap bmap = null; 440 while (noBitmap) { 441 try { 442 // Try to decode, downsample if low-memory. 443 bmap = BitmapFactory.decodeResource( 444 res, id, options); 445 noBitmap = false; 446 } catch (java.lang.OutOfMemoryError e) { 447 // Retry before failing for good. 448 if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) { 449 throw e; 450 } 451 bmap = null; 452 System.gc(); 453 options.inSampleSize *= 2; 454 } 455 } 456 return bmap; 457 } 458 459 public static XMPMeta getXmpObject(Context context) { 460 try { 461 InputStream is = context.getContentResolver().openInputStream( 462 MasterImage.getImage().getUri()); 463 return XmpUtilHelper.extractXMPMeta(is); 464 } catch (FileNotFoundException e) { 465 return null; 466 } 467 } 468 469 /** 470 * Determine if this is a light cycle 360 image 471 * 472 * @return true if it is a light Cycle image that is full 360 473 */ 474 public static boolean queryLightCycle360(Context context) { 475 InputStream is = null; 476 try { 477 is = context.getContentResolver().openInputStream(MasterImage.getImage().getUri()); 478 XMPMeta meta = XmpUtilHelper.extractXMPMeta(is); 479 if (meta == null) { 480 return false; 481 } 482 String namespace = "http://ns.google.com/photos/1.0/panorama/"; 483 String cropWidthName = "GPano:CroppedAreaImageWidthPixels"; 484 String fullWidthName = "GPano:FullPanoWidthPixels"; 485 486 if (!meta.doesPropertyExist(namespace, cropWidthName)) { 487 return false; 488 } 489 if (!meta.doesPropertyExist(namespace, fullWidthName)) { 490 return false; 491 } 492 493 Integer cropValue = meta.getPropertyInteger(namespace, cropWidthName); 494 Integer fullValue = meta.getPropertyInteger(namespace, fullWidthName); 495 496 // Definition of a 360: 497 // GFullPanoWidthPixels == CroppedAreaImageWidthPixels 498 if (cropValue != null && fullValue != null) { 499 return cropValue.equals(fullValue); 500 } 501 502 return false; 503 } catch (FileNotFoundException e) { 504 return false; 505 } catch (XMPException e) { 506 return false; 507 } finally { 508 Utils.closeSilently(is); 509 } 510 } 511 512 public static List<ExifTag> getExif(Context context, Uri uri) { 513 String path = getLocalPathFromUri(context, uri); 514 if (path != null) { 515 Uri localUri = Uri.parse(path); 516 String mimeType = getMimeType(localUri); 517 if (!JPEG_MIME_TYPE.equals(mimeType)) { 518 return null; 519 } 520 try { 521 ExifInterface exif = new ExifInterface(); 522 exif.readExif(path); 523 List<ExifTag> taglist = exif.getAllTags(); 524 return taglist; 525 } catch (IOException e) { 526 Log.w(LOGTAG, "Failed to read EXIF tags", e); 527 } 528 } 529 return null; 530 } 531} 532