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.Bitmap.CompressFormat; 26 import android.graphics.BitmapFactory; 27 import android.graphics.BitmapRegionDecoder; 28 import android.graphics.Matrix; 29 import android.graphics.Rect; 30 import android.graphics.Bitmap.CompressFormat; 31 import android.net.Uri; 32 import android.provider.MediaStore; 33 import android.util.Log; 34 35 import com.adobe.xmp.XMPException; 36 import com.adobe.xmp.XMPMeta; 37 import com.android.gallery3d.R; 38 import com.android.gallery3d.common.Utils; 39 import com.android.gallery3d.exif.ExifTag; 40 import com.android.gallery3d.exif.ExifInterface; 41 import com.android.gallery3d.filtershow.FilterShowActivity; 42 import com.android.gallery3d.filtershow.HistoryAdapter; 43 import com.android.gallery3d.filtershow.filters.FiltersManager; 44 import com.android.gallery3d.filtershow.imageshow.ImageShow; 45 import com.android.gallery3d.filtershow.imageshow.MasterImage; 46 import com.android.gallery3d.filtershow.presets.ImagePreset; 47 import com.android.gallery3d.filtershow.tools.BitmapTask; 48 import com.android.gallery3d.filtershow.tools.SaveCopyTask; 49 import com.android.gallery3d.util.InterruptableOutputStream; 50 import com.android.gallery3d.util.XmpUtilHelper; 51 52 import java.io.ByteArrayInputStream; 53 import java.io.Closeable; 54 import java.io.File; 55 import java.io.FileInputStream; 56 import java.io.FileNotFoundException; 57 import java.io.IOException; 58 import java.io.InputStream; 59 import java.io.OutputStream; 60 import java.util.Vector; 61 import java.util.concurrent.locks.ReentrantLock; 62 63 64 // TODO: this class has waaaay to much bitmap copying. Cleanup. 65 public class ImageLoader { 66 67 private static final String LOGTAG = "ImageLoader"; 68 private final Vector<ImageShow> mListeners = new Vector<ImageShow>(); 69 private Bitmap mOriginalBitmapSmall = null; 70 private Bitmap mOriginalBitmapLarge = null; 71 private Bitmap mOriginalBitmapHighres = null; 72 private Bitmap mBackgroundBitmap = null; 73 74 private final ZoomCache mZoomCache = new ZoomCache(); 75 76 private int mOrientation = 0; 77 private HistoryAdapter mAdapter = null; 78 79 private FilterShowActivity mActivity = null; 80 81 public static final String JPEG_MIME_TYPE = "image/jpeg"; 82 83 public static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos"; 84 public static final int DEFAULT_COMPRESS_QUALITY = 95; 85 86 public static final int ORI_NORMAL = ExifInterface.Orientation.TOP_LEFT; 87 public static final int ORI_ROTATE_90 = ExifInterface.Orientation.RIGHT_TOP; 88 public static final int ORI_ROTATE_180 = ExifInterface.Orientation.BOTTOM_LEFT; 89 public static final int ORI_ROTATE_270 = ExifInterface.Orientation.RIGHT_BOTTOM; 90 public static final int ORI_FLIP_HOR = ExifInterface.Orientation.TOP_RIGHT; 91 public static final int ORI_FLIP_VERT = ExifInterface.Orientation.BOTTOM_RIGHT; 92 public static final int ORI_TRANSPOSE = ExifInterface.Orientation.LEFT_TOP; 93 public static final int ORI_TRANSVERSE = ExifInterface.Orientation.LEFT_BOTTOM; 94 95 private static final int BITMAP_LOAD_BACKOUT_ATTEMPTS = 5; 96 private Context mContext = null; 97 private Uri mUri = null; 98 99 private Rect mOriginalBounds = null; 100 private static int mZoomOrientation = ORI_NORMAL; 101 102 static final int MAX_BITMAP_DIM = 900; 103 104 private ReentrantLock mLoadingLock = new ReentrantLock(); 105 106 public ImageLoader(FilterShowActivity activity, Context context) { 107 mActivity = activity; 108 mContext = context; 109 } 110 111 public static int getZoomOrientation() { 112 return mZoomOrientation; 113 } 114 115 public FilterShowActivity getActivity() { 116 return mActivity; 117 } 118 119 public boolean loadBitmap(Uri uri, int size) { 120 mLoadingLock.lock(); 121 mUri = uri; 122 mOrientation = getOrientation(mContext, uri); 123 mOriginalBitmapSmall = loadScaledBitmap(uri, 160); 124 if (mOriginalBitmapSmall == null) { 125 // Couldn't read the bitmap, let's exit 126 mLoadingLock.unlock(); 127 return false; 128 } 129 mOriginalBitmapLarge = loadScaledBitmap(uri, size); 130 if (mOriginalBitmapLarge == null) { 131 mLoadingLock.unlock(); 132 return false; 133 } 134 if (MasterImage.getImage().supportsHighRes()) { 135 int highresPreviewSize = mOriginalBitmapLarge.getWidth() * 2; 136 if (highresPreviewSize > mOriginalBounds.width()) { 137 highresPreviewSize = mOriginalBounds.width(); 138 } 139 mOriginalBitmapHighres = loadScaledBitmap(uri, highresPreviewSize, false); 140 } 141 updateBitmaps(); 142 mLoadingLock.unlock(); 143 return true; 144 } 145 146 public Uri getUri() { 147 return mUri; 148 } 149 150 public Rect getOriginalBounds() { 151 return mOriginalBounds; 152 } 153 154 public static int getOrientation(Context context, Uri uri) { 155 if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { 156 String mimeType = context.getContentResolver().getType(uri); 157 if (mimeType != ImageLoader.JPEG_MIME_TYPE) { 158 return -1; 159 } 160 String path = uri.getPath(); 161 int orientation = -1; 162 InputStream is = null; 163 ExifInterface exif = new ExifInterface(); 164 try { 165 exif.readExif(path); 166 orientation = ExifInterface.getRotationForOrientationValue( 167 exif.getTagIntValue(ExifInterface.TAG_ORIENTATION).shortValue()); 168 } catch (IOException e) { 169 Log.w(LOGTAG, "Failed to read EXIF orientation", e); 170 } 171 return orientation; 172 } 173 Cursor cursor = null; 174 try { 175 cursor = context.getContentResolver().query(uri, 176 new String[] { 177 MediaStore.Images.ImageColumns.ORIENTATION 178 }, 179 null, null, null); 180 if (cursor.moveToNext()) { 181 int ori = cursor.getInt(0); 182 183 switch (ori) { 184 case 0: 185 return ORI_NORMAL; 186 case 90: 187 return ORI_ROTATE_90; 188 case 270: 189 return ORI_ROTATE_270; 190 case 180: 191 return ORI_ROTATE_180; 192 default: 193 return -1; 194 } 195 } else { 196 return -1; 197 } 198 } catch (SQLiteException e) { 199 return -1; 200 } catch (IllegalArgumentException e) { 201 return -1; 202 } finally { 203 Utils.closeSilently(cursor); 204 } 205 } 206 207 private void updateBitmaps() { 208 if (mOrientation > 1) { 209 mOriginalBitmapSmall = rotateToPortrait(mOriginalBitmapSmall, mOrientation); 210 mOriginalBitmapLarge = rotateToPortrait(mOriginalBitmapLarge, mOrientation); 211 if (mOriginalBitmapHighres != null) { 212 mOriginalBitmapHighres = rotateToPortrait(mOriginalBitmapHighres, mOrientation); 213 } 214 } 215 mZoomOrientation = mOrientation; 216 warnListeners(); 217 } 218 219 public Bitmap decodeImage(int id, BitmapFactory.Options options) { 220 return BitmapFactory.decodeResource(mContext.getResources(), id, options); 221 } 222 223 public static Bitmap rotateToPortrait(Bitmap bitmap, int ori) { 224 Matrix matrix = new Matrix(); 225 int w = bitmap.getWidth(); 226 int h = bitmap.getHeight(); 227 if (ori == ORI_ROTATE_90 || 228 ori == ORI_ROTATE_270 || 229 ori == ORI_TRANSPOSE || 230 ori == ORI_TRANSVERSE) { 231 int tmp = w; 232 w = h; 233 h = tmp; 234 } 235 switch (ori) { 236 case ORI_ROTATE_90: 237 matrix.setRotate(90, w / 2f, h / 2f); 238 break; 239 case ORI_ROTATE_180: 240 matrix.setRotate(180, w / 2f, h / 2f); 241 break; 242 case ORI_ROTATE_270: 243 matrix.setRotate(270, w / 2f, h / 2f); 244 break; 245 case ORI_FLIP_HOR: 246 matrix.preScale(-1, 1); 247 break; 248 case ORI_FLIP_VERT: 249 matrix.preScale(1, -1); 250 break; 251 case ORI_TRANSPOSE: 252 matrix.setRotate(90, w / 2f, h / 2f); 253 matrix.preScale(1, -1); 254 break; 255 case ORI_TRANSVERSE: 256 matrix.setRotate(270, w / 2f, h / 2f); 257 matrix.preScale(1, -1); 258 break; 259 case ORI_NORMAL: 260 default: 261 return bitmap; 262 } 263 264 return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), 265 bitmap.getHeight(), matrix, true); 266 } 267 268 private Bitmap loadRegionBitmap(Uri uri, BitmapFactory.Options options, Rect bounds) { 269 InputStream is = null; 270 try { 271 is = mContext.getContentResolver().openInputStream(uri); 272 BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false); 273 Rect r = new Rect(0, 0, decoder.getWidth(), decoder.getHeight()); 274 // return null if bounds are not entirely within the bitmap 275 if (!r.contains(bounds)) { 276 return null; 277 } 278 return decoder.decodeRegion(bounds, options); 279 } catch (FileNotFoundException e) { 280 Log.e(LOGTAG, "FileNotFoundException: " + uri); 281 } catch (Exception e) { 282 e.printStackTrace(); 283 } finally { 284 Utils.closeSilently(is); 285 } 286 return null; 287 } 288 289 private Bitmap loadScaledBitmap(Uri uri, int size) { 290 return loadScaledBitmap(uri, size, true); 291 } 292 293 private Bitmap loadScaledBitmap(Uri uri, int size, boolean enforceSize) { 294 InputStream is = null; 295 try { 296 is = mContext.getContentResolver().openInputStream(uri); 297 Log.v(LOGTAG, "loading uri " + uri.getPath() + " input stream: " 298 + is); 299 BitmapFactory.Options o = new BitmapFactory.Options(); 300 o.inJustDecodeBounds = true; 301 BitmapFactory.decodeStream(is, null, o); 302 303 int width_tmp = o.outWidth; 304 int height_tmp = o.outHeight; 305 306 mOriginalBounds = new Rect(0, 0, width_tmp, height_tmp); 307 308 int scale = 1; 309 while (true) { 310 if (width_tmp <= 2 || height_tmp <= 2) { 311 break; 312 } 313 if (!enforceSize 314 || (width_tmp <= MAX_BITMAP_DIM 315 && height_tmp <= MAX_BITMAP_DIM)) { 316 if (width_tmp / 2 < size || height_tmp / 2 < size) { 317 break; 318 } 319 } 320 width_tmp /= 2; 321 height_tmp /= 2; 322 scale *= 2; 323 } 324 325 // decode with inSampleSize 326 BitmapFactory.Options o2 = new BitmapFactory.Options(); 327 o2.inSampleSize = scale; 328 o2.inMutable = true; 329 330 Utils.closeSilently(is); 331 is = mContext.getContentResolver().openInputStream(uri); 332 return BitmapFactory.decodeStream(is, null, o2); 333 } catch (FileNotFoundException e) { 334 Log.e(LOGTAG, "FileNotFoundException: " + uri); 335 } catch (Exception e) { 336 e.printStackTrace(); 337 } finally { 338 Utils.closeSilently(is); 339 } 340 return null; 341 } 342 343 public Bitmap getBackgroundBitmap(Resources resources) { 344 if (mBackgroundBitmap == null) { 345 mBackgroundBitmap = BitmapFactory.decodeResource(resources, 346 R.drawable.filtershow_background); 347 } 348 return mBackgroundBitmap; 349 350 } 351 352 public Bitmap getOriginalBitmapSmall() { 353 return mOriginalBitmapSmall; 354 } 355 356 public Bitmap getOriginalBitmapLarge() { 357 return mOriginalBitmapLarge; 358 } 359 360 public Bitmap getOriginalBitmapHighres() { 361 return mOriginalBitmapHighres; 362 } 363 364 public void addListener(ImageShow imageShow) { 365 mLoadingLock.lock(); 366 if (!mListeners.contains(imageShow)) { 367 mListeners.add(imageShow); 368 } 369 mLoadingLock.unlock(); 370 } 371 372 private void warnListeners() { 373 mActivity.runOnUiThread(mWarnListenersRunnable); 374 } 375 376 private Runnable mWarnListenersRunnable = new Runnable() { 377 378 @Override 379 public void run() { 380 for (int i = 0; i < mListeners.size(); i++) { 381 ImageShow imageShow = mListeners.elementAt(i); 382 imageShow.imageLoaded(); 383 } 384 } 385 }; 386 387 public Bitmap getScaleOneImageForPreset(Rect bounds, Rect destination) { 388 mLoadingLock.lock(); 389 BitmapFactory.Options options = new BitmapFactory.Options(); 390 options.inMutable = true; 391 if (destination != null) { 392 if (bounds.width() > destination.width()) { 393 int sampleSize = 1; 394 int w = bounds.width(); 395 while (w > destination.width()) { 396 sampleSize *= 2; 397 w /= sampleSize; 398 } 399 options.inSampleSize = sampleSize; 400 } 401 } 402 Bitmap bmp = loadRegionBitmap(mUri, options, bounds); 403 mLoadingLock.unlock(); 404 return bmp; 405 } 406 407 public void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity, 408 File destination) { 409 new SaveCopyTask(mContext, mUri, destination, new SaveCopyTask.Callback() { 410 411 @Override 412 public void onComplete(Uri result) { 413 filterShowActivity.completeSaveImage(result); 414 } 415 416 }).execute(preset); 417 } 418 419 public static Bitmap loadMutableBitmap(Context context, Uri sourceUri) { 420 BitmapFactory.Options options = new BitmapFactory.Options(); 421 return loadMutableBitmap(context, sourceUri, options); 422 } 423 424 public static Bitmap loadMutableBitmap(Context context, Uri sourceUri, 425 BitmapFactory.Options options) { 426 // TODO: on <3.x we need a copy of the bitmap (inMutable doesn't 427 // exist) 428 options.inMutable = true; 429 430 Bitmap bitmap = decodeUriWithBackouts(context, sourceUri, options); 431 if (bitmap == null) { 432 return null; 433 } 434 int orientation = ImageLoader.getOrientation(context, sourceUri); 435 bitmap = ImageLoader.rotateToPortrait(bitmap, orientation); 436 return bitmap; 437 } 438 439 public static Bitmap decodeUriWithBackouts(Context context, Uri sourceUri, 440 BitmapFactory.Options options) { 441 boolean noBitmap = true; 442 int num_tries = 0; 443 InputStream is = getInputStream(context, sourceUri); 444 445 if (options.inSampleSize < 1) { 446 options.inSampleSize = 1; 447 } 448 // Stopgap fix for low-memory devices. 449 Bitmap bmap = null; 450 while (noBitmap) { 451 if (is == null) { 452 return null; 453 } 454 try { 455 // Try to decode, downsample if low-memory. 456 bmap = BitmapFactory.decodeStream(is, null, options); 457 noBitmap = false; 458 } catch (java.lang.OutOfMemoryError e) { 459 // Try 5 times before failing for good. 460 if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) { 461 throw e; 462 } 463 is = null; 464 bmap = null; 465 System.gc(); 466 is = getInputStream(context, sourceUri); 467 options.inSampleSize *= 2; 468 } 469 } 470 Utils.closeSilently(is); 471 return bmap; 472 } 473 474 private static InputStream getInputStream(Context context, Uri sourceUri) { 475 InputStream is = null; 476 try { 477 is = context.getContentResolver().openInputStream(sourceUri); 478 } catch (FileNotFoundException e) { 479 Log.w(LOGTAG, "could not load bitmap ", e); 480 Utils.closeSilently(is); 481 is = null; 482 } 483 return is; 484 } 485 486 public static Bitmap decodeResourceWithBackouts(Resources res, BitmapFactory.Options options, 487 int id) { 488 boolean noBitmap = true; 489 int num_tries = 0; 490 if (options.inSampleSize < 1) { 491 options.inSampleSize = 1; 492 } 493 // Stopgap fix for low-memory devices. 494 Bitmap bmap = null; 495 while (noBitmap) { 496 try { 497 // Try to decode, downsample if low-memory. 498 bmap = BitmapFactory.decodeResource( 499 res, id, options); 500 noBitmap = false; 501 } catch (java.lang.OutOfMemoryError e) { 502 // Try 5 times before failing for good. 503 if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) { 504 throw e; 505 } 506 bmap = null; 507 System.gc(); 508 options.inSampleSize *= 2; 509 } 510 } 511 return bmap; 512 } 513 514 public void returnFilteredResult(ImagePreset preset, 515 final FilterShowActivity filterShowActivity) { 516 BitmapTask.Callbacks<ImagePreset> cb = new BitmapTask.Callbacks<ImagePreset>() { 517 518 @Override 519 public void onComplete(Bitmap result) { 520 filterShowActivity.onFilteredResult(result); 521 } 522 523 @Override 524 public void onCancel() { 525 } 526 527 @Override 528 public Bitmap onExecute(ImagePreset param) { 529 if (param == null || mUri == null) { 530 return null; 531 } 532 BitmapFactory.Options options = new BitmapFactory.Options(); 533 boolean noBitmap = true; 534 int num_tries = 0; 535 if (options.inSampleSize < 1) { 536 options.inSampleSize = 1; 537 } 538 Bitmap bitmap = null; 539 // Stopgap fix for low-memory devices. 540 while (noBitmap) { 541 try { 542 // Try to do bitmap operations, downsample if low-memory 543 bitmap = loadMutableBitmap(mContext, mUri, options); 544 if (bitmap == null) { 545 Log.w(LOGTAG, "Failed to save image!"); 546 return null; 547 } 548 CachingPipeline pipeline = new CachingPipeline( 549 FiltersManager.getManager(), "Saving"); 550 bitmap = pipeline.renderFinalImage(bitmap, param); 551 noBitmap = false; 552 } catch (java.lang.OutOfMemoryError e) { 553 // Try 5 times before failing for good. 554 if (++num_tries >= 5) { 555 throw e; 556 } 557 bitmap = null; 558 System.gc(); 559 options.inSampleSize *= 2; 560 } 561 } 562 return bitmap; 563 } 564 }; 565 566 (new BitmapTask<ImagePreset>(cb)).execute(preset); 567 } 568 569 private String getFileExtension(String requestFormat) { 570 String outputFormat = (requestFormat == null) 571 ? "jpg" 572 : requestFormat; 573 outputFormat = outputFormat.toLowerCase(); 574 return (outputFormat.equals("png") || outputFormat.equals("gif")) 575 ? "png" // We don't support gif compression. 576 : "jpg"; 577 } 578 579 private CompressFormat convertExtensionToCompressFormat(String extension) { 580 return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG; 581 } 582 583 public void saveToUri(Bitmap bmap, Uri uri, final String outputFormat, 584 final FilterShowActivity filterShowActivity) { 585 586 OutputStream out = null; 587 try { 588 out = filterShowActivity.getContentResolver().openOutputStream(uri); 589 } catch (FileNotFoundException e) { 590 Log.w(LOGTAG, "cannot write output", e); 591 out = null; 592 } finally { 593 if (bmap == null || out == null) { 594 return; 595 } 596 } 597 598 final InterruptableOutputStream ios = new InterruptableOutputStream(out); 599 600 BitmapTask.Callbacks<Bitmap> cb = new BitmapTask.Callbacks<Bitmap>() { 601 602 @Override 603 public void onComplete(Bitmap result) { 604 filterShowActivity.done(); 605 } 606 607 @Override 608 public void onCancel() { 609 ios.interrupt(); 610 } 611 612 @Override 613 public Bitmap onExecute(Bitmap param) { 614 CompressFormat cf = convertExtensionToCompressFormat(getFileExtension(outputFormat)); 615 param.compress(cf, DEFAULT_COMPRESS_QUALITY, ios); 616 Utils.closeSilently(ios); 617 return null; 618 } 619 }; 620 621 (new BitmapTask<Bitmap>(cb)).execute(bmap); 622 } 623 624 public void setAdapter(HistoryAdapter adapter) { 625 mAdapter = adapter; 626 } 627 628 public HistoryAdapter getHistory() { 629 return mAdapter; 630 } 631 632 public XMPMeta getXmpObject() { 633 try { 634 InputStream is = mContext.getContentResolver().openInputStream(getUri()); 635 return XmpUtilHelper.extractXMPMeta(is); 636 } catch (FileNotFoundException e) { 637 return null; 638 } 639 } 640 641 /** 642 * Determine if this is a light cycle 360 image 643 * 644 * @return true if it is a light Cycle image that is full 360 645 */ 646 public boolean queryLightCycle360() { 647 InputStream is = null; 648 try { 649 is = mContext.getContentResolver().openInputStream(getUri()); 650 XMPMeta meta = XmpUtilHelper.extractXMPMeta(is); 651 if (meta == null) { 652 return false; 653 } 654 String name = meta.getPacketHeader(); 655 String namespace = "http://ns.google.com/photos/1.0/panorama/"; 656 String cropWidthName = "GPano:CroppedAreaImageWidthPixels"; 657 String fullWidthName = "GPano:FullPanoWidthPixels"; 658 659 if (!meta.doesPropertyExist(namespace, cropWidthName)) { 660 return false; 661 } 662 if (!meta.doesPropertyExist(namespace, fullWidthName)) { 663 return false; 664 } 665 666 Integer cropValue = meta.getPropertyInteger(namespace, cropWidthName); 667 Integer fullValue = meta.getPropertyInteger(namespace, fullWidthName); 668 669 // Definition of a 360: 670 // GFullPanoWidthPixels == CroppedAreaImageWidthPixels 671 if (cropValue != null && fullValue != null) { 672 return cropValue.equals(fullValue); 673 } 674 675 return false; 676 } catch (FileNotFoundException e) { 677 return false; 678 } catch (XMPException e) { 679 return false; 680 } finally { 681 Utils.closeSilently(is); 682 } 683 } 684 } 685