BitmapRegionTileSource.java revision 6090995951c6e2e4dcf38102f01793f8a94166e1
1/* 2 * Copyright (C) 2013 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.photos; 18 19import android.annotation.TargetApi; 20import android.content.Context; 21import android.content.res.Resources; 22import android.graphics.Bitmap; 23import android.graphics.Bitmap.Config; 24import android.graphics.BitmapFactory; 25import android.graphics.BitmapRegionDecoder; 26import android.graphics.Canvas; 27import android.graphics.Paint; 28import android.graphics.Rect; 29import android.net.Uri; 30import android.os.Build; 31import android.os.Build.VERSION_CODES; 32import android.util.Log; 33 34import com.android.gallery3d.common.BitmapUtils; 35import com.android.gallery3d.common.Utils; 36import com.android.gallery3d.exif.ExifInterface; 37import com.android.gallery3d.glrenderer.BasicTexture; 38import com.android.gallery3d.glrenderer.BitmapTexture; 39import com.android.photos.views.TiledImageRenderer; 40 41import java.io.BufferedInputStream; 42import java.io.FileNotFoundException; 43import java.io.IOException; 44import java.io.InputStream; 45 46interface SimpleBitmapRegionDecoder { 47 int getWidth(); 48 int getHeight(); 49 Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options); 50} 51 52class SimpleBitmapRegionDecoderWrapper implements SimpleBitmapRegionDecoder { 53 BitmapRegionDecoder mDecoder; 54 private SimpleBitmapRegionDecoderWrapper(BitmapRegionDecoder decoder) { 55 mDecoder = decoder; 56 } 57 public static SimpleBitmapRegionDecoderWrapper newInstance( 58 String pathName, boolean isShareable) { 59 try { 60 BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(pathName, isShareable); 61 if (d != null) { 62 return new SimpleBitmapRegionDecoderWrapper(d); 63 } 64 } catch (IOException e) { 65 Log.w("BitmapRegionTileSource", "getting decoder failed for path " + pathName, e); 66 return null; 67 } 68 return null; 69 } 70 public static SimpleBitmapRegionDecoderWrapper newInstance( 71 InputStream is, boolean isShareable) { 72 try { 73 BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(is, isShareable); 74 if (d != null) { 75 return new SimpleBitmapRegionDecoderWrapper(d); 76 } 77 } catch (IOException e) { 78 Log.w("BitmapRegionTileSource", "getting decoder failed", e); 79 return null; 80 } 81 return null; 82 } 83 public int getWidth() { 84 return mDecoder.getWidth(); 85 } 86 public int getHeight() { 87 return mDecoder.getHeight(); 88 } 89 public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) { 90 return mDecoder.decodeRegion(wantRegion, options); 91 } 92} 93 94class DumbBitmapRegionDecoder implements SimpleBitmapRegionDecoder { 95 Bitmap mBuffer; 96 Canvas mTempCanvas; 97 Paint mTempPaint; 98 private DumbBitmapRegionDecoder(Bitmap b) { 99 mBuffer = b; 100 } 101 public static DumbBitmapRegionDecoder newInstance(String pathName) { 102 Bitmap b = BitmapFactory.decodeFile(pathName); 103 if (b != null) { 104 return new DumbBitmapRegionDecoder(b); 105 } 106 return null; 107 } 108 public static DumbBitmapRegionDecoder newInstance(InputStream is) { 109 Bitmap b = BitmapFactory.decodeStream(is); 110 if (b != null) { 111 return new DumbBitmapRegionDecoder(b); 112 } 113 return null; 114 } 115 public int getWidth() { 116 return mBuffer.getWidth(); 117 } 118 public int getHeight() { 119 return mBuffer.getHeight(); 120 } 121 public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) { 122 if (mTempCanvas == null) { 123 mTempCanvas = new Canvas(); 124 mTempPaint = new Paint(); 125 mTempPaint.setFilterBitmap(true); 126 } 127 int sampleSize = Math.max(options.inSampleSize, 1); 128 Bitmap newBitmap = Bitmap.createBitmap( 129 wantRegion.width() / sampleSize, 130 wantRegion.height() / sampleSize, 131 Bitmap.Config.ARGB_8888); 132 mTempCanvas.setBitmap(newBitmap); 133 mTempCanvas.save(); 134 mTempCanvas.scale(1f / sampleSize, 1f / sampleSize); 135 mTempCanvas.drawBitmap(mBuffer, -wantRegion.left, -wantRegion.top, mTempPaint); 136 mTempCanvas.restore(); 137 mTempCanvas.setBitmap(null); 138 return newBitmap; 139 } 140} 141 142/** 143 * A {@link com.android.photos.views.TiledImageRenderer.TileSource} using 144 * {@link BitmapRegionDecoder} to wrap a local file 145 */ 146@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) 147public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { 148 149 private static final String TAG = "BitmapRegionTileSource"; 150 151 private static final boolean REUSE_BITMAP = 152 Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; 153 private static final int GL_SIZE_LIMIT = 2048; 154 // This must be no larger than half the size of the GL_SIZE_LIMIT 155 // due to decodePreview being allowed to be up to 2x the size of the target 156 public static final int MAX_PREVIEW_SIZE = GL_SIZE_LIMIT / 2; 157 158 public static abstract class BitmapSource { 159 private SimpleBitmapRegionDecoder mDecoder; 160 private Bitmap mPreview; 161 private int mPreviewSize; 162 private int mRotation; 163 public enum State { NOT_LOADED, LOADED, ERROR_LOADING }; 164 private State mState = State.NOT_LOADED; 165 public BitmapSource(int previewSize) { 166 mPreviewSize = previewSize; 167 } 168 public boolean loadInBackground() { 169 ExifInterface ei = new ExifInterface(); 170 if (readExif(ei)) { 171 Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION); 172 if (ori != null) { 173 mRotation = ExifInterface.getRotationForOrientationValue(ori.shortValue()); 174 } 175 } 176 mDecoder = loadBitmapRegionDecoder(); 177 if (mDecoder == null) { 178 mState = State.ERROR_LOADING; 179 return false; 180 } else { 181 int width = mDecoder.getWidth(); 182 int height = mDecoder.getHeight(); 183 if (mPreviewSize != 0) { 184 int previewSize = Math.min(mPreviewSize, MAX_PREVIEW_SIZE); 185 BitmapFactory.Options opts = new BitmapFactory.Options(); 186 opts.inPreferredConfig = Bitmap.Config.ARGB_8888; 187 opts.inPreferQualityOverSpeed = true; 188 189 float scale = (float) previewSize / Math.max(width, height); 190 opts.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale); 191 opts.inJustDecodeBounds = false; 192 mPreview = loadPreviewBitmap(opts); 193 } 194 mState = State.LOADED; 195 return true; 196 } 197 } 198 199 public State getLoadingState() { 200 return mState; 201 } 202 203 public SimpleBitmapRegionDecoder getBitmapRegionDecoder() { 204 return mDecoder; 205 } 206 207 public Bitmap getPreviewBitmap() { 208 return mPreview; 209 } 210 211 public int getPreviewSize() { 212 return mPreviewSize; 213 } 214 215 public int getRotation() { 216 return mRotation; 217 } 218 219 public abstract boolean readExif(ExifInterface ei); 220 public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder(); 221 public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options); 222 } 223 224 public static class FilePathBitmapSource extends BitmapSource { 225 private String mPath; 226 public FilePathBitmapSource(String path, int previewSize) { 227 super(previewSize); 228 mPath = path; 229 } 230 @Override 231 public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() { 232 SimpleBitmapRegionDecoder d; 233 d = SimpleBitmapRegionDecoderWrapper.newInstance(mPath, true); 234 if (d == null) { 235 d = DumbBitmapRegionDecoder.newInstance(mPath); 236 } 237 return d; 238 } 239 @Override 240 public Bitmap loadPreviewBitmap(BitmapFactory.Options options) { 241 return BitmapFactory.decodeFile(mPath, options); 242 } 243 @Override 244 public boolean readExif(ExifInterface ei) { 245 try { 246 ei.readExif(mPath); 247 return true; 248 } catch (IOException e) { 249 Log.w("BitmapRegionTileSource", "getting decoder failed", e); 250 return false; 251 } 252 } 253 } 254 255 public static class UriBitmapSource extends BitmapSource { 256 private Context mContext; 257 private Uri mUri; 258 public UriBitmapSource(Context context, Uri uri, int previewSize) { 259 super(previewSize); 260 mContext = context; 261 mUri = uri; 262 } 263 private InputStream regenerateInputStream() throws FileNotFoundException { 264 InputStream is = mContext.getContentResolver().openInputStream(mUri); 265 return new BufferedInputStream(is); 266 } 267 @Override 268 public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() { 269 try { 270 InputStream is = regenerateInputStream(); 271 SimpleBitmapRegionDecoder regionDecoder = 272 SimpleBitmapRegionDecoderWrapper.newInstance(is, false); 273 Utils.closeSilently(is); 274 if (regionDecoder == null) { 275 is = regenerateInputStream(); 276 regionDecoder = DumbBitmapRegionDecoder.newInstance(is); 277 Utils.closeSilently(is); 278 } 279 return regionDecoder; 280 } catch (FileNotFoundException e) { 281 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e); 282 return null; 283 } catch (IOException e) { 284 Log.e("BitmapRegionTileSource", "Failure while reading URI " + mUri, e); 285 return null; 286 } 287 } 288 @Override 289 public Bitmap loadPreviewBitmap(BitmapFactory.Options options) { 290 try { 291 InputStream is = regenerateInputStream(); 292 Bitmap b = BitmapFactory.decodeStream(is, null, options); 293 Utils.closeSilently(is); 294 return b; 295 } catch (FileNotFoundException e) { 296 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e); 297 return null; 298 } 299 } 300 @Override 301 public boolean readExif(ExifInterface ei) { 302 InputStream is = null; 303 try { 304 is = regenerateInputStream(); 305 ei.readExif(is); 306 Utils.closeSilently(is); 307 return true; 308 } catch (FileNotFoundException e) { 309 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e); 310 return false; 311 } catch (IOException e) { 312 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e); 313 return false; 314 } finally { 315 Utils.closeSilently(is); 316 } 317 } 318 } 319 320 public static class ResourceBitmapSource extends BitmapSource { 321 private Resources mRes; 322 private int mResId; 323 public ResourceBitmapSource(Resources res, int resId, int previewSize) { 324 super(previewSize); 325 mRes = res; 326 mResId = resId; 327 } 328 private InputStream regenerateInputStream() { 329 InputStream is = mRes.openRawResource(mResId); 330 return new BufferedInputStream(is); 331 } 332 @Override 333 public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() { 334 InputStream is = regenerateInputStream(); 335 SimpleBitmapRegionDecoder regionDecoder = 336 SimpleBitmapRegionDecoderWrapper.newInstance(is, false); 337 Utils.closeSilently(is); 338 if (regionDecoder == null) { 339 is = regenerateInputStream(); 340 regionDecoder = DumbBitmapRegionDecoder.newInstance(is); 341 Utils.closeSilently(is); 342 } 343 return regionDecoder; 344 } 345 @Override 346 public Bitmap loadPreviewBitmap(BitmapFactory.Options options) { 347 return BitmapFactory.decodeResource(mRes, mResId, options); 348 } 349 @Override 350 public boolean readExif(ExifInterface ei) { 351 try { 352 InputStream is = regenerateInputStream(); 353 ei.readExif(is); 354 Utils.closeSilently(is); 355 return true; 356 } catch (IOException e) { 357 Log.e("BitmapRegionTileSource", "Error reading resource", e); 358 return false; 359 } 360 } 361 } 362 363 SimpleBitmapRegionDecoder mDecoder; 364 int mWidth; 365 int mHeight; 366 int mTileSize; 367 private BasicTexture mPreview; 368 private final int mRotation; 369 370 // For use only by getTile 371 private Rect mWantRegion = new Rect(); 372 private Rect mOverlapRegion = new Rect(); 373 private BitmapFactory.Options mOptions; 374 private Canvas mCanvas; 375 376 public BitmapRegionTileSource(Context context, BitmapSource source) { 377 mTileSize = TiledImageRenderer.suggestedTileSize(context); 378 mRotation = source.getRotation(); 379 mDecoder = source.getBitmapRegionDecoder(); 380 if (mDecoder != null) { 381 mWidth = mDecoder.getWidth(); 382 mHeight = mDecoder.getHeight(); 383 mOptions = new BitmapFactory.Options(); 384 mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; 385 mOptions.inPreferQualityOverSpeed = true; 386 mOptions.inTempStorage = new byte[16 * 1024]; 387 int previewSize = source.getPreviewSize(); 388 if (previewSize != 0) { 389 previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE); 390 // Although this is the same size as the Bitmap that is likely already 391 // loaded, the lifecycle is different and interactions are on a different 392 // thread. Thus to simplify, this source will decode its own bitmap. 393 Bitmap preview = decodePreview(source, previewSize); 394 if (preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) { 395 mPreview = new BitmapTexture(preview); 396 } else { 397 Log.w(TAG, String.format( 398 "Failed to create preview of apropriate size! " 399 + " in: %dx%d, out: %dx%d", 400 mWidth, mHeight, 401 preview.getWidth(), preview.getHeight())); 402 } 403 } 404 } 405 } 406 407 @Override 408 public int getTileSize() { 409 return mTileSize; 410 } 411 412 @Override 413 public int getImageWidth() { 414 return mWidth; 415 } 416 417 @Override 418 public int getImageHeight() { 419 return mHeight; 420 } 421 422 @Override 423 public BasicTexture getPreview() { 424 return mPreview; 425 } 426 427 @Override 428 public int getRotation() { 429 return mRotation; 430 } 431 432 @Override 433 public Bitmap getTile(int level, int x, int y, Bitmap bitmap) { 434 int tileSize = getTileSize(); 435 if (!REUSE_BITMAP) { 436 return getTileWithoutReusingBitmap(level, x, y, tileSize); 437 } 438 439 int t = tileSize << level; 440 mWantRegion.set(x, y, x + t, y + t); 441 442 if (bitmap == null) { 443 bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888); 444 } 445 446 mOptions.inSampleSize = (1 << level); 447 mOptions.inBitmap = bitmap; 448 449 try { 450 bitmap = mDecoder.decodeRegion(mWantRegion, mOptions); 451 } finally { 452 if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) { 453 mOptions.inBitmap = null; 454 } 455 } 456 457 if (bitmap == null) { 458 Log.w("BitmapRegionTileSource", "fail in decoding region"); 459 } 460 return bitmap; 461 } 462 463 private Bitmap getTileWithoutReusingBitmap( 464 int level, int x, int y, int tileSize) { 465 466 int t = tileSize << level; 467 mWantRegion.set(x, y, x + t, y + t); 468 469 mOverlapRegion.set(0, 0, mWidth, mHeight); 470 471 mOptions.inSampleSize = (1 << level); 472 Bitmap bitmap = mDecoder.decodeRegion(mOverlapRegion, mOptions); 473 474 if (bitmap == null) { 475 Log.w(TAG, "fail in decoding region"); 476 } 477 478 if (mWantRegion.equals(mOverlapRegion)) { 479 return bitmap; 480 } 481 482 Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888); 483 if (mCanvas == null) { 484 mCanvas = new Canvas(); 485 } 486 mCanvas.setBitmap(result); 487 mCanvas.drawBitmap(bitmap, 488 (mOverlapRegion.left - mWantRegion.left) >> level, 489 (mOverlapRegion.top - mWantRegion.top) >> level, null); 490 mCanvas.setBitmap(null); 491 return result; 492 } 493 494 /** 495 * Note that the returned bitmap may have a long edge that's longer 496 * than the targetSize, but it will always be less than 2x the targetSize 497 */ 498 private Bitmap decodePreview(BitmapSource source, int targetSize) { 499 Bitmap result = source.getPreviewBitmap(); 500 if (result == null) { 501 return null; 502 } 503 504 // We need to resize down if the decoder does not support inSampleSize 505 // or didn't support the specified inSampleSize (some decoders only do powers of 2) 506 float scale = (float) targetSize / (float) (Math.max(result.getWidth(), result.getHeight())); 507 508 if (scale <= 0.5) { 509 result = BitmapUtils.resizeBitmapByScale(result, scale, true); 510 } 511 return ensureGLCompatibleBitmap(result); 512 } 513 514 private static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) { 515 if (bitmap == null || bitmap.getConfig() != null) { 516 return bitmap; 517 } 518 Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false); 519 bitmap.recycle(); 520 return newBitmap; 521 } 522} 523