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.BitmapFactory; 24import android.graphics.BitmapRegionDecoder; 25import android.graphics.Canvas; 26import android.graphics.Paint; 27import android.graphics.Rect; 28import android.net.Uri; 29import android.opengl.GLUtils; 30import android.os.Build; 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( 57 String pathName, boolean isShareable) { 58 try { 59 BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(pathName, isShareable); 60 if (d != null) { 61 return new SimpleBitmapRegionDecoderWrapper(d); 62 } 63 } catch (IOException e) { 64 Log.w("BitmapRegionTileSource", "getting decoder failed for path " + pathName, e); 65 return null; 66 } 67 return null; 68 } 69 public static SimpleBitmapRegionDecoderWrapper newInstance( 70 InputStream is, boolean isShareable) { 71 try { 72 BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(is, isShareable); 73 if (d != null) { 74 return new SimpleBitmapRegionDecoderWrapper(d); 75 } 76 } catch (IOException e) { 77 Log.w("BitmapRegionTileSource", "getting decoder failed", e); 78 return null; 79 } 80 return null; 81 } 82 public int getWidth() { 83 return mDecoder.getWidth(); 84 } 85 public int getHeight() { 86 return mDecoder.getHeight(); 87 } 88 public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) { 89 return mDecoder.decodeRegion(wantRegion, options); 90 } 91} 92 93class DumbBitmapRegionDecoder implements SimpleBitmapRegionDecoder { 94 Bitmap mBuffer; 95 Canvas mTempCanvas; 96 Paint mTempPaint; 97 private DumbBitmapRegionDecoder(Bitmap b) { 98 mBuffer = b; 99 } 100 public static DumbBitmapRegionDecoder newInstance(String pathName) { 101 Bitmap b = BitmapFactory.decodeFile(pathName); 102 if (b != null) { 103 return new DumbBitmapRegionDecoder(b); 104 } 105 return null; 106 } 107 public static DumbBitmapRegionDecoder newInstance(InputStream is) { 108 Bitmap b = BitmapFactory.decodeStream(is); 109 if (b != null) { 110 return new DumbBitmapRegionDecoder(b); 111 } 112 return null; 113 } 114 public int getWidth() { 115 return mBuffer.getWidth(); 116 } 117 public int getHeight() { 118 return mBuffer.getHeight(); 119 } 120 public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) { 121 if (mTempCanvas == null) { 122 mTempCanvas = new Canvas(); 123 mTempPaint = new Paint(); 124 mTempPaint.setFilterBitmap(true); 125 } 126 int sampleSize = Math.max(options.inSampleSize, 1); 127 Bitmap newBitmap = Bitmap.createBitmap( 128 wantRegion.width() / sampleSize, 129 wantRegion.height() / sampleSize, 130 Bitmap.Config.ARGB_8888); 131 mTempCanvas.setBitmap(newBitmap); 132 mTempCanvas.save(); 133 mTempCanvas.scale(1f / sampleSize, 1f / sampleSize); 134 mTempCanvas.drawBitmap(mBuffer, -wantRegion.left, -wantRegion.top, mTempPaint); 135 mTempCanvas.restore(); 136 mTempCanvas.setBitmap(null); 137 return newBitmap; 138 } 139} 140 141/** 142 * A {@link com.android.photos.views.TiledImageRenderer.TileSource} using 143 * {@link BitmapRegionDecoder} to wrap a local file 144 */ 145@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) 146public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { 147 148 private static final String TAG = "BitmapRegionTileSource"; 149 150 private static final int GL_SIZE_LIMIT = 2048; 151 // This must be no larger than half the size of the GL_SIZE_LIMIT 152 // due to decodePreview being allowed to be up to 2x the size of the target 153 private static final int MAX_PREVIEW_SIZE = GL_SIZE_LIMIT / 2; 154 155 public static abstract class BitmapSource { 156 private SimpleBitmapRegionDecoder mDecoder; 157 private Bitmap mPreview; 158 private int mRotation; 159 public enum State { NOT_LOADED, LOADED, ERROR_LOADING }; 160 private State mState = State.NOT_LOADED; 161 162 public boolean loadInBackground(InBitmapProvider bitmapProvider) { 163 ExifInterface ei = new ExifInterface(); 164 if (readExif(ei)) { 165 Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION); 166 if (ori != null) { 167 mRotation = ExifInterface.getRotationForOrientationValue(ori.shortValue()); 168 } 169 } 170 mDecoder = loadBitmapRegionDecoder(); 171 if (mDecoder == null) { 172 mState = State.ERROR_LOADING; 173 return false; 174 } else { 175 int width = mDecoder.getWidth(); 176 int height = mDecoder.getHeight(); 177 178 BitmapFactory.Options opts = new BitmapFactory.Options(); 179 opts.inPreferredConfig = Bitmap.Config.ARGB_8888; 180 opts.inPreferQualityOverSpeed = true; 181 182 float scale = (float) MAX_PREVIEW_SIZE / Math.max(width, height); 183 opts.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale); 184 opts.inJustDecodeBounds = false; 185 opts.inMutable = true; 186 187 if (bitmapProvider != null) { 188 int expectedPixles = (width / opts.inSampleSize) * (height / opts.inSampleSize); 189 Bitmap reusableBitmap = bitmapProvider.forPixelCount(expectedPixles); 190 if (reusableBitmap != null) { 191 // Try loading with reusable bitmap 192 opts.inBitmap = reusableBitmap; 193 try { 194 mPreview = loadPreviewBitmap(opts); 195 } catch (IllegalArgumentException e) { 196 Log.d(TAG, "Unable to reusage bitmap", e); 197 opts.inBitmap = null; 198 mPreview = null; 199 } 200 } 201 } 202 if (mPreview == null) { 203 mPreview = loadPreviewBitmap(opts); 204 } 205 206 // Verify that the bitmap can be used on GL surface 207 try { 208 GLUtils.getInternalFormat(mPreview); 209 GLUtils.getType(mPreview); 210 mState = State.LOADED; 211 } catch (IllegalArgumentException e) { 212 Log.d(TAG, "Image cannot be rendered on a GL surface", e); 213 mState = State.ERROR_LOADING; 214 } 215 return true; 216 } 217 } 218 219 public State getLoadingState() { 220 return mState; 221 } 222 223 public SimpleBitmapRegionDecoder getBitmapRegionDecoder() { 224 return mDecoder; 225 } 226 227 public Bitmap getPreviewBitmap() { 228 return mPreview; 229 } 230 231 public int getRotation() { 232 return mRotation; 233 } 234 235 public abstract boolean readExif(ExifInterface ei); 236 public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder(); 237 public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options); 238 239 public interface InBitmapProvider { 240 Bitmap forPixelCount(int count); 241 } 242 } 243 244 public static class FilePathBitmapSource extends BitmapSource { 245 private String mPath; 246 public FilePathBitmapSource(String path) { 247 mPath = path; 248 } 249 @Override 250 public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() { 251 SimpleBitmapRegionDecoder d; 252 d = SimpleBitmapRegionDecoderWrapper.newInstance(mPath, true); 253 if (d == null) { 254 d = DumbBitmapRegionDecoder.newInstance(mPath); 255 } 256 return d; 257 } 258 @Override 259 public Bitmap loadPreviewBitmap(BitmapFactory.Options options) { 260 return BitmapFactory.decodeFile(mPath, options); 261 } 262 @Override 263 public boolean readExif(ExifInterface ei) { 264 try { 265 ei.readExif(mPath); 266 return true; 267 } catch (NullPointerException e) { 268 Log.w("BitmapRegionTileSource", "reading exif failed", e); 269 return false; 270 } catch (IOException e) { 271 Log.w("BitmapRegionTileSource", "getting decoder failed", e); 272 return false; 273 } 274 } 275 } 276 277 public static class UriBitmapSource extends BitmapSource { 278 private Context mContext; 279 private Uri mUri; 280 public UriBitmapSource(Context context, Uri uri) { 281 mContext = context; 282 mUri = uri; 283 } 284 private InputStream regenerateInputStream() throws FileNotFoundException { 285 InputStream is = mContext.getContentResolver().openInputStream(mUri); 286 return new BufferedInputStream(is); 287 } 288 @Override 289 public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() { 290 try { 291 InputStream is = regenerateInputStream(); 292 SimpleBitmapRegionDecoder regionDecoder = 293 SimpleBitmapRegionDecoderWrapper.newInstance(is, false); 294 Utils.closeSilently(is); 295 if (regionDecoder == null) { 296 is = regenerateInputStream(); 297 regionDecoder = DumbBitmapRegionDecoder.newInstance(is); 298 Utils.closeSilently(is); 299 } 300 return regionDecoder; 301 } catch (FileNotFoundException e) { 302 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e); 303 return null; 304 } 305 } 306 @Override 307 public Bitmap loadPreviewBitmap(BitmapFactory.Options options) { 308 try { 309 InputStream is = regenerateInputStream(); 310 Bitmap b = BitmapFactory.decodeStream(is, null, options); 311 Utils.closeSilently(is); 312 return b; 313 } catch (FileNotFoundException e) { 314 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e); 315 return null; 316 } 317 } 318 @Override 319 public boolean readExif(ExifInterface ei) { 320 InputStream is = null; 321 try { 322 is = regenerateInputStream(); 323 ei.readExif(is); 324 Utils.closeSilently(is); 325 return true; 326 } catch (FileNotFoundException e) { 327 Log.d("BitmapRegionTileSource", "Failed to load URI " + mUri, e); 328 return false; 329 } catch (IOException e) { 330 Log.d("BitmapRegionTileSource", "Failed to load URI " + mUri, e); 331 return false; 332 } catch (NullPointerException e) { 333 Log.d("BitmapRegionTileSource", "Failed to read EXIF for URI " + mUri, e); 334 return false; 335 } finally { 336 Utils.closeSilently(is); 337 } 338 } 339 } 340 341 public static class ResourceBitmapSource extends BitmapSource { 342 private Resources mRes; 343 private int mResId; 344 public ResourceBitmapSource(Resources res, int resId) { 345 mRes = res; 346 mResId = resId; 347 } 348 private InputStream regenerateInputStream() { 349 InputStream is = mRes.openRawResource(mResId); 350 return new BufferedInputStream(is); 351 } 352 @Override 353 public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() { 354 InputStream is = regenerateInputStream(); 355 SimpleBitmapRegionDecoder regionDecoder = 356 SimpleBitmapRegionDecoderWrapper.newInstance(is, false); 357 Utils.closeSilently(is); 358 if (regionDecoder == null) { 359 is = regenerateInputStream(); 360 regionDecoder = DumbBitmapRegionDecoder.newInstance(is); 361 Utils.closeSilently(is); 362 } 363 return regionDecoder; 364 } 365 @Override 366 public Bitmap loadPreviewBitmap(BitmapFactory.Options options) { 367 return BitmapFactory.decodeResource(mRes, mResId, options); 368 } 369 @Override 370 public boolean readExif(ExifInterface ei) { 371 try { 372 InputStream is = regenerateInputStream(); 373 ei.readExif(is); 374 Utils.closeSilently(is); 375 return true; 376 } catch (IOException e) { 377 Log.e("BitmapRegionTileSource", "Error reading resource", e); 378 return false; 379 } 380 } 381 } 382 383 SimpleBitmapRegionDecoder mDecoder; 384 int mWidth; 385 int mHeight; 386 int mTileSize; 387 private BasicTexture mPreview; 388 private final int mRotation; 389 390 // For use only by getTile 391 private Rect mWantRegion = new Rect(); 392 private BitmapFactory.Options mOptions; 393 394 public BitmapRegionTileSource(Context context, BitmapSource source, byte[] tempStorage) { 395 mTileSize = TiledImageRenderer.suggestedTileSize(context); 396 mRotation = source.getRotation(); 397 mDecoder = source.getBitmapRegionDecoder(); 398 if (mDecoder != null) { 399 mWidth = mDecoder.getWidth(); 400 mHeight = mDecoder.getHeight(); 401 mOptions = new BitmapFactory.Options(); 402 mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; 403 mOptions.inPreferQualityOverSpeed = true; 404 mOptions.inTempStorage = tempStorage; 405 406 Bitmap preview = source.getPreviewBitmap(); 407 if (preview != null && 408 preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) { 409 mPreview = new BitmapTexture(preview); 410 } else { 411 Log.w(TAG, String.format( 412 "Failed to create preview of apropriate size! " 413 + " in: %dx%d, out: %dx%d", 414 mWidth, mHeight, 415 preview.getWidth(), preview.getHeight())); 416 } 417 } 418 } 419 420 public Bitmap getBitmap() { 421 return mPreview instanceof BitmapTexture ? ((BitmapTexture) mPreview).getBitmap() : null; 422 } 423 424 @Override 425 public int getTileSize() { 426 return mTileSize; 427 } 428 429 @Override 430 public int getImageWidth() { 431 return mWidth; 432 } 433 434 @Override 435 public int getImageHeight() { 436 return mHeight; 437 } 438 439 @Override 440 public BasicTexture getPreview() { 441 return mPreview; 442 } 443 444 @Override 445 public int getRotation() { 446 return mRotation; 447 } 448 449 @Override 450 public Bitmap getTile(int level, int x, int y, Bitmap bitmap) { 451 int tileSize = getTileSize(); 452 int t = tileSize << level; 453 mWantRegion.set(x, y, x + t, y + t); 454 455 if (bitmap == null) { 456 bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888); 457 } 458 459 mOptions.inSampleSize = (1 << level); 460 mOptions.inBitmap = bitmap; 461 462 try { 463 bitmap = mDecoder.decodeRegion(mWantRegion, mOptions); 464 } finally { 465 if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) { 466 mOptions.inBitmap = null; 467 } 468 } 469 470 if (bitmap == null) { 471 Log.w("BitmapRegionTileSource", "fail in decoding region"); 472 } 473 return bitmap; 474 } 475} 476