blob: 764156de03fd765d9907501c98bda5f6ef6eb331 [file] [log] [blame]
Michael Jurka104c4562013-07-08 18:03:46 -07001/*
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;
Michael Jurkaeadbfc52013-09-04 00:45:37 +020021import android.content.res.Resources;
Michael Jurka104c4562013-07-08 18:03:46 -070022import android.graphics.Bitmap;
23import android.graphics.Bitmap.Config;
24import android.graphics.BitmapFactory;
25import android.graphics.BitmapRegionDecoder;
26import android.graphics.Canvas;
Michael Jurka9789c422013-10-30 14:59:37 +010027import android.graphics.Paint;
Michael Jurka104c4562013-07-08 18:03:46 -070028import 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;
Michael Jurka619a1802013-10-29 15:50:06 +010035import com.android.gallery3d.common.Utils;
Michael Jurka862f7e32013-10-16 18:23:56 -070036import com.android.gallery3d.exif.ExifInterface;
Michael Jurka104c4562013-07-08 18:03:46 -070037import com.android.gallery3d.glrenderer.BasicTexture;
38import com.android.gallery3d.glrenderer.BitmapTexture;
39import com.android.photos.views.TiledImageRenderer;
40
41import java.io.BufferedInputStream;
Michael Jurka862f7e32013-10-16 18:23:56 -070042import java.io.FileNotFoundException;
Michael Jurka104c4562013-07-08 18:03:46 -070043import java.io.IOException;
44import java.io.InputStream;
45
Michael Jurka9789c422013-10-30 14:59:37 +010046interface 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 {
Michael Jurka9789c422013-10-30 14:59:37 +010095 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
Michael Jurka104c4562013-07-08 18:03:46 -0700142/**
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
Michael Jurka83699e52013-10-23 20:59:51 +0200156 public static final int MAX_PREVIEW_SIZE = GL_SIZE_LIMIT / 2;
Michael Jurka104c4562013-07-08 18:03:46 -0700157
Michael Jurka862f7e32013-10-16 18:23:56 -0700158 public static abstract class BitmapSource {
Michael Jurka9789c422013-10-30 14:59:37 +0100159 private SimpleBitmapRegionDecoder mDecoder;
Michael Jurka862f7e32013-10-16 18:23:56 -0700160 private Bitmap mPreview;
161 private int mPreviewSize;
162 private int mRotation;
Michael Jurka619a1802013-10-29 15:50:06 +0100163 public enum State { NOT_LOADED, LOADED, ERROR_LOADING };
164 private State mState = State.NOT_LOADED;
Michael Jurka862f7e32013-10-16 18:23:56 -0700165 public BitmapSource(int previewSize) {
166 mPreviewSize = previewSize;
167 }
Michael Jurka619a1802013-10-29 15:50:06 +0100168 public boolean loadInBackground() {
Michael Jurka862f7e32013-10-16 18:23:56 -0700169 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();
Michael Jurka619a1802013-10-29 15:50:06 +0100177 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;
Michael Jurka862f7e32013-10-16 18:23:56 -0700188
Michael Jurka619a1802013-10-29 15:50:06 +0100189 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;
Michael Jurka862f7e32013-10-16 18:23:56 -0700196 }
197 }
198
Michael Jurka619a1802013-10-29 15:50:06 +0100199 public State getLoadingState() {
200 return mState;
201 }
202
Michael Jurka9789c422013-10-30 14:59:37 +0100203 public SimpleBitmapRegionDecoder getBitmapRegionDecoder() {
Michael Jurka862f7e32013-10-16 18:23:56 -0700204 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);
Michael Jurka9789c422013-10-30 14:59:37 +0100220 public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder();
Michael Jurka862f7e32013-10-16 18:23:56 -0700221 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
Michael Jurka9789c422013-10-30 14:59:37 +0100231 public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
232 SimpleBitmapRegionDecoder d;
233 d = SimpleBitmapRegionDecoderWrapper.newInstance(mPath, true);
234 if (d == null) {
235 d = DumbBitmapRegionDecoder.newInstance(mPath);
Michael Jurka862f7e32013-10-16 18:23:56 -0700236 }
Michael Jurka9789c422013-10-30 14:59:37 +0100237 return d;
Michael Jurka862f7e32013-10-16 18:23:56 -0700238 }
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;
Michael Jurkab1f50d72014-01-23 14:37:59 +0100248 } catch (NullPointerException e) {
249 Log.w("BitmapRegionTileSource", "reading exif failed", e);
250 return false;
Michael Jurka862f7e32013-10-16 18:23:56 -0700251 } catch (IOException e) {
252 Log.w("BitmapRegionTileSource", "getting decoder failed", e);
253 return false;
254 }
255 }
256 }
257
258 public static class UriBitmapSource extends BitmapSource {
259 private Context mContext;
260 private Uri mUri;
261 public UriBitmapSource(Context context, Uri uri, int previewSize) {
262 super(previewSize);
263 mContext = context;
264 mUri = uri;
265 }
266 private InputStream regenerateInputStream() throws FileNotFoundException {
267 InputStream is = mContext.getContentResolver().openInputStream(mUri);
268 return new BufferedInputStream(is);
269 }
270 @Override
Michael Jurka9789c422013-10-30 14:59:37 +0100271 public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
Michael Jurka862f7e32013-10-16 18:23:56 -0700272 try {
Michael Jurka619a1802013-10-29 15:50:06 +0100273 InputStream is = regenerateInputStream();
Michael Jurka9789c422013-10-30 14:59:37 +0100274 SimpleBitmapRegionDecoder regionDecoder =
275 SimpleBitmapRegionDecoderWrapper.newInstance(is, false);
Michael Jurka619a1802013-10-29 15:50:06 +0100276 Utils.closeSilently(is);
Michael Jurka9789c422013-10-30 14:59:37 +0100277 if (regionDecoder == null) {
278 is = regenerateInputStream();
279 regionDecoder = DumbBitmapRegionDecoder.newInstance(is);
280 Utils.closeSilently(is);
281 }
Michael Jurka619a1802013-10-29 15:50:06 +0100282 return regionDecoder;
Michael Jurka862f7e32013-10-16 18:23:56 -0700283 } catch (FileNotFoundException e) {
284 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
285 return null;
286 } catch (IOException e) {
287 Log.e("BitmapRegionTileSource", "Failure while reading URI " + mUri, e);
288 return null;
289 }
290 }
291 @Override
292 public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
293 try {
Michael Jurka619a1802013-10-29 15:50:06 +0100294 InputStream is = regenerateInputStream();
295 Bitmap b = BitmapFactory.decodeStream(is, null, options);
296 Utils.closeSilently(is);
297 return b;
Michael Jurka862f7e32013-10-16 18:23:56 -0700298 } catch (FileNotFoundException e) {
299 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
300 return null;
301 }
302 }
303 @Override
304 public boolean readExif(ExifInterface ei) {
Michael Jurka9789c422013-10-30 14:59:37 +0100305 InputStream is = null;
Michael Jurka862f7e32013-10-16 18:23:56 -0700306 try {
Michael Jurka9789c422013-10-30 14:59:37 +0100307 is = regenerateInputStream();
Michael Jurka619a1802013-10-29 15:50:06 +0100308 ei.readExif(is);
309 Utils.closeSilently(is);
Michael Jurka862f7e32013-10-16 18:23:56 -0700310 return true;
311 } catch (FileNotFoundException e) {
312 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
313 return false;
314 } catch (IOException e) {
Michael Jurka619a1802013-10-29 15:50:06 +0100315 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
Michael Jurka862f7e32013-10-16 18:23:56 -0700316 return false;
Michael Jurkab1f50d72014-01-23 14:37:59 +0100317 } catch (NullPointerException e) {
318 Log.e("BitmapRegionTileSource", "Failed to read EXIF for URI " + mUri, e);
319 return false;
Michael Jurka9789c422013-10-30 14:59:37 +0100320 } finally {
321 Utils.closeSilently(is);
Michael Jurka862f7e32013-10-16 18:23:56 -0700322 }
323 }
324 }
325
326 public static class ResourceBitmapSource extends BitmapSource {
327 private Resources mRes;
328 private int mResId;
329 public ResourceBitmapSource(Resources res, int resId, int previewSize) {
330 super(previewSize);
331 mRes = res;
332 mResId = resId;
333 }
334 private InputStream regenerateInputStream() {
335 InputStream is = mRes.openRawResource(mResId);
336 return new BufferedInputStream(is);
337 }
338 @Override
Michael Jurka9789c422013-10-30 14:59:37 +0100339 public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
340 InputStream is = regenerateInputStream();
341 SimpleBitmapRegionDecoder regionDecoder =
342 SimpleBitmapRegionDecoderWrapper.newInstance(is, false);
343 Utils.closeSilently(is);
344 if (regionDecoder == null) {
345 is = regenerateInputStream();
346 regionDecoder = DumbBitmapRegionDecoder.newInstance(is);
Michael Jurka619a1802013-10-29 15:50:06 +0100347 Utils.closeSilently(is);
Michael Jurka862f7e32013-10-16 18:23:56 -0700348 }
Michael Jurka9789c422013-10-30 14:59:37 +0100349 return regionDecoder;
Michael Jurka862f7e32013-10-16 18:23:56 -0700350 }
351 @Override
352 public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
353 return BitmapFactory.decodeResource(mRes, mResId, options);
354 }
355 @Override
356 public boolean readExif(ExifInterface ei) {
357 try {
Michael Jurka619a1802013-10-29 15:50:06 +0100358 InputStream is = regenerateInputStream();
359 ei.readExif(is);
360 Utils.closeSilently(is);
Michael Jurka862f7e32013-10-16 18:23:56 -0700361 return true;
362 } catch (IOException e) {
363 Log.e("BitmapRegionTileSource", "Error reading resource", e);
364 return false;
365 }
366 }
367 }
368
Michael Jurka9789c422013-10-30 14:59:37 +0100369 SimpleBitmapRegionDecoder mDecoder;
Michael Jurka104c4562013-07-08 18:03:46 -0700370 int mWidth;
371 int mHeight;
372 int mTileSize;
373 private BasicTexture mPreview;
374 private final int mRotation;
375
376 // For use only by getTile
377 private Rect mWantRegion = new Rect();
378 private Rect mOverlapRegion = new Rect();
379 private BitmapFactory.Options mOptions;
380 private Canvas mCanvas;
381
Michael Jurka862f7e32013-10-16 18:23:56 -0700382 public BitmapRegionTileSource(Context context, BitmapSource source) {
Michael Jurka104c4562013-07-08 18:03:46 -0700383 mTileSize = TiledImageRenderer.suggestedTileSize(context);
Michael Jurka862f7e32013-10-16 18:23:56 -0700384 mRotation = source.getRotation();
385 mDecoder = source.getBitmapRegionDecoder();
Michael Jurka619a1802013-10-29 15:50:06 +0100386 if (mDecoder != null) {
387 mWidth = mDecoder.getWidth();
388 mHeight = mDecoder.getHeight();
389 mOptions = new BitmapFactory.Options();
390 mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
391 mOptions.inPreferQualityOverSpeed = true;
392 mOptions.inTempStorage = new byte[16 * 1024];
393 int previewSize = source.getPreviewSize();
394 if (previewSize != 0) {
395 previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE);
396 // Although this is the same size as the Bitmap that is likely already
397 // loaded, the lifecycle is different and interactions are on a different
398 // thread. Thus to simplify, this source will decode its own bitmap.
399 Bitmap preview = decodePreview(source, previewSize);
400 if (preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) {
401 mPreview = new BitmapTexture(preview);
402 } else {
403 Log.w(TAG, String.format(
404 "Failed to create preview of apropriate size! "
405 + " in: %dx%d, out: %dx%d",
406 mWidth, mHeight,
407 preview.getWidth(), preview.getHeight()));
408 }
Michael Jurka104c4562013-07-08 18:03:46 -0700409 }
410 }
411 }
412
413 @Override
414 public int getTileSize() {
415 return mTileSize;
416 }
417
418 @Override
419 public int getImageWidth() {
420 return mWidth;
421 }
422
423 @Override
424 public int getImageHeight() {
425 return mHeight;
426 }
427
428 @Override
429 public BasicTexture getPreview() {
430 return mPreview;
431 }
432
433 @Override
434 public int getRotation() {
435 return mRotation;
436 }
437
438 @Override
439 public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
440 int tileSize = getTileSize();
441 if (!REUSE_BITMAP) {
442 return getTileWithoutReusingBitmap(level, x, y, tileSize);
443 }
444
445 int t = tileSize << level;
446 mWantRegion.set(x, y, x + t, y + t);
447
448 if (bitmap == null) {
449 bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888);
450 }
451
452 mOptions.inSampleSize = (1 << level);
453 mOptions.inBitmap = bitmap;
454
455 try {
456 bitmap = mDecoder.decodeRegion(mWantRegion, mOptions);
457 } finally {
458 if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) {
459 mOptions.inBitmap = null;
460 }
461 }
462
463 if (bitmap == null) {
464 Log.w("BitmapRegionTileSource", "fail in decoding region");
465 }
466 return bitmap;
467 }
468
469 private Bitmap getTileWithoutReusingBitmap(
470 int level, int x, int y, int tileSize) {
471
472 int t = tileSize << level;
473 mWantRegion.set(x, y, x + t, y + t);
474
475 mOverlapRegion.set(0, 0, mWidth, mHeight);
476
477 mOptions.inSampleSize = (1 << level);
478 Bitmap bitmap = mDecoder.decodeRegion(mOverlapRegion, mOptions);
479
480 if (bitmap == null) {
481 Log.w(TAG, "fail in decoding region");
482 }
483
484 if (mWantRegion.equals(mOverlapRegion)) {
485 return bitmap;
486 }
487
488 Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888);
489 if (mCanvas == null) {
490 mCanvas = new Canvas();
491 }
492 mCanvas.setBitmap(result);
493 mCanvas.drawBitmap(bitmap,
494 (mOverlapRegion.left - mWantRegion.left) >> level,
495 (mOverlapRegion.top - mWantRegion.top) >> level, null);
496 mCanvas.setBitmap(null);
497 return result;
498 }
499
500 /**
501 * Note that the returned bitmap may have a long edge that's longer
502 * than the targetSize, but it will always be less than 2x the targetSize
503 */
Michael Jurka862f7e32013-10-16 18:23:56 -0700504 private Bitmap decodePreview(BitmapSource source, int targetSize) {
505 Bitmap result = source.getPreviewBitmap();
Michael Jurka104c4562013-07-08 18:03:46 -0700506 if (result == null) {
507 return null;
508 }
509
510 // We need to resize down if the decoder does not support inSampleSize
511 // or didn't support the specified inSampleSize (some decoders only do powers of 2)
Michael Jurka862f7e32013-10-16 18:23:56 -0700512 float scale = (float) targetSize / (float) (Math.max(result.getWidth(), result.getHeight()));
Michael Jurka104c4562013-07-08 18:03:46 -0700513
514 if (scale <= 0.5) {
515 result = BitmapUtils.resizeBitmapByScale(result, scale, true);
516 }
517 return ensureGLCompatibleBitmap(result);
518 }
519
520 private static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) {
521 if (bitmap == null || bitmap.getConfig() != null) {
522 return bitmap;
523 }
524 Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false);
525 bitmap.recycle();
526 return newBitmap;
527 }
528}