BitmapRegionTileSource.java revision 5271ea16c1989f99c0db7ab80b2b321441f60023
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.exif.ExifInterface;
35import com.android.gallery3d.glrenderer.BasicTexture;
36import com.android.gallery3d.glrenderer.BitmapTexture;
37import com.android.photos.views.TiledImageRenderer;
38
39import java.io.BufferedInputStream;
40import java.io.FileNotFoundException;
41import java.io.IOException;
42import java.io.InputStream;
43
44/**
45 * A {@link com.android.photos.views.TiledImageRenderer.TileSource} using
46 * {@link BitmapRegionDecoder} to wrap a local file
47 */
48@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
49public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
50
51    private static final String TAG = "BitmapRegionTileSource";
52
53    private static final boolean REUSE_BITMAP =
54            Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
55    private static final int GL_SIZE_LIMIT = 2048;
56    // This must be no larger than half the size of the GL_SIZE_LIMIT
57    // due to decodePreview being allowed to be up to 2x the size of the target
58    public static final int MAX_PREVIEW_SIZE = GL_SIZE_LIMIT / 2;
59
60    public static abstract class BitmapSource {
61        private BitmapRegionDecoder mDecoder;
62        private Bitmap mPreview;
63        private int mPreviewSize;
64        private int mRotation;
65        public BitmapSource(int previewSize) {
66            mPreviewSize = previewSize;
67        }
68        public void loadInBackground() {
69            ExifInterface ei = new ExifInterface();
70            if (readExif(ei)) {
71                Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION);
72                if (ori != null) {
73                    mRotation = ExifInterface.getRotationForOrientationValue(ori.shortValue());
74                }
75            }
76            mDecoder = loadBitmapRegionDecoder();
77            int width = mDecoder.getWidth();
78            int height = mDecoder.getHeight();
79            if (mPreviewSize != 0) {
80                int previewSize = Math.min(mPreviewSize, MAX_PREVIEW_SIZE);
81                BitmapFactory.Options opts = new BitmapFactory.Options();
82                opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
83                opts.inPreferQualityOverSpeed = true;
84
85                float scale = (float) previewSize / Math.max(width, height);
86                opts.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
87                opts.inJustDecodeBounds = false;
88                mPreview = loadPreviewBitmap(opts);
89            }
90        }
91
92        public BitmapRegionDecoder getBitmapRegionDecoder() {
93            return mDecoder;
94        }
95
96        public Bitmap getPreviewBitmap() {
97            return mPreview;
98        }
99
100        public int getPreviewSize() {
101            return mPreviewSize;
102        }
103
104        public int getRotation() {
105            return mRotation;
106        }
107
108        public abstract boolean readExif(ExifInterface ei);
109        public abstract BitmapRegionDecoder loadBitmapRegionDecoder();
110        public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options);
111    }
112
113    public static class FilePathBitmapSource extends BitmapSource {
114        private String mPath;
115        public FilePathBitmapSource(String path, int previewSize) {
116            super(previewSize);
117            mPath = path;
118        }
119        @Override
120        public BitmapRegionDecoder loadBitmapRegionDecoder() {
121            try {
122                return BitmapRegionDecoder.newInstance(mPath, true);
123            } catch (IOException e) {
124                Log.w("BitmapRegionTileSource", "getting decoder failed", e);
125                return null;
126            }
127        }
128        @Override
129        public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
130            return BitmapFactory.decodeFile(mPath, options);
131        }
132        @Override
133        public boolean readExif(ExifInterface ei) {
134            try {
135                ei.readExif(mPath);
136                return true;
137            } catch (IOException e) {
138                Log.w("BitmapRegionTileSource", "getting decoder failed", e);
139                return false;
140            }
141        }
142    }
143
144    public static class UriBitmapSource extends BitmapSource {
145        private Context mContext;
146        private Uri mUri;
147        public UriBitmapSource(Context context, Uri uri, int previewSize) {
148            super(previewSize);
149            mContext = context;
150            mUri = uri;
151        }
152        private InputStream regenerateInputStream() throws FileNotFoundException {
153            InputStream is = mContext.getContentResolver().openInputStream(mUri);
154            return new BufferedInputStream(is);
155        }
156        @Override
157        public BitmapRegionDecoder loadBitmapRegionDecoder() {
158            try {
159                return BitmapRegionDecoder.newInstance(regenerateInputStream(), true);
160            } catch (FileNotFoundException e) {
161                Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
162                return null;
163            } catch (IOException e) {
164                Log.e("BitmapRegionTileSource", "Failure while reading URI " + mUri, e);
165                return null;
166            }
167        }
168        @Override
169        public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
170            try {
171                return BitmapFactory.decodeStream(regenerateInputStream(), null, options);
172            } catch (FileNotFoundException e) {
173                Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
174                return null;
175            }
176        }
177        @Override
178        public boolean readExif(ExifInterface ei) {
179            try {
180                ei.readExif(regenerateInputStream());
181                return true;
182            } catch (FileNotFoundException e) {
183                Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
184                return false;
185            } catch (IOException e) {
186                Log.e("BitmapRegionTileSource", "Failure while reading URI " + mUri, e);
187                return false;
188            }
189        }
190    }
191
192    public static class ResourceBitmapSource extends BitmapSource {
193        private Resources mRes;
194        private int mResId;
195        public ResourceBitmapSource(Resources res, int resId, int previewSize) {
196            super(previewSize);
197            mRes = res;
198            mResId = resId;
199        }
200        private InputStream regenerateInputStream() {
201            InputStream is = mRes.openRawResource(mResId);
202            return new BufferedInputStream(is);
203        }
204        @Override
205        public BitmapRegionDecoder loadBitmapRegionDecoder() {
206            try {
207                return BitmapRegionDecoder.newInstance(regenerateInputStream(), true);
208            } catch (IOException e) {
209                Log.e("BitmapRegionTileSource", "Error reading resource", e);
210                return null;
211            }
212        }
213        @Override
214        public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
215            return BitmapFactory.decodeResource(mRes, mResId, options);
216        }
217        @Override
218        public boolean readExif(ExifInterface ei) {
219            try {
220                ei.readExif(regenerateInputStream());
221                return true;
222            } catch (IOException e) {
223                Log.e("BitmapRegionTileSource", "Error reading resource", e);
224                return false;
225            }
226        }
227    }
228
229    BitmapRegionDecoder mDecoder;
230    int mWidth;
231    int mHeight;
232    int mTileSize;
233    private BasicTexture mPreview;
234    private final int mRotation;
235
236    // For use only by getTile
237    private Rect mWantRegion = new Rect();
238    private Rect mOverlapRegion = new Rect();
239    private BitmapFactory.Options mOptions;
240    private Canvas mCanvas;
241
242    public BitmapRegionTileSource(Context context, BitmapSource source) {
243        mTileSize = TiledImageRenderer.suggestedTileSize(context);
244        mRotation = source.getRotation();
245        mDecoder = source.getBitmapRegionDecoder();
246        mWidth = mDecoder.getWidth();
247        mHeight = mDecoder.getHeight();
248        mOptions = new BitmapFactory.Options();
249        mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
250        mOptions.inPreferQualityOverSpeed = true;
251        mOptions.inTempStorage = new byte[16 * 1024];
252        int previewSize = source.getPreviewSize();
253        if (previewSize != 0) {
254            previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE);
255            // Although this is the same size as the Bitmap that is likely already
256            // loaded, the lifecycle is different and interactions are on a different
257            // thread. Thus to simplify, this source will decode its own bitmap.
258            Bitmap preview = decodePreview(source, previewSize);
259            if (preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) {
260                mPreview = new BitmapTexture(preview);
261            } else {
262                Log.w(TAG, String.format(
263                        "Failed to create preview of apropriate size! "
264                        + " in: %dx%d, out: %dx%d",
265                        mWidth, mHeight,
266                        preview.getWidth(), preview.getHeight()));
267            }
268        }
269    }
270
271    @Override
272    public int getTileSize() {
273        return mTileSize;
274    }
275
276    @Override
277    public int getImageWidth() {
278        return mWidth;
279    }
280
281    @Override
282    public int getImageHeight() {
283        return mHeight;
284    }
285
286    @Override
287    public BasicTexture getPreview() {
288        return mPreview;
289    }
290
291    @Override
292    public int getRotation() {
293        return mRotation;
294    }
295
296    @Override
297    public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
298        int tileSize = getTileSize();
299        if (!REUSE_BITMAP) {
300            return getTileWithoutReusingBitmap(level, x, y, tileSize);
301        }
302
303        int t = tileSize << level;
304        mWantRegion.set(x, y, x + t, y + t);
305
306        if (bitmap == null) {
307            bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888);
308        }
309
310        mOptions.inSampleSize = (1 << level);
311        mOptions.inBitmap = bitmap;
312
313        try {
314            bitmap = mDecoder.decodeRegion(mWantRegion, mOptions);
315        } finally {
316            if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) {
317                mOptions.inBitmap = null;
318            }
319        }
320
321        if (bitmap == null) {
322            Log.w("BitmapRegionTileSource", "fail in decoding region");
323        }
324        return bitmap;
325    }
326
327    private Bitmap getTileWithoutReusingBitmap(
328            int level, int x, int y, int tileSize) {
329
330        int t = tileSize << level;
331        mWantRegion.set(x, y, x + t, y + t);
332
333        mOverlapRegion.set(0, 0, mWidth, mHeight);
334
335        mOptions.inSampleSize = (1 << level);
336        Bitmap bitmap = mDecoder.decodeRegion(mOverlapRegion, mOptions);
337
338        if (bitmap == null) {
339            Log.w(TAG, "fail in decoding region");
340        }
341
342        if (mWantRegion.equals(mOverlapRegion)) {
343            return bitmap;
344        }
345
346        Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888);
347        if (mCanvas == null) {
348            mCanvas = new Canvas();
349        }
350        mCanvas.setBitmap(result);
351        mCanvas.drawBitmap(bitmap,
352                (mOverlapRegion.left - mWantRegion.left) >> level,
353                (mOverlapRegion.top - mWantRegion.top) >> level, null);
354        mCanvas.setBitmap(null);
355        return result;
356    }
357
358    /**
359     * Note that the returned bitmap may have a long edge that's longer
360     * than the targetSize, but it will always be less than 2x the targetSize
361     */
362    private Bitmap decodePreview(BitmapSource source, int targetSize) {
363        Bitmap result = source.getPreviewBitmap();
364        if (result == null) {
365            return null;
366        }
367
368        // We need to resize down if the decoder does not support inSampleSize
369        // or didn't support the specified inSampleSize (some decoders only do powers of 2)
370        float scale = (float) targetSize / (float) (Math.max(result.getWidth(), result.getHeight()));
371
372        if (scale <= 0.5) {
373            result = BitmapUtils.resizeBitmapByScale(result, scale, true);
374        }
375        return ensureGLCompatibleBitmap(result);
376    }
377
378    private static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) {
379        if (bitmap == null || bitmap.getConfig() != null) {
380            return bitmap;
381        }
382        Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false);
383        bitmap.recycle();
384        return newBitmap;
385    }
386}
387