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