• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.bumptech.glide.load.resource.bitmap;
2 
3 import android.annotation.TargetApi;
4 import android.graphics.Bitmap;
5 import android.graphics.BitmapFactory;
6 import android.os.Build;
7 import android.util.Log;
8 import com.bumptech.glide.load.DecodeFormat;
9 import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
10 import com.bumptech.glide.util.ByteArrayPool;
11 
12 import java.io.IOException;
13 import java.io.InputStream;
14 import java.util.ArrayDeque;
15 import java.util.EnumSet;
16 import java.util.Queue;
17 import java.util.Set;
18 
19 /**
20  * A base class with methods for loading and decoding images from InputStreams.
21  */
22 public abstract class Downsampler implements BitmapDecoder<InputStream> {
23     private static final String TAG = "Downsampler";
24 
25     private static final boolean CAN_RECYCLE = Build.VERSION.SDK_INT >= 11;
26     private static final Set<ImageHeaderParser.ImageType> TYPES_THAT_USE_POOL = EnumSet.of(
27             ImageHeaderParser.ImageType.JPEG, ImageHeaderParser.ImageType.PNG_A, ImageHeaderParser.ImageType.PNG);
28 
29     private static final Queue<BitmapFactory.Options> OPTIONS_QUEUE = new ArrayDeque<BitmapFactory.Options>();
30 
31     @TargetApi(11)
getDefaultOptions()32     private static synchronized BitmapFactory.Options getDefaultOptions() {
33         BitmapFactory.Options decodeBitmapOptions = OPTIONS_QUEUE.poll();
34         if (decodeBitmapOptions == null) {
35             decodeBitmapOptions = new BitmapFactory.Options();
36             resetOptions(decodeBitmapOptions);
37         }
38 
39         return decodeBitmapOptions;
40     }
41 
releaseOptions(BitmapFactory.Options decodeBitmapOptions)42     private static void releaseOptions(BitmapFactory.Options decodeBitmapOptions) {
43         resetOptions(decodeBitmapOptions);
44         OPTIONS_QUEUE.offer(decodeBitmapOptions);
45     }
46 
47     @TargetApi(11)
resetOptions(BitmapFactory.Options decodeBitmapOptions)48     private static void resetOptions(BitmapFactory.Options decodeBitmapOptions) {
49         decodeBitmapOptions.inTempStorage = null;
50         decodeBitmapOptions.inDither = false;
51         decodeBitmapOptions.inScaled = false;
52         decodeBitmapOptions.inSampleSize = 1;
53         decodeBitmapOptions.inPreferredConfig = null;
54         decodeBitmapOptions.inJustDecodeBounds = false;
55 
56         if (CAN_RECYCLE)  {
57             decodeBitmapOptions.inBitmap = null;
58             decodeBitmapOptions.inMutable = true;
59         }
60     }
61 
62     /**
63      * Load and scale the image uniformly (maintaining the image's aspect ratio) so that the dimensions of the image
64      * will be greater than or equal to the given width and height.
65      *
66      */
67     public static Downsampler AT_LEAST = new Downsampler() {
68         @Override
69         protected int getSampleSize(int inWidth, int inHeight, int outWidth, int outHeight) {
70             return Math.min(inHeight / outHeight, inWidth / outWidth);
71         }
72 
73         @Override
74         public String getId() {
75             return "AT_LEAST.com.bumptech.glide.load.data.bitmap";
76         }
77     };
78 
79     /**
80      * Load and scale the image uniformly (maintaining the image's aspect ratio) so that the dimensions of the image
81      * will be less than or equal to the given width and height.
82      *
83      */
84     public static Downsampler AT_MOST = new Downsampler() {
85         @Override
86         protected int getSampleSize(int inWidth, int inHeight, int outWidth, int outHeight) {
87             return Math.max(inHeight / outHeight, inWidth / outWidth);
88         }
89 
90         @Override
91         public String getId() {
92             return "AT_MOST.com.bumptech.glide.load.data.bitmap";
93         }
94     };
95 
96     /**
97      * Load the image at its original size
98      *
99      */
100     public static Downsampler NONE = new Downsampler() {
101         @Override
102         protected int getSampleSize(int inWidth, int inHeight, int outWidth, int outHeight) {
103             return 0;
104         }
105 
106         @Override
107         public String getId() {
108             return "NONE.com.bumptech.glide.load.data.bitmap";
109         }
110     };
111 
112     // 5MB. This is the max image header size we can handle, we preallocate a much smaller buffer but will resize up to
113     // this amount if necessary.
114     private static final int MARK_POSITION = 5 * 1024 * 1024;
115 
116 
117     /**
118      * Load the image for the given InputStream. If a recycled Bitmap whose dimensions exactly match those of the image
119      * for the given InputStream is available, the operation is much less expensive in terms of memory.
120      *
121      * Note - this method will throw an exception of a Bitmap with dimensions not matching those of the image for the
122      * given InputStream is provided.
123      *
124      * @param is An InputStream to the data for the image
125      * @param pool A pool of recycled bitmaps
126      * @param outWidth The width the final image should be close to
127      * @param outHeight The height the final image should be close to
128      * @return A new bitmap containing the image from the given InputStream, or recycle if recycle is not null
129      */
130     @Override
decode(InputStream is, BitmapPool pool, int outWidth, int outHeight, DecodeFormat decodeFormat)131     public Bitmap decode(InputStream is, BitmapPool pool, int outWidth, int outHeight, DecodeFormat decodeFormat) {
132         final ByteArrayPool byteArrayPool = ByteArrayPool.get();
133         byte[] bytesForOptions = byteArrayPool.getBytes();
134         byte[] bytesForStream = byteArrayPool.getBytes();
135         RecyclableBufferedInputStream bis = new RecyclableBufferedInputStream(is, bytesForStream);
136         bis.mark(MARK_POSITION);
137         int orientation = 0;
138         try {
139             orientation = new ImageHeaderParser(bis).getOrientation();
140         } catch (IOException e) {
141             e.printStackTrace();
142         }
143         try {
144             bis.reset();
145         } catch (IOException e) {
146             e.printStackTrace();
147         }
148 
149         final BitmapFactory.Options options = getDefaultOptions();
150         options.inTempStorage = bytesForOptions;
151 
152         final int[] inDimens = getDimensions(bis, options);
153         final int inWidth = inDimens[0];
154         final int inHeight = inDimens[1];
155 
156         final int degreesToRotate = TransformationUtils.getExifOrientationDegrees(orientation);
157         final int sampleSize;
158         if (degreesToRotate == 90 || degreesToRotate == 270) {
159             // If we're rotating the image +-90 degrees, we need to downsample accordingly so the image width is
160             // decreased to near our target's height and the image height is decreased to near our target width.
161             sampleSize = getSampleSize(inHeight, inWidth, outWidth, outHeight);
162         } else {
163             sampleSize = getSampleSize(inWidth, inHeight, outWidth, outHeight);
164         }
165 
166         final Bitmap downsampled = downsampleWithSize(bis, options, pool, inWidth, inHeight, sampleSize, decodeFormat);
167 
168         Bitmap rotated = null;
169         if (downsampled != null) {
170             rotated = TransformationUtils.rotateImageExif(downsampled, pool, orientation);
171 
172             if (downsampled != rotated && !pool.put(downsampled)) {
173                 downsampled.recycle();
174             }
175         }
176 
177         byteArrayPool.releaseBytes(bytesForOptions);
178         byteArrayPool.releaseBytes(bytesForStream);
179         releaseOptions(options);
180         return rotated;
181     }
182 
downsampleWithSize(RecyclableBufferedInputStream bis, BitmapFactory.Options options, BitmapPool pool, int inWidth, int inHeight, int sampleSize, DecodeFormat decodeFormat)183     protected Bitmap downsampleWithSize(RecyclableBufferedInputStream bis, BitmapFactory.Options options,
184             BitmapPool pool, int inWidth, int inHeight, int sampleSize, DecodeFormat decodeFormat) {
185         // Prior to KitKat, the inBitmap size must exactly match the size of the bitmap we're decoding.
186         Bitmap.Config config = getConfig(bis, decodeFormat);
187         options.inSampleSize = sampleSize;
188         options.inPreferredConfig = config;
189         if (options.inSampleSize == 1 || Build.VERSION.SDK_INT >= 19) {
190             if (shouldUsePool(bis)) {
191                 setInBitmap(options, pool.get(inWidth, inHeight, config));
192             }
193         }
194         return decodeStream(bis, options);
195     }
196 
shouldUsePool(RecyclableBufferedInputStream bis)197     private boolean shouldUsePool(RecyclableBufferedInputStream bis) {
198         // On KitKat+, any bitmap can be used to decode any other bitmap.
199         if (Build.VERSION.SDK_INT >= 19) {
200             return true;
201         }
202 
203         bis.mark(1024);
204         try {
205             final ImageHeaderParser.ImageType type = new ImageHeaderParser(bis).getType();
206             // cannot reuse bitmaps when decoding images that are not PNG or JPG.
207             // look at : https://groups.google.com/forum/#!msg/android-developers/Mp0MFVFi1Fo/e8ZQ9FGdWdEJ
208             return TYPES_THAT_USE_POOL.contains(type);
209         } catch (IOException e) {
210             e.printStackTrace();
211         } finally {
212             try {
213                 bis.reset();
214             } catch (IOException e) {
215                 e.printStackTrace();
216             }
217         }
218         return false;
219     }
220 
getConfig(RecyclableBufferedInputStream bis, DecodeFormat format)221     private Bitmap.Config getConfig(RecyclableBufferedInputStream bis, DecodeFormat format) {
222         if (format == DecodeFormat.ALWAYS_ARGB_8888) {
223             return Bitmap.Config.ARGB_8888;
224         }
225 
226         boolean hasAlpha = false;
227         bis.mark(1024); //we probably only need 25, but this is safer (particularly since the buffer size is > 1024)
228         try {
229             hasAlpha = new ImageHeaderParser(bis).hasAlpha();
230         } catch (IOException e) {
231             e.printStackTrace();
232         } finally {
233             try {
234                 bis.reset();
235             } catch (IOException e) {
236                 e.printStackTrace();
237             }
238         }
239 
240         return hasAlpha ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
241     }
242 
243     /**
244      * Determine the amount of downsampling to use for a load given the dimensions of the image to be downsampled and
245      * the dimensions of the view/target the image will be displayed in.
246      *
247      * @see BitmapFactory.Options#inSampleSize
248      *
249      * @param inWidth The width of the image to be downsampled
250      * @param inHeight The height of the image to be downsampled
251      * @param outWidth The width of the view/target the image will be displayed in
252      * @param outHeight The height of the view/target the imag will be displayed in
253      * @return An integer to pass in to {@link BitmapFactory#decodeStream(java.io.InputStream, android.graphics.Rect, android.graphics.BitmapFactory.Options)}
254      */
getSampleSize(int inWidth, int inHeight, int outWidth, int outHeight)255     protected abstract int getSampleSize(int inWidth, int inHeight, int outWidth, int outHeight);
256 
257     /**
258      * A method for getting the dimensions of an image from the given InputStream
259      *
260      * @param bis The InputStream representing the image
261      * @param options The options to pass to {@link BitmapFactory#decodeStream(java.io.InputStream, android.graphics.Rect, android.graphics.BitmapFactory.Options)}
262      * @return an array containing the dimensions of the image in the form {width, height}
263      */
getDimensions(RecyclableBufferedInputStream bis, BitmapFactory.Options options)264     public int[] getDimensions(RecyclableBufferedInputStream bis, BitmapFactory.Options options) {
265         options.inJustDecodeBounds = true;
266         decodeStream(bis, options);
267         options.inJustDecodeBounds = false;
268         return new int[] { options.outWidth, options.outHeight };
269     }
270 
271 
decodeStream(RecyclableBufferedInputStream bis, BitmapFactory.Options options)272     private Bitmap decodeStream(RecyclableBufferedInputStream bis, BitmapFactory.Options options) {
273          if (options.inJustDecodeBounds) {
274              bis.mark(MARK_POSITION); //this is large, but jpeg headers are not size bounded so we need
275                          //something large enough to minimize the possibility of not being able to fit
276                          //enough of the header in the buffer to get the image size so that we don't fail
277                          //to load images. The BufferedInputStream will create a new buffer of 2x the
278                          //original size each time we use up the buffer space without passing the mark so
279                          //this is a maximum bound on the buffer size, not a default. Most of the time we
280                          //won't go past our pre-allocated 16kb
281          }
282 
283         final Bitmap result = BitmapFactory.decodeStream(bis, null, options);
284 
285         try {
286             if (options.inJustDecodeBounds) {
287                 bis.reset();
288                 bis.clearMark();
289             } else {
290                 bis.close();
291             }
292         } catch (IOException e) {
293             if (Log.isLoggable(TAG, Log.ERROR)) {
294                 Log.e(TAG, "Exception loading inDecodeBounds=" + options.inJustDecodeBounds
295                         + " sample=" + options.inSampleSize, e);
296             }
297         }
298 
299         return result;
300     }
301 
302     @TargetApi(11)
setInBitmap(BitmapFactory.Options options, Bitmap recycled)303     private static void setInBitmap(BitmapFactory.Options options, Bitmap recycled) {
304         if (Build.VERSION.SDK_INT >= 11) {
305             options.inBitmap = recycled;
306         }
307     }
308 }
309