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