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.MasterImage; 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; 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 */ 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 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 */ 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 133 // Fall back to checking EXIF tags in file. 134 if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { 135 String mimeType = getMimeType(uri); 136 if (!JPEG_MIME_TYPE.equals(mimeType)) { 137 return ORI_NORMAL; 138 } 139 String path = uri.getPath(); 140 ExifInterface exif = new ExifInterface(); 141 try { 142 exif.readExif(path); 143 Integer tagval = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION); 144 if (tagval != null) { 145 int orientation = tagval; 146 switch(orientation) { 147 case ORI_NORMAL: 148 case ORI_ROTATE_90: 149 case ORI_ROTATE_180: 150 case ORI_ROTATE_270: 151 case ORI_FLIP_HOR: 152 case ORI_FLIP_VERT: 153 case ORI_TRANSPOSE: 154 case ORI_TRANSVERSE: 155 return orientation; 156 default: 157 return ORI_NORMAL; 158 } 159 } 160 } catch (IOException e) { 161 Log.w(LOGTAG, "Failed to read EXIF orientation", e); 162 } 163 } 164 return ORI_NORMAL; 165 } 166 167 /** 168 * Returns the rotation of image at the given URI as one of 0, 90, 180, 169 * 270. Defaults to 0. 170 */ 171 public static int getMetadataRotation(Context context, Uri uri) { 172 int orientation = getMetadataOrientation(context, uri); 173 switch(orientation) { 174 case ORI_ROTATE_90: 175 return 90; 176 case ORI_ROTATE_180: 177 return 180; 178 case ORI_ROTATE_270: 179 return 270; 180 default: 181 return 0; 182 } 183 } 184 185 /** 186 * Takes an orientation and a bitmap, and returns the bitmap transformed 187 * to that orientation. 188 */ 189 public static Bitmap orientBitmap(Bitmap bitmap, int ori) { 190 Matrix matrix = new Matrix(); 191 int w = bitmap.getWidth(); 192 int h = bitmap.getHeight(); 193 if (ori == ORI_ROTATE_90 || 194 ori == ORI_ROTATE_270 || 195 ori == ORI_TRANSPOSE || 196 ori == ORI_TRANSVERSE) { 197 int tmp = w; 198 w = h; 199 h = tmp; 200 } 201 switch (ori) { 202 case ORI_ROTATE_90: 203 matrix.setRotate(90, w / 2f, h / 2f); 204 break; 205 case ORI_ROTATE_180: 206 matrix.setRotate(180, w / 2f, h / 2f); 207 break; 208 case ORI_ROTATE_270: 209 matrix.setRotate(270, w / 2f, h / 2f); 210 break; 211 case ORI_FLIP_HOR: 212 matrix.preScale(-1, 1); 213 break; 214 case ORI_FLIP_VERT: 215 matrix.preScale(1, -1); 216 break; 217 case ORI_TRANSPOSE: 218 matrix.setRotate(90, w / 2f, h / 2f); 219 matrix.preScale(1, -1); 220 break; 221 case ORI_TRANSVERSE: 222 matrix.setRotate(270, w / 2f, h / 2f); 223 matrix.preScale(1, -1); 224 break; 225 case ORI_NORMAL: 226 default: 227 return bitmap; 228 } 229 return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), 230 bitmap.getHeight(), matrix, true); 231 } 232 233 /** 234 * Returns the bitmap for the rectangular region given by "bounds" 235 * if it is a subset of the bitmap stored at uri. Otherwise returns 236 * null. 237 */ 238 public static Bitmap loadRegionBitmap(Context context, BitmapCache cache, 239 Uri uri, BitmapFactory.Options options, 240 Rect bounds) { 241 InputStream is = null; 242 int w = 0; 243 int h = 0; 244 if (options.inSampleSize != 0) { 245 return null; 246 } 247 try { 248 is = context.getContentResolver().openInputStream(uri); 249 BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false); 250 Rect r = new Rect(0, 0, decoder.getWidth(), decoder.getHeight()); 251 w = decoder.getWidth(); 252 h = decoder.getHeight(); 253 Rect imageBounds = new Rect(bounds); 254 // return null if bounds are not entirely within the bitmap 255 if (!r.contains(imageBounds)) { 256 imageBounds.intersect(r); 257 bounds.left = imageBounds.left; 258 bounds.top = imageBounds.top; 259 } 260 Bitmap reuse = cache.getBitmap(imageBounds.width(), 261 imageBounds.height(), BitmapCache.REGION); 262 options.inBitmap = reuse; 263 Bitmap bitmap = decoder.decodeRegion(imageBounds, options); 264 if (bitmap != reuse) { 265 cache.cache(reuse); // not reused, put back in cache 266 } 267 return bitmap; 268 } catch (FileNotFoundException e) { 269 Log.e(LOGTAG, "FileNotFoundException for " + uri, e); 270 } catch (IOException e) { 271 Log.e(LOGTAG, "FileNotFoundException for " + uri, e); 272 } catch (IllegalArgumentException e) { 273 Log.e(LOGTAG, "exc, image decoded " + w + " x " + h + " bounds: " 274 + bounds.left + "," + bounds.top + " - " 275 + bounds.width() + "x" + bounds.height() + " exc: " + e); 276 } finally { 277 Utils.closeSilently(is); 278 } 279 return null; 280 } 281 282 /** 283 * Returns the bounds of the bitmap stored at a given Url. 284 */ 285 public static Rect loadBitmapBounds(Context context, Uri uri) { 286 BitmapFactory.Options o = new BitmapFactory.Options(); 287 o.inJustDecodeBounds = true; 288 loadBitmap(context, uri, o); 289 return new Rect(0, 0, o.outWidth, o.outHeight); 290 } 291 292 /** 293 * Loads a bitmap that has been downsampled using sampleSize from a given url. 294 */ 295 public static Bitmap loadDownsampledBitmap(Context context, Uri uri, int sampleSize) { 296 BitmapFactory.Options options = new BitmapFactory.Options(); 297 options.inMutable = true; 298 options.inSampleSize = sampleSize; 299 return loadBitmap(context, uri, options); 300 } 301 302 303 /** 304 * Returns the bitmap from the given uri loaded using the given options. 305 * Returns null on failure. 306 */ 307 public static Bitmap loadBitmap(Context context, Uri uri, BitmapFactory.Options o) { 308 if (uri == null || context == null) { 309 throw new IllegalArgumentException("bad argument to loadBitmap"); 310 } 311 InputStream is = null; 312 try { 313 is = context.getContentResolver().openInputStream(uri); 314 return BitmapFactory.decodeStream(is, null, o); 315 } catch (FileNotFoundException e) { 316 Log.e(LOGTAG, "FileNotFoundException for " + uri, e); 317 } finally { 318 Utils.closeSilently(is); 319 } 320 return null; 321 } 322 323 /** 324 * Loads a bitmap at a given URI that is downsampled so that both sides are 325 * smaller than maxSideLength. The Bitmap's original dimensions are stored 326 * in the rect originalBounds. 327 * 328 * @param uri URI of image to open. 329 * @param context context whose ContentResolver to use. 330 * @param maxSideLength max side length of returned bitmap. 331 * @param originalBounds If not null, set to the actual bounds of the stored bitmap. 332 * @param useMin use min or max side of the original image 333 * @return downsampled bitmap or null if this operation failed. 334 */ 335 public static Bitmap loadConstrainedBitmap(Uri uri, Context context, int maxSideLength, 336 Rect originalBounds, boolean useMin) { 337 if (maxSideLength <= 0 || uri == null || context == null) { 338 throw new IllegalArgumentException("bad argument to getScaledBitmap"); 339 } 340 // Get width and height of stored bitmap 341 Rect storedBounds = loadBitmapBounds(context, uri); 342 if (originalBounds != null) { 343 originalBounds.set(storedBounds); 344 } 345 int w = storedBounds.width(); 346 int h = storedBounds.height(); 347 348 // If bitmap cannot be decoded, return null 349 if (w <= 0 || h <= 0) { 350 return null; 351 } 352 353 // Find best downsampling size 354 int imageSide = 0; 355 if (useMin) { 356 imageSide = Math.min(w, h); 357 } else { 358 imageSide = Math.max(w, h); 359 } 360 int sampleSize = 1; 361 while (imageSide > maxSideLength) { 362 imageSide >>>= 1; 363 sampleSize <<= 1; 364 } 365 366 // Make sure sample size is reasonable 367 if (sampleSize <= 0 || 368 0 >= (int) (Math.min(w, h) / sampleSize)) { 369 return null; 370 } 371 return loadDownsampledBitmap(context, uri, sampleSize); 372 } 373 374 /** 375 * Loads a bitmap at a given URI that is downsampled so that both sides are 376 * smaller than maxSideLength. The Bitmap's original dimensions are stored 377 * in the rect originalBounds. The output is also transformed to the given 378 * orientation. 379 * 380 * @param uri URI of image to open. 381 * @param context context whose ContentResolver to use. 382 * @param maxSideLength max side length of returned bitmap. 383 * @param orientation the orientation to transform the bitmap to. 384 * @param originalBounds set to the actual bounds of the stored bitmap. 385 * @return downsampled bitmap or null if this operation failed. 386 */ 387 public static Bitmap loadOrientedConstrainedBitmap(Uri uri, Context context, int maxSideLength, 388 int orientation, Rect originalBounds) { 389 Bitmap bmap = loadConstrainedBitmap(uri, context, maxSideLength, originalBounds, false); 390 if (bmap != null) { 391 bmap = orientBitmap(bmap, orientation); 392 if (bmap.getConfig()!= Bitmap.Config.ARGB_8888){ 393 bmap = bmap.copy( Bitmap.Config.ARGB_8888,true); 394 } 395 } 396 return bmap; 397 } 398 399 public static Bitmap getScaleOneImageForPreset(Context context, 400 BitmapCache cache, 401 Uri uri, Rect bounds, 402 Rect destination) { 403 BitmapFactory.Options options = new BitmapFactory.Options(); 404 options.inMutable = true; 405 if (destination != null) { 406 int thresholdWidth = (int) (destination.width() * OVERDRAW_ZOOM); 407 if (bounds.width() > thresholdWidth) { 408 int sampleSize = 1; 409 int w = bounds.width(); 410 while (w > thresholdWidth) { 411 sampleSize *= 2; 412 w /= sampleSize; 413 } 414 options.inSampleSize = sampleSize; 415 } 416 } 417 return loadRegionBitmap(context, cache, uri, options, bounds); 418 } 419 420 /** 421 * Loads a bitmap that is downsampled by at least the input sample size. In 422 * low-memory situations, the bitmap may be downsampled further. 423 */ 424 public static Bitmap loadBitmapWithBackouts(Context context, Uri sourceUri, int sampleSize) { 425 boolean noBitmap = true; 426 int num_tries = 0; 427 if (sampleSize <= 0) { 428 sampleSize = 1; 429 } 430 Bitmap bmap = null; 431 while (noBitmap) { 432 try { 433 // Try to decode, downsample if low-memory. 434 bmap = loadDownsampledBitmap(context, sourceUri, sampleSize); 435 noBitmap = false; 436 } catch (java.lang.OutOfMemoryError e) { 437 // Try with more downsampling before failing for good. 438 if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) { 439 throw e; 440 } 441 bmap = null; 442 System.gc(); 443 sampleSize *= 2; 444 } 445 } 446 return bmap; 447 } 448 449 /** 450 * Loads an oriented bitmap that is downsampled by at least the input sample 451 * size. In low-memory situations, the bitmap may be downsampled further. 452 */ 453 public static Bitmap loadOrientedBitmapWithBackouts(Context context, Uri sourceUri, 454 int sampleSize) { 455 Bitmap bitmap = loadBitmapWithBackouts(context, sourceUri, sampleSize); 456 if (bitmap == null) { 457 return null; 458 } 459 int orientation = getMetadataOrientation(context, sourceUri); 460 bitmap = orientBitmap(bitmap, orientation); 461 return bitmap; 462 } 463 464 /** 465 * Loads bitmap from a resource that may be downsampled in low-memory situations. 466 */ 467 public static Bitmap decodeResourceWithBackouts(Resources res, BitmapFactory.Options options, 468 int id) { 469 boolean noBitmap = true; 470 int num_tries = 0; 471 if (options.inSampleSize < 1) { 472 options.inSampleSize = 1; 473 } 474 // Stopgap fix for low-memory devices. 475 Bitmap bmap = null; 476 while (noBitmap) { 477 try { 478 // Try to decode, downsample if low-memory. 479 bmap = BitmapFactory.decodeResource( 480 res, id, options); 481 noBitmap = false; 482 } catch (java.lang.OutOfMemoryError e) { 483 // Retry before failing for good. 484 if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) { 485 throw e; 486 } 487 bmap = null; 488 System.gc(); 489 options.inSampleSize *= 2; 490 } 491 } 492 return bmap; 493 } 494 495 public static XMPMeta getXmpObject(Context context) { 496 try { 497 InputStream is = context.getContentResolver().openInputStream( 498 MasterImage.getImage().getUri()); 499 return XmpUtilHelper.extractXMPMeta(is); 500 } catch (FileNotFoundException e) { 501 return null; 502 } 503 } 504 505 /** 506 * Determine if this is a light cycle 360 image 507 * 508 * @return true if it is a light Cycle image that is full 360 509 */ 510 public static boolean queryLightCycle360(Context context) { 511 InputStream is = null; 512 try { 513 is = context.getContentResolver().openInputStream(MasterImage.getImage().getUri()); 514 XMPMeta meta = XmpUtilHelper.extractXMPMeta(is); 515 if (meta == null) { 516 return false; 517 } 518 String namespace = "http://ns.google.com/photos/1.0/panorama/"; 519 String cropWidthName = "GPano:CroppedAreaImageWidthPixels"; 520 String fullWidthName = "GPano:FullPanoWidthPixels"; 521 522 if (!meta.doesPropertyExist(namespace, cropWidthName)) { 523 return false; 524 } 525 if (!meta.doesPropertyExist(namespace, fullWidthName)) { 526 return false; 527 } 528 529 Integer cropValue = meta.getPropertyInteger(namespace, cropWidthName); 530 Integer fullValue = meta.getPropertyInteger(namespace, fullWidthName); 531 532 // Definition of a 360: 533 // GFullPanoWidthPixels == CroppedAreaImageWidthPixels 534 if (cropValue != null && fullValue != null) { 535 return cropValue.equals(fullValue); 536 } 537 538 return false; 539 } catch (FileNotFoundException e) { 540 return false; 541 } catch (XMPException e) { 542 return false; 543 } finally { 544 Utils.closeSilently(is); 545 } 546 } 547 548 public static List<ExifTag> getExif(Context context, Uri uri) { 549 String path = getLocalPathFromUri(context, uri); 550 if (path != null) { 551 Uri localUri = Uri.parse(path); 552 String mimeType = getMimeType(localUri); 553 if (!JPEG_MIME_TYPE.equals(mimeType)) { 554 return null; 555 } 556 try { 557 ExifInterface exif = new ExifInterface(); 558 exif.readExif(path); 559 List<ExifTag> taglist = exif.getAllTags(); 560 return taglist; 561 } catch (IOException e) { 562 Log.w(LOGTAG, "Failed to read EXIF tags", e); 563 } 564 } 565 return null; 566 } 567 } 568