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