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 17 package com.android.photos; 18 19 import android.annotation.TargetApi; 20 import android.content.Context; 21 import android.content.res.Resources; 22 import android.graphics.Bitmap; 23 import android.graphics.BitmapFactory; 24 import android.graphics.BitmapRegionDecoder; 25 import android.graphics.Canvas; 26 import android.graphics.Paint; 27 import android.graphics.Rect; 28 import android.net.Uri; 29 import android.opengl.GLUtils; 30 import android.os.Build; 31 import android.util.Log; 32 33 import com.android.gallery3d.common.BitmapUtils; 34 import com.android.gallery3d.common.Utils; 35 import com.android.gallery3d.exif.ExifInterface; 36 import com.android.gallery3d.glrenderer.BasicTexture; 37 import com.android.gallery3d.glrenderer.BitmapTexture; 38 import com.android.photos.views.TiledImageRenderer; 39 40 import java.io.BufferedInputStream; 41 import java.io.FileNotFoundException; 42 import java.io.IOException; 43 import java.io.InputStream; 44 45 interface SimpleBitmapRegionDecoder { 46 int getWidth(); 47 int getHeight(); 48 Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options); 49 } 50 51 class 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 93 class 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) 146 public 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 /** Returns whether loading was successful. */ 163 public boolean loadInBackground(InBitmapProvider bitmapProvider) { 164 ExifInterface ei = new ExifInterface(); 165 if (readExif(ei)) { 166 Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION); 167 if (ori != null) { 168 mRotation = ExifInterface.getRotationForOrientationValue(ori.shortValue()); 169 } 170 } 171 mDecoder = loadBitmapRegionDecoder(); 172 if (mDecoder == null) { 173 mState = State.ERROR_LOADING; 174 return false; 175 } else { 176 int width = mDecoder.getWidth(); 177 int height = mDecoder.getHeight(); 178 179 BitmapFactory.Options opts = new BitmapFactory.Options(); 180 opts.inPreferredConfig = Bitmap.Config.ARGB_8888; 181 opts.inPreferQualityOverSpeed = true; 182 183 float scale = (float) MAX_PREVIEW_SIZE / Math.max(width, height); 184 opts.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale); 185 opts.inJustDecodeBounds = false; 186 opts.inMutable = true; 187 188 if (bitmapProvider != null) { 189 int expectedPixles = (width / opts.inSampleSize) * (height / opts.inSampleSize); 190 Bitmap reusableBitmap = bitmapProvider.forPixelCount(expectedPixles); 191 if (reusableBitmap != null) { 192 // Try loading with reusable bitmap 193 opts.inBitmap = reusableBitmap; 194 try { 195 mPreview = loadPreviewBitmap(opts); 196 } catch (IllegalArgumentException e) { 197 Log.d(TAG, "Unable to reuse bitmap", e); 198 opts.inBitmap = null; 199 mPreview = null; 200 } 201 } 202 } 203 if (mPreview == null) { 204 mPreview = loadPreviewBitmap(opts); 205 } 206 if (mPreview == null) { 207 mState = State.ERROR_LOADING; 208 return false; 209 } 210 211 // Verify that the bitmap can be used on GL surface 212 try { 213 GLUtils.getInternalFormat(mPreview); 214 GLUtils.getType(mPreview); 215 mState = State.LOADED; 216 } catch (IllegalArgumentException e) { 217 Log.d(TAG, "Image cannot be rendered on a GL surface", e); 218 mState = State.ERROR_LOADING; 219 } 220 return mState == State.LOADED; 221 } 222 } 223 224 public State getLoadingState() { 225 return mState; 226 } 227 228 public SimpleBitmapRegionDecoder getBitmapRegionDecoder() { 229 return mDecoder; 230 } 231 232 public Bitmap getPreviewBitmap() { 233 return mPreview; 234 } 235 236 public int getRotation() { 237 return mRotation; 238 } 239 240 public abstract boolean readExif(ExifInterface ei); 241 public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder(); 242 public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options); 243 244 public interface InBitmapProvider { 245 Bitmap forPixelCount(int count); 246 } 247 } 248 249 public static class FilePathBitmapSource extends BitmapSource { 250 private String mPath; 251 public FilePathBitmapSource(String path) { 252 mPath = path; 253 } 254 @Override 255 public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() { 256 SimpleBitmapRegionDecoder d; 257 d = SimpleBitmapRegionDecoderWrapper.newInstance(mPath, true); 258 if (d == null) { 259 d = DumbBitmapRegionDecoder.newInstance(mPath); 260 } 261 return d; 262 } 263 @Override 264 public Bitmap loadPreviewBitmap(BitmapFactory.Options options) { 265 return BitmapFactory.decodeFile(mPath, options); 266 } 267 @Override 268 public boolean readExif(ExifInterface ei) { 269 try { 270 ei.readExif(mPath); 271 return true; 272 } catch (NullPointerException e) { 273 Log.w("BitmapRegionTileSource", "reading exif failed", e); 274 return false; 275 } catch (IOException e) { 276 Log.w("BitmapRegionTileSource", "getting decoder failed", e); 277 return false; 278 } 279 } 280 } 281 282 public static class UriBitmapSource extends BitmapSource { 283 private Context mContext; 284 private Uri mUri; 285 public UriBitmapSource(Context context, Uri uri) { 286 mContext = context; 287 mUri = uri; 288 } 289 private InputStream regenerateInputStream() throws FileNotFoundException { 290 InputStream is = mContext.getContentResolver().openInputStream(mUri); 291 return new BufferedInputStream(is); 292 } 293 @Override 294 public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() { 295 try { 296 InputStream is = regenerateInputStream(); 297 SimpleBitmapRegionDecoder regionDecoder = 298 SimpleBitmapRegionDecoderWrapper.newInstance(is, false); 299 Utils.closeSilently(is); 300 if (regionDecoder == null) { 301 is = regenerateInputStream(); 302 regionDecoder = DumbBitmapRegionDecoder.newInstance(is); 303 Utils.closeSilently(is); 304 } 305 return regionDecoder; 306 } catch (FileNotFoundException e) { 307 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e); 308 return null; 309 } 310 } 311 @Override 312 public Bitmap loadPreviewBitmap(BitmapFactory.Options options) { 313 try { 314 InputStream is = regenerateInputStream(); 315 Bitmap b = BitmapFactory.decodeStream(is, null, options); 316 Utils.closeSilently(is); 317 return b; 318 } catch (FileNotFoundException | OutOfMemoryError e) { 319 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e); 320 return null; 321 } 322 } 323 @Override 324 public boolean readExif(ExifInterface ei) { 325 InputStream is = null; 326 try { 327 is = regenerateInputStream(); 328 ei.readExif(is); 329 Utils.closeSilently(is); 330 return true; 331 } catch (FileNotFoundException e) { 332 Log.d("BitmapRegionTileSource", "Failed to load URI " + mUri, e); 333 return false; 334 } catch (IOException e) { 335 Log.d("BitmapRegionTileSource", "Failed to load URI " + mUri, e); 336 return false; 337 } catch (NullPointerException e) { 338 Log.d("BitmapRegionTileSource", "Failed to read EXIF for URI " + mUri, e); 339 return false; 340 } finally { 341 Utils.closeSilently(is); 342 } 343 } 344 } 345 346 public static class ResourceBitmapSource extends BitmapSource { 347 private Resources mRes; 348 private int mResId; 349 public ResourceBitmapSource(Resources res, int resId) { 350 mRes = res; 351 mResId = resId; 352 } 353 private InputStream regenerateInputStream() { 354 InputStream is = mRes.openRawResource(mResId); 355 return new BufferedInputStream(is); 356 } 357 @Override 358 public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() { 359 InputStream is = regenerateInputStream(); 360 SimpleBitmapRegionDecoder regionDecoder = 361 SimpleBitmapRegionDecoderWrapper.newInstance(is, false); 362 Utils.closeSilently(is); 363 if (regionDecoder == null) { 364 is = regenerateInputStream(); 365 regionDecoder = DumbBitmapRegionDecoder.newInstance(is); 366 Utils.closeSilently(is); 367 } 368 return regionDecoder; 369 } 370 @Override 371 public Bitmap loadPreviewBitmap(BitmapFactory.Options options) { 372 return BitmapFactory.decodeResource(mRes, mResId, options); 373 } 374 @Override 375 public boolean readExif(ExifInterface ei) { 376 try { 377 InputStream is = regenerateInputStream(); 378 ei.readExif(is); 379 Utils.closeSilently(is); 380 return true; 381 } catch (IOException e) { 382 Log.e("BitmapRegionTileSource", "Error reading resource", e); 383 return false; 384 } 385 } 386 } 387 388 SimpleBitmapRegionDecoder mDecoder; 389 int mWidth; 390 int mHeight; 391 int mTileSize; 392 private BasicTexture mPreview; 393 private final int mRotation; 394 395 // For use only by getTile 396 private Rect mWantRegion = new Rect(); 397 private BitmapFactory.Options mOptions; 398 399 public BitmapRegionTileSource(Context context, BitmapSource source, byte[] tempStorage) { 400 mTileSize = TiledImageRenderer.suggestedTileSize(context); 401 mRotation = source.getRotation(); 402 mDecoder = source.getBitmapRegionDecoder(); 403 if (mDecoder != null) { 404 mWidth = mDecoder.getWidth(); 405 mHeight = mDecoder.getHeight(); 406 mOptions = new BitmapFactory.Options(); 407 mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; 408 mOptions.inPreferQualityOverSpeed = true; 409 mOptions.inTempStorage = tempStorage; 410 411 Bitmap preview = source.getPreviewBitmap(); 412 if (preview != null && 413 preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) { 414 mPreview = new BitmapTexture(preview); 415 } else { 416 Log.w(TAG, String.format( 417 "Failed to create preview of apropriate size! " 418 + " in: %dx%d, out: %dx%d", 419 mWidth, mHeight, 420 preview == null ? -1 : preview.getWidth(), 421 preview == null ? -1 : preview.getHeight())); 422 } 423 } 424 } 425 426 public Bitmap getBitmap() { 427 return mPreview instanceof BitmapTexture ? ((BitmapTexture) mPreview).getBitmap() : null; 428 } 429 430 @Override 431 public int getTileSize() { 432 return mTileSize; 433 } 434 435 @Override 436 public int getImageWidth() { 437 return mWidth; 438 } 439 440 @Override 441 public int getImageHeight() { 442 return mHeight; 443 } 444 445 @Override 446 public BasicTexture getPreview() { 447 return mPreview; 448 } 449 450 @Override 451 public int getRotation() { 452 return mRotation; 453 } 454 455 @Override 456 public Bitmap getTile(int level, int x, int y, Bitmap bitmap) { 457 int tileSize = getTileSize(); 458 int t = tileSize << level; 459 mWantRegion.set(x, y, x + t, y + t); 460 461 if (bitmap == null) { 462 bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888); 463 } 464 465 mOptions.inSampleSize = (1 << level); 466 mOptions.inBitmap = bitmap; 467 468 try { 469 bitmap = mDecoder.decodeRegion(mWantRegion, mOptions); 470 } finally { 471 if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) { 472 mOptions.inBitmap = null; 473 } 474 } 475 476 if (bitmap == null) { 477 Log.w("BitmapRegionTileSource", "fail in decoding region"); 478 } 479 return bitmap; 480 } 481 } 482