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 
17 package com.android.photos;
18 
19 import android.annotation.TargetApi;
20 import android.content.Context;
21 import android.graphics.Bitmap;
22 import android.graphics.Bitmap.Config;
23 import android.graphics.BitmapFactory;
24 import android.graphics.BitmapRegionDecoder;
25 import android.graphics.Canvas;
26 import android.graphics.Rect;
27 import android.os.Build;
28 import android.os.Build.VERSION_CODES;
29 import android.util.Log;
30 
31 import com.android.gallery3d.common.BitmapUtils;
32 import com.android.gallery3d.glrenderer.BasicTexture;
33 import com.android.gallery3d.glrenderer.BitmapTexture;
34 import com.android.photos.views.TiledImageRenderer;
35 
36 import java.io.IOException;
37 
38 /**
39  * A {@link com.android.photos.views.TiledImageRenderer.TileSource} using
40  * {@link BitmapRegionDecoder} to wrap a local file
41  */
42 @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
43 public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
44 
45     private static final String TAG = "BitmapRegionTileSource";
46 
47     private static final boolean REUSE_BITMAP =
48             Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
49     private static final int GL_SIZE_LIMIT = 2048;
50     // This must be no larger than half the size of the GL_SIZE_LIMIT
51     // due to decodePreview being allowed to be up to 2x the size of the target
52     private static final int MAX_PREVIEW_SIZE = 1024;
53 
54     BitmapRegionDecoder mDecoder;
55     int mWidth;
56     int mHeight;
57     int mTileSize;
58     private BasicTexture mPreview;
59     private final int mRotation;
60 
61     // For use only by getTile
62     private Rect mWantRegion = new Rect();
63     private Rect mOverlapRegion = new Rect();
64     private BitmapFactory.Options mOptions;
65     private Canvas mCanvas;
66 
BitmapRegionTileSource(Context context, String path, int previewSize, int rotation)67     public BitmapRegionTileSource(Context context, String path, int previewSize, int rotation) {
68         mTileSize = TiledImageRenderer.suggestedTileSize(context);
69         mRotation = rotation;
70         try {
71             mDecoder = BitmapRegionDecoder.newInstance(path, true);
72             mWidth = mDecoder.getWidth();
73             mHeight = mDecoder.getHeight();
74         } catch (IOException e) {
75             Log.w("BitmapRegionTileSource", "ctor failed", e);
76         }
77         mOptions = new BitmapFactory.Options();
78         mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
79         mOptions.inPreferQualityOverSpeed = true;
80         mOptions.inTempStorage = new byte[16 * 1024];
81         if (previewSize != 0) {
82             previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE);
83             // Although this is the same size as the Bitmap that is likely already
84             // loaded, the lifecycle is different and interactions are on a different
85             // thread. Thus to simplify, this source will decode its own bitmap.
86             Bitmap preview = decodePreview(path, previewSize);
87             if (preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) {
88                 mPreview = new BitmapTexture(preview);
89             } else {
90                 Log.w(TAG, String.format(
91                         "Failed to create preview of apropriate size! "
92                         + " in: %dx%d, out: %dx%d",
93                         mWidth, mHeight,
94                         preview.getWidth(), preview.getHeight()));
95             }
96         }
97     }
98 
99     @Override
getTileSize()100     public int getTileSize() {
101         return mTileSize;
102     }
103 
104     @Override
getImageWidth()105     public int getImageWidth() {
106         return mWidth;
107     }
108 
109     @Override
getImageHeight()110     public int getImageHeight() {
111         return mHeight;
112     }
113 
114     @Override
getPreview()115     public BasicTexture getPreview() {
116         return mPreview;
117     }
118 
119     @Override
getRotation()120     public int getRotation() {
121         return mRotation;
122     }
123 
124     @Override
getTile(int level, int x, int y, Bitmap bitmap)125     public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
126         int tileSize = getTileSize();
127         if (!REUSE_BITMAP) {
128             return getTileWithoutReusingBitmap(level, x, y, tileSize);
129         }
130 
131         int t = tileSize << level;
132         mWantRegion.set(x, y, x + t, y + t);
133 
134         if (bitmap == null) {
135             bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888);
136         }
137 
138         mOptions.inSampleSize = (1 << level);
139         mOptions.inBitmap = bitmap;
140 
141         try {
142             bitmap = mDecoder.decodeRegion(mWantRegion, mOptions);
143         } finally {
144             if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) {
145                 mOptions.inBitmap = null;
146             }
147         }
148 
149         if (bitmap == null) {
150             Log.w("BitmapRegionTileSource", "fail in decoding region");
151         }
152         return bitmap;
153     }
154 
getTileWithoutReusingBitmap( int level, int x, int y, int tileSize)155     private Bitmap getTileWithoutReusingBitmap(
156             int level, int x, int y, int tileSize) {
157 
158         int t = tileSize << level;
159         mWantRegion.set(x, y, x + t, y + t);
160 
161         mOverlapRegion.set(0, 0, mWidth, mHeight);
162 
163         mOptions.inSampleSize = (1 << level);
164         Bitmap bitmap = mDecoder.decodeRegion(mOverlapRegion, mOptions);
165 
166         if (bitmap == null) {
167             Log.w(TAG, "fail in decoding region");
168         }
169 
170         if (mWantRegion.equals(mOverlapRegion)) {
171             return bitmap;
172         }
173 
174         Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888);
175         if (mCanvas == null) {
176             mCanvas = new Canvas();
177         }
178         mCanvas.setBitmap(result);
179         mCanvas.drawBitmap(bitmap,
180                 (mOverlapRegion.left - mWantRegion.left) >> level,
181                 (mOverlapRegion.top - mWantRegion.top) >> level, null);
182         mCanvas.setBitmap(null);
183         return result;
184     }
185 
186     /**
187      * Note that the returned bitmap may have a long edge that's longer
188      * than the targetSize, but it will always be less than 2x the targetSize
189      */
decodePreview(String file, int targetSize)190     private Bitmap decodePreview(String file, int targetSize) {
191         float scale = (float) targetSize / Math.max(mWidth, mHeight);
192         mOptions.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
193         mOptions.inJustDecodeBounds = false;
194 
195         Bitmap result = BitmapFactory.decodeFile(file, mOptions);
196         if (result == null) {
197             return null;
198         }
199 
200         // We need to resize down if the decoder does not support inSampleSize
201         // or didn't support the specified inSampleSize (some decoders only do powers of 2)
202         scale = (float) targetSize / (float) (Math.max(result.getWidth(), result.getHeight()));
203 
204         if (scale <= 0.5) {
205             result = BitmapUtils.resizeBitmapByScale(result, scale, true);
206         }
207         return ensureGLCompatibleBitmap(result);
208     }
209 
ensureGLCompatibleBitmap(Bitmap bitmap)210     private static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) {
211         if (bitmap == null || bitmap.getConfig() != null) {
212             return bitmap;
213         }
214         Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false);
215         bitmap.recycle();
216         return newBitmap;
217     }
218 }
219