BitmapRegionTileSource.java revision 6090995951c6e2e4dcf38102f01793f8a94166e1
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.Paint;
28import 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;
35import com.android.gallery3d.common.Utils;
36import com.android.gallery3d.exif.ExifInterface;
37import com.android.gallery3d.glrenderer.BasicTexture;
38import com.android.gallery3d.glrenderer.BitmapTexture;
39import com.android.photos.views.TiledImageRenderer;
40
41import java.io.BufferedInputStream;
42import java.io.FileNotFoundException;
43import java.io.IOException;
44import java.io.InputStream;
45
46interface 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 {
95    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
142/**
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
156    public static final int MAX_PREVIEW_SIZE = GL_SIZE_LIMIT / 2;
157
158    public static abstract class BitmapSource {
159        private SimpleBitmapRegionDecoder mDecoder;
160        private Bitmap mPreview;
161        private int mPreviewSize;
162        private int mRotation;
163        public enum State { NOT_LOADED, LOADED, ERROR_LOADING };
164        private State mState = State.NOT_LOADED;
165        public BitmapSource(int previewSize) {
166            mPreviewSize = previewSize;
167        }
168        public boolean loadInBackground() {
169            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();
177            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;
188
189                    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;
196            }
197        }
198
199        public State getLoadingState() {
200            return mState;
201        }
202
203        public SimpleBitmapRegionDecoder getBitmapRegionDecoder() {
204            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);
220        public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder();
221        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
231        public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
232            SimpleBitmapRegionDecoder d;
233            d = SimpleBitmapRegionDecoderWrapper.newInstance(mPath, true);
234            if (d == null) {
235                d = DumbBitmapRegionDecoder.newInstance(mPath);
236            }
237            return d;
238        }
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
268        public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
269            try {
270                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);
277                    Utils.closeSilently(is);
278                }
279                return regionDecoder;
280            } 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 {
291                InputStream is = regenerateInputStream();
292                Bitmap b = BitmapFactory.decodeStream(is, null, options);
293                Utils.closeSilently(is);
294                return b;
295            } 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) {
302            InputStream is = null;
303            try {
304                is = regenerateInputStream();
305                ei.readExif(is);
306                Utils.closeSilently(is);
307                return true;
308            } catch (FileNotFoundException e) {
309                Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
310                return false;
311            } catch (IOException e) {
312                Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
313                return false;
314            } finally {
315                Utils.closeSilently(is);
316            }
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
333        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);
341                Utils.closeSilently(is);
342            }
343            return regionDecoder;
344        }
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 {
352                InputStream is = regenerateInputStream();
353                ei.readExif(is);
354                Utils.closeSilently(is);
355                return true;
356            } catch (IOException e) {
357                Log.e("BitmapRegionTileSource", "Error reading resource", e);
358                return false;
359            }
360        }
361    }
362
363    SimpleBitmapRegionDecoder mDecoder;
364    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
376    public BitmapRegionTileSource(Context context, BitmapSource source) {
377        mTileSize = TiledImageRenderer.suggestedTileSize(context);
378        mRotation = source.getRotation();
379        mDecoder = source.getBitmapRegionDecoder();
380        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                }
403            }
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     */
498    private Bitmap decodePreview(BitmapSource source, int targetSize) {
499        Bitmap result = source.getPreviewBitmap();
500        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)
506        float scale = (float) targetSize / (float) (Math.max(result.getWidth(), result.getHeight()));
507
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}
523