1 /*
2  * Copyright (C) 2010 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.gallery3d.data;
18 
19 import android.annotation.TargetApi;
20 import android.graphics.Bitmap;
21 import android.graphics.Bitmap.Config;
22 import android.graphics.BitmapFactory;
23 import android.graphics.BitmapFactory.Options;
24 import android.graphics.BitmapRegionDecoder;
25 import android.os.Build;
26 
27 import com.android.gallery3d.common.ApiHelper;
28 import com.android.gallery3d.common.BitmapUtils;
29 import com.android.gallery3d.common.Utils;
30 import com.android.photos.data.GalleryBitmapPool;
31 import com.android.gallery3d.ui.Log;
32 import com.android.gallery3d.util.ThreadPool.CancelListener;
33 import com.android.gallery3d.util.ThreadPool.JobContext;
34 
35 import java.io.FileDescriptor;
36 import java.io.FileInputStream;
37 import java.io.InputStream;
38 
39 public class DecodeUtils {
40     private static final String TAG = "DecodeUtils";
41 
42     private static class DecodeCanceller implements CancelListener {
43         Options mOptions;
44 
DecodeCanceller(Options options)45         public DecodeCanceller(Options options) {
46             mOptions = options;
47         }
48 
49         @Override
onCancel()50         public void onCancel() {
51             mOptions.requestCancelDecode();
52         }
53     }
54 
55     @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
setOptionsMutable(Options options)56     public static void setOptionsMutable(Options options) {
57         if (ApiHelper.HAS_OPTIONS_IN_MUTABLE) options.inMutable = true;
58     }
59 
decode(JobContext jc, FileDescriptor fd, Options options)60     public static Bitmap decode(JobContext jc, FileDescriptor fd, Options options) {
61         if (options == null) options = new Options();
62         jc.setCancelListener(new DecodeCanceller(options));
63         setOptionsMutable(options);
64         return ensureGLCompatibleBitmap(
65                 BitmapFactory.decodeFileDescriptor(fd, null, options));
66     }
67 
decodeBounds(JobContext jc, FileDescriptor fd, Options options)68     public static void decodeBounds(JobContext jc, FileDescriptor fd,
69             Options options) {
70         Utils.assertTrue(options != null);
71         options.inJustDecodeBounds = true;
72         jc.setCancelListener(new DecodeCanceller(options));
73         BitmapFactory.decodeFileDescriptor(fd, null, options);
74         options.inJustDecodeBounds = false;
75     }
76 
decode(JobContext jc, byte[] bytes, Options options)77     public static Bitmap decode(JobContext jc, byte[] bytes, Options options) {
78         return decode(jc, bytes, 0, bytes.length, options);
79     }
80 
decode(JobContext jc, byte[] bytes, int offset, int length, Options options)81     public static Bitmap decode(JobContext jc, byte[] bytes, int offset,
82             int length, Options options) {
83         if (options == null) options = new Options();
84         jc.setCancelListener(new DecodeCanceller(options));
85         setOptionsMutable(options);
86         return ensureGLCompatibleBitmap(
87                 BitmapFactory.decodeByteArray(bytes, offset, length, options));
88     }
89 
decodeBounds(JobContext jc, byte[] bytes, int offset, int length, Options options)90     public static void decodeBounds(JobContext jc, byte[] bytes, int offset,
91             int length, Options options) {
92         Utils.assertTrue(options != null);
93         options.inJustDecodeBounds = true;
94         jc.setCancelListener(new DecodeCanceller(options));
95         BitmapFactory.decodeByteArray(bytes, offset, length, options);
96         options.inJustDecodeBounds = false;
97     }
98 
decodeThumbnail( JobContext jc, String filePath, Options options, int targetSize, int type)99     public static Bitmap decodeThumbnail(
100             JobContext jc, String filePath, Options options, int targetSize, int type) {
101         FileInputStream fis = null;
102         try {
103             fis = new FileInputStream(filePath);
104             FileDescriptor fd = fis.getFD();
105             return decodeThumbnail(jc, fd, options, targetSize, type);
106         } catch (Exception ex) {
107             Log.w(TAG, ex);
108             return null;
109         } finally {
110             Utils.closeSilently(fis);
111         }
112     }
113 
decodeThumbnail( JobContext jc, FileDescriptor fd, Options options, int targetSize, int type)114     public static Bitmap decodeThumbnail(
115             JobContext jc, FileDescriptor fd, Options options, int targetSize, int type) {
116         if (options == null) options = new Options();
117         jc.setCancelListener(new DecodeCanceller(options));
118 
119         options.inJustDecodeBounds = true;
120         BitmapFactory.decodeFileDescriptor(fd, null, options);
121         if (jc.isCancelled()) return null;
122 
123         int w = options.outWidth;
124         int h = options.outHeight;
125 
126         if (type == MediaItem.TYPE_MICROTHUMBNAIL) {
127             // We center-crop the original image as it's micro thumbnail. In this case,
128             // we want to make sure the shorter side >= "targetSize".
129             float scale = (float) targetSize / Math.min(w, h);
130             options.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
131 
132             // For an extremely wide image, e.g. 300x30000, we may got OOM when decoding
133             // it for TYPE_MICROTHUMBNAIL. So we add a max number of pixels limit here.
134             final int MAX_PIXEL_COUNT = 640000; // 400 x 1600
135             if ((w / options.inSampleSize) * (h / options.inSampleSize) > MAX_PIXEL_COUNT) {
136                 options.inSampleSize = BitmapUtils.computeSampleSize(
137                         (float) Math.sqrt((double) MAX_PIXEL_COUNT / (w * h)));
138             }
139         } else {
140             // For screen nail, we only want to keep the longer side >= targetSize.
141             float scale = (float) targetSize / Math.max(w, h);
142             options.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
143         }
144 
145         options.inJustDecodeBounds = false;
146         setOptionsMutable(options);
147 
148         Bitmap result = BitmapFactory.decodeFileDescriptor(fd, null, options);
149         if (result == null) return null;
150 
151         // We need to resize down if the decoder does not support inSampleSize
152         // (For example, GIF images)
153         float scale = (float) targetSize / (type == MediaItem.TYPE_MICROTHUMBNAIL
154                 ? Math.min(result.getWidth(), result.getHeight())
155                 : Math.max(result.getWidth(), result.getHeight()));
156 
157         if (scale <= 0.5) result = BitmapUtils.resizeBitmapByScale(result, scale, true);
158         return ensureGLCompatibleBitmap(result);
159     }
160 
161     /**
162      * Decodes the bitmap from the given byte array if the image size is larger than the given
163      * requirement.
164      *
165      * Note: The returned image may be resized down. However, both width and height must be
166      * larger than the <code>targetSize</code>.
167      */
decodeIfBigEnough(JobContext jc, byte[] data, Options options, int targetSize)168     public static Bitmap decodeIfBigEnough(JobContext jc, byte[] data,
169             Options options, int targetSize) {
170         if (options == null) options = new Options();
171         jc.setCancelListener(new DecodeCanceller(options));
172 
173         options.inJustDecodeBounds = true;
174         BitmapFactory.decodeByteArray(data, 0, data.length, options);
175         if (jc.isCancelled()) return null;
176         if (options.outWidth < targetSize || options.outHeight < targetSize) {
177             return null;
178         }
179         options.inSampleSize = BitmapUtils.computeSampleSizeLarger(
180                 options.outWidth, options.outHeight, targetSize);
181         options.inJustDecodeBounds = false;
182         setOptionsMutable(options);
183 
184         return ensureGLCompatibleBitmap(
185                 BitmapFactory.decodeByteArray(data, 0, data.length, options));
186     }
187 
188     // TODO: This function should not be called directly from
189     // DecodeUtils.requestDecode(...), since we don't have the knowledge
190     // if the bitmap will be uploaded to GL.
ensureGLCompatibleBitmap(Bitmap bitmap)191     public static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) {
192         if (bitmap == null || bitmap.getConfig() != null) return bitmap;
193         Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false);
194         bitmap.recycle();
195         return newBitmap;
196     }
197 
createBitmapRegionDecoder( JobContext jc, byte[] bytes, int offset, int length, boolean shareable)198     public static BitmapRegionDecoder createBitmapRegionDecoder(
199             JobContext jc, byte[] bytes, int offset, int length,
200             boolean shareable) {
201         if (offset < 0 || length <= 0 || offset + length > bytes.length) {
202             throw new IllegalArgumentException(String.format(
203                     "offset = %s, length = %s, bytes = %s",
204                     offset, length, bytes.length));
205         }
206 
207         try {
208             return BitmapRegionDecoder.newInstance(
209                     bytes, offset, length, shareable);
210         } catch (Throwable t)  {
211             Log.w(TAG, t);
212             return null;
213         }
214     }
215 
createBitmapRegionDecoder( JobContext jc, String filePath, boolean shareable)216     public static BitmapRegionDecoder createBitmapRegionDecoder(
217             JobContext jc, String filePath, boolean shareable) {
218         try {
219             return BitmapRegionDecoder.newInstance(filePath, shareable);
220         } catch (Throwable t)  {
221             Log.w(TAG, t);
222             return null;
223         }
224     }
225 
createBitmapRegionDecoder( JobContext jc, FileDescriptor fd, boolean shareable)226     public static BitmapRegionDecoder createBitmapRegionDecoder(
227             JobContext jc, FileDescriptor fd, boolean shareable) {
228         try {
229             return BitmapRegionDecoder.newInstance(fd, shareable);
230         } catch (Throwable t)  {
231             Log.w(TAG, t);
232             return null;
233         }
234     }
235 
createBitmapRegionDecoder( JobContext jc, InputStream is, boolean shareable)236     public static BitmapRegionDecoder createBitmapRegionDecoder(
237             JobContext jc, InputStream is, boolean shareable) {
238         try {
239             return BitmapRegionDecoder.newInstance(is, shareable);
240         } catch (Throwable t)  {
241             // We often cancel the creating of bitmap region decoder,
242             // so just log one line.
243             Log.w(TAG, "requestCreateBitmapRegionDecoder: " + t);
244             return null;
245         }
246     }
247 
248     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
decodeUsingPool(JobContext jc, byte[] data, int offset, int length, BitmapFactory.Options options)249     public static Bitmap decodeUsingPool(JobContext jc, byte[] data, int offset,
250             int length, BitmapFactory.Options options) {
251         if (options == null) options = new BitmapFactory.Options();
252         if (options.inSampleSize < 1) options.inSampleSize = 1;
253         options.inPreferredConfig = Bitmap.Config.ARGB_8888;
254         options.inBitmap = (options.inSampleSize == 1)
255                 ? findCachedBitmap(jc, data, offset, length, options) : null;
256         try {
257             Bitmap bitmap = decode(jc, data, offset, length, options);
258             if (options.inBitmap != null && options.inBitmap != bitmap) {
259                 GalleryBitmapPool.getInstance().put(options.inBitmap);
260                 options.inBitmap = null;
261             }
262             return bitmap;
263         } catch (IllegalArgumentException e) {
264             if (options.inBitmap == null) throw e;
265 
266             Log.w(TAG, "decode fail with a given bitmap, try decode to a new bitmap");
267             GalleryBitmapPool.getInstance().put(options.inBitmap);
268             options.inBitmap = null;
269             return decode(jc, data, offset, length, options);
270         }
271     }
272 
273     // This is the same as the method above except the source data comes
274     // from a file descriptor instead of a byte array.
275     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
decodeUsingPool(JobContext jc, FileDescriptor fileDescriptor, Options options)276     public static Bitmap decodeUsingPool(JobContext jc,
277             FileDescriptor fileDescriptor, Options options) {
278         if (options == null) options = new BitmapFactory.Options();
279         if (options.inSampleSize < 1) options.inSampleSize = 1;
280         options.inPreferredConfig = Bitmap.Config.ARGB_8888;
281         options.inBitmap = (options.inSampleSize == 1)
282                 ? findCachedBitmap(jc, fileDescriptor, options) : null;
283         try {
284             Bitmap bitmap = DecodeUtils.decode(jc, fileDescriptor, options);
285             if (options.inBitmap != null && options.inBitmap != bitmap) {
286                 GalleryBitmapPool.getInstance().put(options.inBitmap);
287                 options.inBitmap = null;
288             }
289             return bitmap;
290         } catch (IllegalArgumentException e) {
291             if (options.inBitmap == null) throw e;
292 
293             Log.w(TAG, "decode fail with a given bitmap, try decode to a new bitmap");
294             GalleryBitmapPool.getInstance().put(options.inBitmap);
295             options.inBitmap = null;
296             return decode(jc, fileDescriptor, options);
297         }
298     }
299 
findCachedBitmap(JobContext jc, byte[] data, int offset, int length, Options options)300     private static Bitmap findCachedBitmap(JobContext jc, byte[] data,
301             int offset, int length, Options options) {
302         decodeBounds(jc, data, offset, length, options);
303         return GalleryBitmapPool.getInstance().get(options.outWidth, options.outHeight);
304     }
305 
findCachedBitmap(JobContext jc, FileDescriptor fileDescriptor, Options options)306     private static Bitmap findCachedBitmap(JobContext jc, FileDescriptor fileDescriptor,
307             Options options) {
308         decodeBounds(jc, fileDescriptor, options);
309         return GalleryBitmapPool.getInstance().get(options.outWidth, options.outHeight);
310     }
311 }
312