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