ImageLoader.java revision a0d689f180db4c995d5be39a326b1fb5b21ff5c0
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 final ZoomCache mZoomCache = new ZoomCache(); 74 75 private int mOrientation = 0; 76 private HistoryAdapter mAdapter = null; 77 78 private FilterShowActivity mActivity = null; 79 80 public static final String JPEG_MIME_TYPE = "image/jpeg"; 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 } 106 107 public static int getZoomOrientation() { 108 return mZoomOrientation; 109 } 110 111 public FilterShowActivity getActivity() { 112 return mActivity; 113 } 114 115 public boolean loadBitmap(Uri uri, int size) { 116 mLoadingLock.lock(); 117 mUri = uri; 118 mOrientation = getOrientation(mContext, uri); 119 mOriginalBitmapSmall = loadScaledBitmap(uri, 160); 120 if (mOriginalBitmapSmall == null) { 121 // Couldn't read the bitmap, let's exit 122 mLoadingLock.unlock(); 123 return false; 124 } 125 mOriginalBitmapLarge = loadScaledBitmap(uri, size); 126 if (mOriginalBitmapLarge == null) { 127 mLoadingLock.unlock(); 128 return false; 129 } 130 updateBitmaps(); 131 mLoadingLock.unlock(); 132 return true; 133 } 134 135 public Uri getUri() { 136 return mUri; 137 } 138 139 public Rect getOriginalBounds() { 140 return mOriginalBounds; 141 } 142 143 public static int getOrientation(Context context, Uri uri) { 144 if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { 145 String mimeType = context.getContentResolver().getType(uri); 146 if (mimeType != ImageLoader.JPEG_MIME_TYPE) { 147 return -1; 148 } 149 String path = uri.getPath(); 150 int orientation = -1; 151 InputStream is = null; 152 try { 153 is = new FileInputStream(path); 154 ExifParser parser = ExifParser.parse(is, ExifParser.OPTION_IFD_0); 155 int event = parser.next(); 156 while (event != ExifParser.EVENT_END) { 157 if (event == ExifParser.EVENT_NEW_TAG) { 158 ExifTag tag = parser.getTag(); 159 if (tag.getTagId() == ExifTag.TAG_ORIENTATION) { 160 orientation = (int) tag.getValueAt(0); 161 break; 162 } 163 } 164 event = parser.next(); 165 } 166 } catch (IOException e) { 167 e.printStackTrace(); 168 } catch (ExifInvalidFormatException e) { 169 e.printStackTrace(); 170 } finally { 171 Utils.closeSilently(is); 172 } 173 return orientation; 174 } 175 Cursor cursor = null; 176 try { 177 cursor = context.getContentResolver().query(uri, 178 new String[] { 179 MediaStore.Images.ImageColumns.ORIENTATION 180 }, 181 null, null, null); 182 if (cursor.moveToNext()) { 183 int ori = cursor.getInt(0); 184 185 switch (ori) { 186 case 0: 187 return ORI_NORMAL; 188 case 90: 189 return ORI_ROTATE_90; 190 case 270: 191 return ORI_ROTATE_270; 192 case 180: 193 return ORI_ROTATE_180; 194 default: 195 return -1; 196 } 197 } else { 198 return -1; 199 } 200 } catch (SQLiteException e) { 201 return ExifInterface.ORIENTATION_UNDEFINED; 202 } catch (IllegalArgumentException e) { 203 return ExifInterface.ORIENTATION_UNDEFINED; 204 } finally { 205 Utils.closeSilently(cursor); 206 } 207 } 208 209 private void updateBitmaps() { 210 if (mOrientation > 1) { 211 mOriginalBitmapSmall = rotateToPortrait(mOriginalBitmapSmall, mOrientation); 212 mOriginalBitmapLarge = rotateToPortrait(mOriginalBitmapLarge, mOrientation); 213 } 214 mZoomOrientation = mOrientation; 215 warnListeners(); 216 } 217 218 public Bitmap decodeImage(int id, BitmapFactory.Options options) { 219 return BitmapFactory.decodeResource(mContext.getResources(), id, options); 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 Bitmap loadRegionBitmap(Uri uri, Rect bounds) { 268 InputStream is = null; 269 try { 270 is = mContext.getContentResolver().openInputStream(uri); 271 BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false); 272 return decoder.decodeRegion(bounds, null); 273 } catch (FileNotFoundException e) { 274 Log.e(LOGTAG, "FileNotFoundException: " + uri); 275 } catch (Exception e) { 276 e.printStackTrace(); 277 } finally { 278 Utils.closeSilently(is); 279 } 280 return null; 281 } 282 283 static final int MAX_BITMAP_DIM = 900; 284 285 private Bitmap loadScaledBitmap(Uri uri, int size) { 286 InputStream is = null; 287 try { 288 is = mContext.getContentResolver().openInputStream(uri); 289 Log.v(LOGTAG, "loading uri " + uri.getPath() + " input stream: " 290 + is); 291 BitmapFactory.Options o = new BitmapFactory.Options(); 292 o.inJustDecodeBounds = true; 293 BitmapFactory.decodeStream(is, null, o); 294 295 int width_tmp = o.outWidth; 296 int height_tmp = o.outHeight; 297 298 mOriginalBounds = new Rect(0, 0, width_tmp, height_tmp); 299 300 int scale = 1; 301 while (true) { 302 if (width_tmp <= MAX_BITMAP_DIM && height_tmp <= MAX_BITMAP_DIM) { 303 if (width_tmp / 2 < size || height_tmp / 2 < size) { 304 break; 305 } 306 } 307 width_tmp /= 2; 308 height_tmp /= 2; 309 scale *= 2; 310 } 311 312 // decode with inSampleSize 313 BitmapFactory.Options o2 = new BitmapFactory.Options(); 314 o2.inSampleSize = scale; 315 316 Utils.closeSilently(is); 317 is = mContext.getContentResolver().openInputStream(uri); 318 return BitmapFactory.decodeStream(is, null, o2); 319 } catch (FileNotFoundException e) { 320 Log.e(LOGTAG, "FileNotFoundException: " + uri); 321 } catch (Exception e) { 322 e.printStackTrace(); 323 } finally { 324 Utils.closeSilently(is); 325 } 326 return null; 327 } 328 329 public Bitmap getBackgroundBitmap(Resources resources) { 330 if (mBackgroundBitmap == null) { 331 mBackgroundBitmap = BitmapFactory.decodeResource(resources, 332 R.drawable.filtershow_background); 333 } 334 return mBackgroundBitmap; 335 336 } 337 338 public Bitmap getOriginalBitmapSmall() { 339 return mOriginalBitmapSmall; 340 } 341 342 public Bitmap getOriginalBitmapLarge() { 343 return mOriginalBitmapLarge; 344 } 345 346 public void addListener(ImageShow imageShow) { 347 mLoadingLock.lock(); 348 if (!mListeners.contains(imageShow)) { 349 mListeners.add(imageShow); 350 } 351 mLoadingLock.unlock(); 352 } 353 354 private void warnListeners() { 355 mActivity.runOnUiThread(mWarnListenersRunnable); 356 } 357 358 private Runnable mWarnListenersRunnable = new Runnable() { 359 360 @Override 361 public void run() { 362 for (int i = 0; i < mListeners.size(); i++) { 363 ImageShow imageShow = mListeners.elementAt(i); 364 imageShow.imageLoaded(); 365 } 366 } 367 }; 368 369 // FIXME: this currently does the loading + filtering on the UI thread -- 370 // need to move this to a background thread. 371 public Bitmap getScaleOneImageForPreset(ImageShow caller, ImagePreset imagePreset, Rect bounds, 372 boolean force) { 373 mLoadingLock.lock(); 374 Bitmap bmp = mZoomCache.getImage(imagePreset, bounds); 375 if (force || bmp == null) { 376 bmp = loadRegionBitmap(mUri, bounds); 377 if (bmp != null) { 378 // TODO: this workaround for RS might not be needed ultimately 379 Bitmap bmp2 = bmp.copy(Bitmap.Config.ARGB_8888, true); 380 float scaleFactor = imagePreset.getScaleFactor(); 381 imagePreset.setScaleFactor(1.0f); 382 bmp2 = imagePreset.apply(bmp2); 383 imagePreset.setScaleFactor(scaleFactor); 384 mZoomCache.setImage(imagePreset, bounds, bmp2); 385 mLoadingLock.unlock(); 386 return bmp2; 387 } 388 } 389 mLoadingLock.unlock(); 390 return bmp; 391 } 392 393 public void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity, 394 File destination) { 395 preset.setQuality(ImagePreset.QUALITY_FINAL); 396 preset.setScaleFactor(1.0f); 397 new SaveCopyTask(mContext, mUri, destination, new SaveCopyTask.Callback() { 398 399 @Override 400 public void onComplete(Uri result) { 401 filterShowActivity.completeSaveImage(result); 402 } 403 404 }).execute(preset); 405 } 406 407 public static Bitmap loadMutableBitmap(Context context, Uri sourceUri) { 408 BitmapFactory.Options options = new BitmapFactory.Options(); 409 // TODO: on <3.x we need a copy of the bitmap (inMutable doesn't 410 // exist) 411 options.inMutable = true; 412 413 InputStream is = null; 414 Bitmap bitmap = null; 415 try { 416 is = context.getContentResolver().openInputStream(sourceUri); 417 bitmap = BitmapFactory.decodeStream(is, null, options); 418 } catch (FileNotFoundException e) { 419 Log.w(LOGTAG, "could not load bitmap ", e); 420 is = null; 421 bitmap = null; 422 } finally { 423 Utils.closeSilently(is); 424 } 425 if (bitmap == null) { 426 return null; 427 } 428 int orientation = ImageLoader.getOrientation(context, sourceUri); 429 bitmap = ImageLoader.rotateToPortrait(bitmap, orientation); 430 return bitmap; 431 } 432 433 public void returnFilteredResult(ImagePreset preset, 434 final FilterShowActivity filterShowActivity) { 435 preset.setQuality(ImagePreset.QUALITY_FINAL); 436 preset.setScaleFactor(1.0f); 437 438 BitmapTask.Callbacks<ImagePreset> cb = new BitmapTask.Callbacks<ImagePreset>() { 439 440 @Override 441 public void onComplete(Bitmap result) { 442 filterShowActivity.onFilteredResult(result); 443 } 444 445 @Override 446 public void onCancel() { 447 } 448 449 @Override 450 public Bitmap onExecute(ImagePreset param) { 451 if (param == null || mUri == null) { 452 return null; 453 } 454 Bitmap bitmap = loadMutableBitmap(mContext, mUri); 455 if (bitmap == null) { 456 Log.w(LOGTAG, "Failed to save image!"); 457 return null; 458 } 459 return param.apply(bitmap); 460 } 461 }; 462 463 (new BitmapTask<ImagePreset>(cb)).execute(preset); 464 } 465 466 private String getFileExtension(String requestFormat) { 467 String outputFormat = (requestFormat == null) 468 ? "jpg" 469 : requestFormat; 470 outputFormat = outputFormat.toLowerCase(); 471 return (outputFormat.equals("png") || outputFormat.equals("gif")) 472 ? "png" // We don't support gif compression. 473 : "jpg"; 474 } 475 476 private CompressFormat convertExtensionToCompressFormat(String extension) { 477 return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG; 478 } 479 480 public void saveToUri(Bitmap bmap, Uri uri, final String outputFormat, 481 final FilterShowActivity filterShowActivity) { 482 483 OutputStream out = null; 484 try { 485 out = filterShowActivity.getContentResolver().openOutputStream(uri); 486 } catch (FileNotFoundException e) { 487 Log.w(LOGTAG, "cannot write output", e); 488 out = null; 489 } finally { 490 if (bmap == null || out == null) { 491 return; 492 } 493 } 494 495 final InterruptableOutputStream ios = new InterruptableOutputStream(out); 496 497 BitmapTask.Callbacks<Bitmap> cb = new BitmapTask.Callbacks<Bitmap>() { 498 499 @Override 500 public void onComplete(Bitmap result) { 501 filterShowActivity.done(); 502 } 503 504 @Override 505 public void onCancel() { 506 ios.interrupt(); 507 } 508 509 @Override 510 public Bitmap onExecute(Bitmap param) { 511 CompressFormat cf = convertExtensionToCompressFormat(getFileExtension(outputFormat)); 512 param.compress(cf, DEFAULT_COMPRESS_QUALITY, ios); 513 Utils.closeSilently(ios); 514 return null; 515 } 516 }; 517 518 (new BitmapTask<Bitmap>(cb)).execute(bmap); 519 } 520 521 public void setAdapter(HistoryAdapter adapter) { 522 mAdapter = adapter; 523 } 524 525 public HistoryAdapter getHistory() { 526 return mAdapter; 527 } 528 529 public XMPMeta getXmpObject() { 530 try { 531 InputStream is = mContext.getContentResolver().openInputStream(getUri()); 532 return XmpUtilHelper.extractXMPMeta(is); 533 } catch (FileNotFoundException e) { 534 return null; 535 } 536 } 537 538 /** 539 * Determine if this is a light cycle 360 image 540 * 541 * @return true if it is a light Cycle image that is full 360 542 */ 543 public boolean queryLightCycle360() { 544 InputStream is = null; 545 try { 546 is = mContext.getContentResolver().openInputStream(getUri()); 547 XMPMeta meta = XmpUtilHelper.extractXMPMeta(is); 548 if (meta == null) { 549 return false; 550 } 551 String name = meta.getPacketHeader(); 552 String namespace = "http://ns.google.com/photos/1.0/panorama/"; 553 String cropWidthName = "GPano:CroppedAreaImageWidthPixels"; 554 String fullWidthName = "GPano:FullPanoWidthPixels"; 555 556 if (!meta.doesPropertyExist(namespace, cropWidthName)) { 557 return false; 558 } 559 if (!meta.doesPropertyExist(namespace, fullWidthName)) { 560 return false; 561 } 562 563 Integer cropValue = meta.getPropertyInteger(namespace, cropWidthName); 564 Integer fullValue = meta.getPropertyInteger(namespace, fullWidthName); 565 566 // Definition of a 360: 567 // GFullPanoWidthPixels == CroppedAreaImageWidthPixels 568 if (cropValue != null && fullValue != null) { 569 return cropValue.equals(fullValue); 570 } 571 572 return false; 573 } catch (FileNotFoundException e) { 574 return false; 575 } catch (XMPException e) { 576 return false; 577 } finally { 578 Utils.closeSilently(is); 579 } 580 } 581} 582