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