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