blob: cdc5cdcbd6c97d893e266595f8b85838c402ae0e [file] [log] [blame]
Michael Jurkae8d1bf72013-09-09 15:58:54 +02001/*
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;
Michael Jurkab2552642013-10-31 11:07:24 +010027import android.graphics.Paint;
Michael Jurkae8d1bf72013-09-09 15:58:54 +020028import 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 Jurka7b215cb2013-10-30 14:40:39 +010035import com.android.gallery3d.common.Utils;
Michael Jurka5271ea12013-10-28 14:37:37 +010036import com.android.gallery3d.exif.ExifInterface;
Michael Jurkae8d1bf72013-09-09 15:58:54 +020037import com.android.gallery3d.glrenderer.BasicTexture;
38import com.android.gallery3d.glrenderer.BitmapTexture;
39import com.android.photos.views.TiledImageRenderer;
40
41import java.io.BufferedInputStream;
Michael Jurka5271ea12013-10-28 14:37:37 +010042import java.io.FileNotFoundException;
Michael Jurkae8d1bf72013-09-09 15:58:54 +020043import java.io.IOException;
44import java.io.InputStream;
45
Michael Jurka7b215cb2013-10-30 14:40:39 +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 }
Michael Jurkab2552642013-10-31 11:07:24 +010057 public static SimpleBitmapRegionDecoderWrapper newInstance(
58 String pathName, boolean isShareable) {
Michael Jurka7b215cb2013-10-30 14:40:39 +010059 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 }
Michael Jurkab2552642013-10-31 11:07:24 +010070 public static SimpleBitmapRegionDecoderWrapper newInstance(
71 InputStream is, boolean isShareable) {
Michael Jurka7b215cb2013-10-30 14:40:39 +010072 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 Jurka7b215cb2013-10-30 14:40:39 +010095 Bitmap mBuffer;
Michael Jurkab2552642013-10-31 11:07:24 +010096 Canvas mTempCanvas;
97 Paint mTempPaint;
Michael Jurka7b215cb2013-10-30 14:40:39 +010098 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) {
Michael Jurkab2552642013-10-31 11:07:24 +0100122 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;
Michael Jurka7b215cb2013-10-30 14:40:39 +0100139 }
140}
141
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200142/**
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 Jurka5271ea12013-10-28 14:37:37 +0100156 public static final int MAX_PREVIEW_SIZE = GL_SIZE_LIMIT / 2;
157
158 public static abstract class BitmapSource {
Michael Jurka7b215cb2013-10-30 14:40:39 +0100159 private SimpleBitmapRegionDecoder mDecoder;
Michael Jurka5271ea12013-10-28 14:37:37 +0100160 private Bitmap mPreview;
161 private int mPreviewSize;
162 private int mRotation;
Michael Jurka7b215cb2013-10-30 14:40:39 +0100163 public enum State { NOT_LOADED, LOADED, ERROR_LOADING };
164 private State mState = State.NOT_LOADED;
Michael Jurka5271ea12013-10-28 14:37:37 +0100165 public BitmapSource(int previewSize) {
166 mPreviewSize = previewSize;
167 }
Michael Jurka7b215cb2013-10-30 14:40:39 +0100168 public boolean loadInBackground() {
Michael Jurka5271ea12013-10-28 14:37:37 +0100169 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 Jurka7b215cb2013-10-30 14:40:39 +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 Jurka5271ea12013-10-28 14:37:37 +0100188
Michael Jurka7b215cb2013-10-30 14:40:39 +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 Jurka5271ea12013-10-28 14:37:37 +0100196 }
197 }
198
Michael Jurka7b215cb2013-10-30 14:40:39 +0100199 public State getLoadingState() {
200 return mState;
201 }
202
203 public SimpleBitmapRegionDecoder getBitmapRegionDecoder() {
Michael Jurka5271ea12013-10-28 14:37:37 +0100204 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 Jurka7b215cb2013-10-30 14:40:39 +0100220 public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder();
Michael Jurka5271ea12013-10-28 14:37:37 +0100221 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 Jurka7b215cb2013-10-30 14:40:39 +0100231 public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
232 SimpleBitmapRegionDecoder d;
233 d = SimpleBitmapRegionDecoderWrapper.newInstance(mPath, true);
234 if (d == null) {
235 d = DumbBitmapRegionDecoder.newInstance(mPath);
Michael Jurka5271ea12013-10-28 14:37:37 +0100236 }
Michael Jurka7b215cb2013-10-30 14:40:39 +0100237 return d;
Michael Jurka5271ea12013-10-28 14:37:37 +0100238 }
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;
248 } catch (IOException e) {
249 Log.w("BitmapRegionTileSource", "getting decoder failed", e);
250 return false;
251 }
252 }
253 }
254
255 public static class UriBitmapSource extends BitmapSource {
256 private Context mContext;
257 private Uri mUri;
258 public UriBitmapSource(Context context, Uri uri, int previewSize) {
259 super(previewSize);
260 mContext = context;
261 mUri = uri;
262 }
263 private InputStream regenerateInputStream() throws FileNotFoundException {
264 InputStream is = mContext.getContentResolver().openInputStream(mUri);
265 return new BufferedInputStream(is);
266 }
267 @Override
Michael Jurka7b215cb2013-10-30 14:40:39 +0100268 public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
Michael Jurka5271ea12013-10-28 14:37:37 +0100269 try {
Michael Jurka7b215cb2013-10-30 14:40:39 +0100270 InputStream is = regenerateInputStream();
271 SimpleBitmapRegionDecoder regionDecoder =
272 SimpleBitmapRegionDecoderWrapper.newInstance(is, false);
273 Utils.closeSilently(is);
274 if (regionDecoder == null) {
275 is = regenerateInputStream();
276 regionDecoder = DumbBitmapRegionDecoder.newInstance(is);
Michael Jurkab2552642013-10-31 11:07:24 +0100277 Utils.closeSilently(is);
Michael Jurka7b215cb2013-10-30 14:40:39 +0100278 }
279 return regionDecoder;
Michael Jurka5271ea12013-10-28 14:37:37 +0100280 } catch (FileNotFoundException e) {
281 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
282 return null;
283 } catch (IOException e) {
284 Log.e("BitmapRegionTileSource", "Failure while reading URI " + mUri, e);
285 return null;
286 }
287 }
288 @Override
289 public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
290 try {
Michael Jurka7b215cb2013-10-30 14:40:39 +0100291 InputStream is = regenerateInputStream();
292 Bitmap b = BitmapFactory.decodeStream(is, null, options);
293 Utils.closeSilently(is);
294 return b;
Michael Jurka5271ea12013-10-28 14:37:37 +0100295 } catch (FileNotFoundException e) {
296 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
297 return null;
298 }
299 }
300 @Override
301 public boolean readExif(ExifInterface ei) {
Michael Jurkab2552642013-10-31 11:07:24 +0100302 InputStream is = null;
Michael Jurka5271ea12013-10-28 14:37:37 +0100303 try {
Michael Jurkab2552642013-10-31 11:07:24 +0100304 is = regenerateInputStream();
Michael Jurka7b215cb2013-10-30 14:40:39 +0100305 ei.readExif(is);
306 Utils.closeSilently(is);
Michael Jurka5271ea12013-10-28 14:37:37 +0100307 return true;
308 } catch (FileNotFoundException e) {
309 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
310 return false;
311 } catch (IOException e) {
Michael Jurka7b215cb2013-10-30 14:40:39 +0100312 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
Michael Jurka5271ea12013-10-28 14:37:37 +0100313 return false;
Michael Jurkab2552642013-10-31 11:07:24 +0100314 } finally {
315 Utils.closeSilently(is);
Michael Jurka5271ea12013-10-28 14:37:37 +0100316 }
317 }
318 }
319
320 public static class ResourceBitmapSource extends BitmapSource {
321 private Resources mRes;
322 private int mResId;
323 public ResourceBitmapSource(Resources res, int resId, int previewSize) {
324 super(previewSize);
325 mRes = res;
326 mResId = resId;
327 }
328 private InputStream regenerateInputStream() {
329 InputStream is = mRes.openRawResource(mResId);
330 return new BufferedInputStream(is);
331 }
332 @Override
Michael Jurka7b215cb2013-10-30 14:40:39 +0100333 public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
334 InputStream is = regenerateInputStream();
335 SimpleBitmapRegionDecoder regionDecoder =
336 SimpleBitmapRegionDecoderWrapper.newInstance(is, false);
337 Utils.closeSilently(is);
338 if (regionDecoder == null) {
339 is = regenerateInputStream();
340 regionDecoder = DumbBitmapRegionDecoder.newInstance(is);
Michael Jurkab2552642013-10-31 11:07:24 +0100341 Utils.closeSilently(is);
Michael Jurka5271ea12013-10-28 14:37:37 +0100342 }
Michael Jurka7b215cb2013-10-30 14:40:39 +0100343 return regionDecoder;
Michael Jurka5271ea12013-10-28 14:37:37 +0100344 }
345 @Override
346 public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
347 return BitmapFactory.decodeResource(mRes, mResId, options);
348 }
349 @Override
350 public boolean readExif(ExifInterface ei) {
351 try {
Michael Jurka7b215cb2013-10-30 14:40:39 +0100352 InputStream is = regenerateInputStream();
353 ei.readExif(is);
354 Utils.closeSilently(is);
Michael Jurka5271ea12013-10-28 14:37:37 +0100355 return true;
356 } catch (IOException e) {
357 Log.e("BitmapRegionTileSource", "Error reading resource", e);
358 return false;
359 }
360 }
361 }
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200362
Michael Jurka7b215cb2013-10-30 14:40:39 +0100363 SimpleBitmapRegionDecoder mDecoder;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200364 int mWidth;
365 int mHeight;
366 int mTileSize;
367 private BasicTexture mPreview;
368 private final int mRotation;
369
370 // For use only by getTile
371 private Rect mWantRegion = new Rect();
372 private Rect mOverlapRegion = new Rect();
373 private BitmapFactory.Options mOptions;
374 private Canvas mCanvas;
375
Michael Jurka5271ea12013-10-28 14:37:37 +0100376 public BitmapRegionTileSource(Context context, BitmapSource source) {
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200377 mTileSize = TiledImageRenderer.suggestedTileSize(context);
Michael Jurka5271ea12013-10-28 14:37:37 +0100378 mRotation = source.getRotation();
379 mDecoder = source.getBitmapRegionDecoder();
Michael Jurka7b215cb2013-10-30 14:40:39 +0100380 if (mDecoder != null) {
381 mWidth = mDecoder.getWidth();
382 mHeight = mDecoder.getHeight();
383 mOptions = new BitmapFactory.Options();
384 mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
385 mOptions.inPreferQualityOverSpeed = true;
386 mOptions.inTempStorage = new byte[16 * 1024];
387 int previewSize = source.getPreviewSize();
388 if (previewSize != 0) {
389 previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE);
390 // Although this is the same size as the Bitmap that is likely already
391 // loaded, the lifecycle is different and interactions are on a different
392 // thread. Thus to simplify, this source will decode its own bitmap.
393 Bitmap preview = decodePreview(source, previewSize);
394 if (preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) {
395 mPreview = new BitmapTexture(preview);
396 } else {
397 Log.w(TAG, String.format(
398 "Failed to create preview of apropriate size! "
399 + " in: %dx%d, out: %dx%d",
400 mWidth, mHeight,
401 preview.getWidth(), preview.getHeight()));
402 }
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200403 }
404 }
405 }
406
407 @Override
408 public int getTileSize() {
409 return mTileSize;
410 }
411
412 @Override
413 public int getImageWidth() {
414 return mWidth;
415 }
416
417 @Override
418 public int getImageHeight() {
419 return mHeight;
420 }
421
422 @Override
423 public BasicTexture getPreview() {
424 return mPreview;
425 }
426
427 @Override
428 public int getRotation() {
429 return mRotation;
430 }
431
432 @Override
433 public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
434 int tileSize = getTileSize();
435 if (!REUSE_BITMAP) {
436 return getTileWithoutReusingBitmap(level, x, y, tileSize);
437 }
438
439 int t = tileSize << level;
440 mWantRegion.set(x, y, x + t, y + t);
441
442 if (bitmap == null) {
443 bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888);
444 }
445
446 mOptions.inSampleSize = (1 << level);
447 mOptions.inBitmap = bitmap;
448
449 try {
450 bitmap = mDecoder.decodeRegion(mWantRegion, mOptions);
451 } finally {
452 if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) {
453 mOptions.inBitmap = null;
454 }
455 }
456
457 if (bitmap == null) {
458 Log.w("BitmapRegionTileSource", "fail in decoding region");
459 }
460 return bitmap;
461 }
462
463 private Bitmap getTileWithoutReusingBitmap(
464 int level, int x, int y, int tileSize) {
465
466 int t = tileSize << level;
467 mWantRegion.set(x, y, x + t, y + t);
468
469 mOverlapRegion.set(0, 0, mWidth, mHeight);
470
471 mOptions.inSampleSize = (1 << level);
472 Bitmap bitmap = mDecoder.decodeRegion(mOverlapRegion, mOptions);
473
474 if (bitmap == null) {
475 Log.w(TAG, "fail in decoding region");
476 }
477
478 if (mWantRegion.equals(mOverlapRegion)) {
479 return bitmap;
480 }
481
482 Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888);
483 if (mCanvas == null) {
484 mCanvas = new Canvas();
485 }
486 mCanvas.setBitmap(result);
487 mCanvas.drawBitmap(bitmap,
488 (mOverlapRegion.left - mWantRegion.left) >> level,
489 (mOverlapRegion.top - mWantRegion.top) >> level, null);
490 mCanvas.setBitmap(null);
491 return result;
492 }
493
494 /**
495 * Note that the returned bitmap may have a long edge that's longer
496 * than the targetSize, but it will always be less than 2x the targetSize
497 */
Michael Jurka5271ea12013-10-28 14:37:37 +0100498 private Bitmap decodePreview(BitmapSource source, int targetSize) {
499 Bitmap result = source.getPreviewBitmap();
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200500 if (result == null) {
501 return null;
502 }
503
504 // We need to resize down if the decoder does not support inSampleSize
505 // or didn't support the specified inSampleSize (some decoders only do powers of 2)
Michael Jurka5271ea12013-10-28 14:37:37 +0100506 float scale = (float) targetSize / (float) (Math.max(result.getWidth(), result.getHeight()));
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200507
508 if (scale <= 0.5) {
509 result = BitmapUtils.resizeBitmapByScale(result, scale, true);
510 }
511 return ensureGLCompatibleBitmap(result);
512 }
513
514 private static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) {
515 if (bitmap == null || bitmap.getConfig() != null) {
516 return bitmap;
517 }
518 Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false);
519 bitmap.recycle();
520 return newBitmap;
521 }
522}