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