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