• 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 
9 import com.bumptech.glide.load.DecodeFormat;
10 import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
11 import com.bumptech.glide.util.ByteArrayPool;
12 import com.bumptech.glide.util.ExceptionCatchingInputStream;
13 import com.bumptech.glide.util.Util;
14 
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.util.EnumSet;
18 import java.util.Queue;
19 import java.util.Set;
20 
21 /**
22  * A base class with methods for loading and decoding images from InputStreams.
23  */
24 public abstract class Downsampler implements BitmapDecoder<InputStream> {
25     private static final String TAG = "Downsampler";
26 
27     private static final Set<ImageHeaderParser.ImageType> TYPES_THAT_USE_POOL = EnumSet.of(
28             ImageHeaderParser.ImageType.JPEG, ImageHeaderParser.ImageType.PNG_A, ImageHeaderParser.ImageType.PNG);
29 
30     private static final Queue<BitmapFactory.Options> OPTIONS_QUEUE = Util.createQueue(0);
31 
32     /**
33      * Load and scale the image uniformly (maintaining the image's aspect ratio) so that the dimensions of the image
34      * will be greater than or equal to the given width and height.
35      */
36     public static final Downsampler AT_LEAST = new Downsampler() {
37         @Override
38         protected int getSampleSize(int inWidth, int inHeight, int outWidth, int outHeight) {
39             return Math.min(inHeight / outHeight, inWidth / outWidth);
40         }
41 
42         @Override
43         public String getId() {
44             return "AT_LEAST.com.bumptech.glide.load.data.bitmap";
45         }
46     };
47 
48     /**
49      * Load and scale the image uniformly (maintaining the image's aspect ratio) so that the dimensions of the image
50      * will be less than or equal to the given width and height.
51      *
52      */
53     public static final Downsampler AT_MOST = new Downsampler() {
54         @Override
55         protected int getSampleSize(int inWidth, int inHeight, int outWidth, int outHeight) {
56             return Math.max(inHeight / outHeight, inWidth / outWidth);
57         }
58 
59         @Override
60         public String getId() {
61             return "AT_MOST.com.bumptech.glide.load.data.bitmap";
62         }
63     };
64 
65     /**
66      * Load the image at its original size.
67      */
68     public static final Downsampler NONE = new Downsampler() {
69         @Override
70         protected int getSampleSize(int inWidth, int inHeight, int outWidth, int outHeight) {
71             return 0;
72         }
73 
74         @Override
75         public String getId() {
76             return "NONE.com.bumptech.glide.load.data.bitmap";
77         }
78     };
79 
80     // 5MB. This is the max image header size we can handle, we preallocate a much smaller buffer but will resize up to
81     // this amount if necessary.
82     private static final int MARK_POSITION = 5 * 1024 * 1024;
83 
84 
85     /**
86      * Load the image for the given InputStream. If a recycled Bitmap whose dimensions exactly match those of the image
87      * for the given InputStream is available, the operation is much less expensive in terms of memory.
88      *
89      * <p>
90      *     Note - this method will throw an exception of a Bitmap with dimensions not matching
91      *     those of the image for the given InputStream is provided.
92      * </p>
93      *
94      * @param is An {@link InputStream} to the data for the image.
95      * @param pool A pool of recycled bitmaps.
96      * @param outWidth The width the final image should be close to.
97      * @param outHeight The height the final image should be close to.
98      * @return A new bitmap containing the image from the given InputStream, or recycle if recycle is not null.
99      */
100     @SuppressWarnings("resource")
101     // see BitmapDecoder.decode
102     @Override
decode(InputStream is, BitmapPool pool, int outWidth, int outHeight, DecodeFormat decodeFormat)103     public Bitmap decode(InputStream is, BitmapPool pool, int outWidth, int outHeight, DecodeFormat decodeFormat) {
104         final ByteArrayPool byteArrayPool = ByteArrayPool.get();
105         final byte[] bytesForOptions = byteArrayPool.getBytes();
106         final byte[] bytesForStream = byteArrayPool.getBytes();
107         final BitmapFactory.Options options = getDefaultOptions();
108 
109         // TODO(#126): when the framework handles exceptions better, consider removing.
110         final ExceptionCatchingInputStream stream =
111                 ExceptionCatchingInputStream.obtain(new RecyclableBufferedInputStream(is, bytesForStream));
112         try {
113             stream.mark(MARK_POSITION);
114             int orientation = 0;
115             try {
116                 orientation = new ImageHeaderParser(stream).getOrientation();
117             } catch (IOException e) {
118                 if (Log.isLoggable(TAG, Log.WARN)) {
119                     Log.w(TAG, "Cannot determine the image orientation from header", e);
120                 }
121             } finally {
122                 try {
123                     stream.reset();
124                 } catch (IOException e) {
125                     if (Log.isLoggable(TAG, Log.WARN)) {
126                         Log.w(TAG, "Cannot reset the input stream", e);
127                     }
128                 }
129             }
130 
131             options.inTempStorage = bytesForOptions;
132 
133             final int[] inDimens = getDimensions(stream, options);
134             final int inWidth = inDimens[0];
135             final int inHeight = inDimens[1];
136 
137             final int degreesToRotate = TransformationUtils.getExifOrientationDegrees(orientation);
138             final int sampleSize = getRoundedSampleSize(degreesToRotate, inWidth, inHeight, outWidth, outHeight);
139 
140             final Bitmap downsampled =
141                     downsampleWithSize(stream, options, pool, inWidth, inHeight, sampleSize,
142                             decodeFormat);
143 
144             // BitmapFactory swallows exceptions during decodes and in some cases when inBitmap is non null, may catch
145             // and log a stack trace but still return a non null bitmap. To avoid displaying partially decoded bitmaps,
146             // we catch exceptions reading from the stream in our ExceptionCatchingInputStream and throw them here.
147             final Exception streamException = stream.getException();
148             if (streamException != null) {
149                 throw new RuntimeException(streamException);
150             }
151 
152             Bitmap rotated = null;
153             if (downsampled != null) {
154                 rotated = TransformationUtils.rotateImageExif(downsampled, pool, orientation);
155 
156                 if (!downsampled.equals(rotated) && !pool.put(downsampled)) {
157                     downsampled.recycle();
158                 }
159             }
160 
161             return rotated;
162         } finally {
163             byteArrayPool.releaseBytes(bytesForOptions);
164             byteArrayPool.releaseBytes(bytesForStream);
165             stream.release();
166             releaseOptions(options);
167         }
168     }
169 
getRoundedSampleSize(int degreesToRotate, int inWidth, int inHeight, int outWidth, int outHeight)170     private int getRoundedSampleSize(int degreesToRotate, int inWidth, int inHeight, int outWidth, int outHeight) {
171         final int exactSampleSize;
172         if (degreesToRotate == 90 || degreesToRotate == 270) {
173             // If we're rotating the image +-90 degrees, we need to downsample accordingly so the image width is
174             // decreased to near our target's height and the image height is decreased to near our target width.
175             exactSampleSize = getSampleSize(inHeight, inWidth, outWidth, outHeight);
176         } else {
177             exactSampleSize = getSampleSize(inWidth, inHeight, outWidth, outHeight);
178         }
179 
180         // BitmapFactory only accepts powers of 2, so it will round down to the nearest power of two that is less than
181         // or equal to the sample size we provide. Because we need to estimate the final image width and height to
182         // re-use Bitmaps, we mirror BitmapFactory's calculation here. For bug, see issue #224. For algorithm see
183         // http://stackoverflow.com/a/17379704/800716.
184         final int powerOfTwoSampleSize = exactSampleSize == 0 ? 0 : Integer.highestOneBit(exactSampleSize - 1);
185 
186         // Although functionally equivalent to 0 for BitmapFactory, 1 is a safer default for our code than 0.
187         return Math.max(1, powerOfTwoSampleSize);
188     }
189 
downsampleWithSize(ExceptionCatchingInputStream is, BitmapFactory.Options options, BitmapPool pool, int inWidth, int inHeight, int sampleSize, DecodeFormat decodeFormat)190     private Bitmap downsampleWithSize(ExceptionCatchingInputStream is, BitmapFactory.Options options, BitmapPool pool,
191             int inWidth, int inHeight, int sampleSize, DecodeFormat decodeFormat) {
192         // Prior to KitKat, the inBitmap size must exactly match the size of the bitmap we're decoding.
193         Bitmap.Config config = getConfig(is, decodeFormat);
194         options.inSampleSize = sampleSize;
195         options.inPreferredConfig = config;
196         if ((options.inSampleSize == 1 || Build.VERSION_CODES.KITKAT <= Build.VERSION.SDK_INT) && shouldUsePool(is)) {
197             int targetWidth = (int) Math.ceil(inWidth / (double) sampleSize);
198             int targetHeight = (int) Math.ceil(inHeight / (double) sampleSize);
199             // BitmapFactory will clear out the Bitmap before writing to it, so getDirty is safe.
200             setInBitmap(options, pool.getDirty(targetWidth, targetHeight, config));
201         }
202         return decodeStream(is, options);
203     }
204 
shouldUsePool(InputStream is)205     private static boolean shouldUsePool(InputStream is) {
206         // On KitKat+, any bitmap can be used to decode any other bitmap.
207         if (Build.VERSION_CODES.KITKAT <= Build.VERSION.SDK_INT) {
208             return true;
209         }
210 
211         is.mark(1024);
212         try {
213             final ImageHeaderParser.ImageType type = new ImageHeaderParser(is).getType();
214             // cannot reuse bitmaps when decoding images that are not PNG or JPG.
215             // look at : https://groups.google.com/forum/#!msg/android-developers/Mp0MFVFi1Fo/e8ZQ9FGdWdEJ
216             return TYPES_THAT_USE_POOL.contains(type);
217         } catch (IOException e) {
218             if (Log.isLoggable(TAG, Log.WARN)) {
219                 Log.w(TAG, "Cannot determine the image type from header", e);
220             }
221         } finally {
222             try {
223                 is.reset();
224             } catch (IOException e) {
225                 if (Log.isLoggable(TAG, Log.WARN)) {
226                     Log.w(TAG, "Cannot reset the input stream", e);
227                 }
228             }
229         }
230         return false;
231     }
232 
getConfig(InputStream is, DecodeFormat format)233     private static Bitmap.Config getConfig(InputStream is, DecodeFormat format) {
234         // Changing configs can cause skewing on 4.1, see issue #128.
235         if (format == DecodeFormat.ALWAYS_ARGB_8888 || Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) {
236             return Bitmap.Config.ARGB_8888;
237         }
238 
239         boolean hasAlpha = false;
240         // We probably only need 25, but this is safer (particularly since the buffer size is > 1024).
241         is.mark(1024);
242         try {
243             hasAlpha = new ImageHeaderParser(is).hasAlpha();
244         } catch (IOException e) {
245             if (Log.isLoggable(TAG, Log.WARN)) {
246                 Log.w(TAG, "Cannot determine whether the image has alpha or not from header for format " + format, e);
247             }
248         } finally {
249             try {
250                 is.reset();
251             } catch (IOException e) {
252                 if (Log.isLoggable(TAG, Log.WARN)) {
253                     Log.w(TAG, "Cannot reset the input stream", e);
254                 }
255             }
256         }
257 
258         return hasAlpha ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
259     }
260 
261     /**
262      * Determine the amount of downsampling to use for a load given the dimensions of the image to be downsampled and
263      * the dimensions of the view/target the image will be displayed in.
264      *
265      * @see android.graphics.BitmapFactory.Options#inSampleSize
266      *
267      * @param inWidth The width of the image to be downsampled.
268      * @param inHeight The height of the image to be downsampled.
269      * @param outWidth The width of the view/target the image will be displayed in.
270      * @param outHeight The height of the view/target the imag will be displayed in.
271      * @return An integer to pass in to {@link BitmapFactory#decodeStream(java.io.InputStream, android.graphics.Rect,
272      *          android.graphics.BitmapFactory.Options)}.
273      */
getSampleSize(int inWidth, int inHeight, int outWidth, int outHeight)274     protected abstract int getSampleSize(int inWidth, int inHeight, int outWidth, int outHeight);
275 
276     /**
277      * A method for getting the dimensions of an image from the given InputStream.
278      *
279      * @param is The InputStream representing the image.
280      * @param options The options to pass to
281      *          {@link BitmapFactory#decodeStream(java.io.InputStream, android.graphics.Rect,
282      *              android.graphics.BitmapFactory.Options)}.
283      * @return an array containing the dimensions of the image in the form {width, height}.
284      */
getDimensions(ExceptionCatchingInputStream is, BitmapFactory.Options options)285     public int[] getDimensions(ExceptionCatchingInputStream is, BitmapFactory.Options options) {
286         options.inJustDecodeBounds = true;
287         decodeStream(is, options);
288         options.inJustDecodeBounds = false;
289         return new int[] { options.outWidth, options.outHeight };
290     }
291 
decodeStream(ExceptionCatchingInputStream is, BitmapFactory.Options options)292     private static Bitmap decodeStream(ExceptionCatchingInputStream is, BitmapFactory.Options options) {
293          if (options.inJustDecodeBounds) {
294              // This is large, but jpeg headers are not size bounded so we need something large enough to minimize
295              // the possibility of not being able to fit enough of the header in the buffer to get the image size so
296              // that we don't fail to load images. The BufferedInputStream will create a new buffer of 2x the
297              // original size each time we use up the buffer space without passing the mark so this is a maximum
298              // bound on the buffer size, not a default. Most of the time we won't go past our pre-allocated 16kb.
299              is.mark(MARK_POSITION);
300          } else {
301              // Once we've read the image header, we no longer need to allow the buffer to expand in size. To avoid
302              // unnecessary allocations reading image data, we fix the mark limit so that it is no larger than our
303              // current buffer size here. See issue #225.
304              is.fixMarkLimit();
305          }
306 
307         final Bitmap result = BitmapFactory.decodeStream(is, null, options);
308 
309         try {
310             if (options.inJustDecodeBounds) {
311                 is.reset();
312             }
313         } catch (IOException e) {
314             if (Log.isLoggable(TAG, Log.ERROR)) {
315                 Log.e(TAG, "Exception loading inDecodeBounds=" + options.inJustDecodeBounds
316                         + " sample=" + options.inSampleSize, e);
317             }
318         }
319 
320         return result;
321     }
322 
323     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
setInBitmap(BitmapFactory.Options options, Bitmap recycled)324     private static void setInBitmap(BitmapFactory.Options options, Bitmap recycled) {
325         if (Build.VERSION_CODES.HONEYCOMB <= Build.VERSION.SDK_INT) {
326             options.inBitmap = recycled;
327         }
328     }
329 
330     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
getDefaultOptions()331     private static synchronized BitmapFactory.Options getDefaultOptions() {
332         BitmapFactory.Options decodeBitmapOptions;
333         synchronized (OPTIONS_QUEUE) {
334             decodeBitmapOptions = OPTIONS_QUEUE.poll();
335         }
336         if (decodeBitmapOptions == null) {
337             decodeBitmapOptions = new BitmapFactory.Options();
338             resetOptions(decodeBitmapOptions);
339         }
340 
341         return decodeBitmapOptions;
342     }
343 
releaseOptions(BitmapFactory.Options decodeBitmapOptions)344     private static void releaseOptions(BitmapFactory.Options decodeBitmapOptions) {
345         resetOptions(decodeBitmapOptions);
346         synchronized (OPTIONS_QUEUE) {
347             OPTIONS_QUEUE.offer(decodeBitmapOptions);
348         }
349     }
350 
351     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
resetOptions(BitmapFactory.Options decodeBitmapOptions)352     private static void resetOptions(BitmapFactory.Options decodeBitmapOptions) {
353         decodeBitmapOptions.inTempStorage = null;
354         decodeBitmapOptions.inDither = false;
355         decodeBitmapOptions.inScaled = false;
356         decodeBitmapOptions.inSampleSize = 1;
357         decodeBitmapOptions.inPreferredConfig = null;
358         decodeBitmapOptions.inJustDecodeBounds = false;
359 
360         if (Build.VERSION_CODES.HONEYCOMB <= Build.VERSION.SDK_INT)  {
361             decodeBitmapOptions.inBitmap = null;
362             decodeBitmapOptions.inMutable = true;
363         }
364     }
365 }
366