1 package com.android.wallpaperpicker.common; 2 3 import android.content.Context; 4 import android.content.res.Resources; 5 import android.graphics.Bitmap; 6 import android.graphics.BitmapFactory; 7 import android.graphics.BitmapRegionDecoder; 8 import android.graphics.Canvas; 9 import android.graphics.Matrix; 10 import android.graphics.Paint; 11 import android.graphics.Point; 12 import android.graphics.Rect; 13 import android.graphics.RectF; 14 import android.net.Uri; 15 import android.util.Log; 16 17 import com.android.gallery3d.common.ExifOrientation; 18 import com.android.gallery3d.common.Utils; 19 20 import java.io.BufferedInputStream; 21 import java.io.ByteArrayInputStream; 22 import java.io.IOException; 23 import java.io.InputStream; 24 25 /** 26 * An abstraction over input stream creation. Also contains some utility methods 27 * for various bitmap operations. 28 */ 29 public abstract class InputStreamProvider { 30 31 private static final String TAG = "InputStreamProvider"; 32 33 /** 34 * Tries to create a new stream or returns null on failure. 35 */ newStream()36 public InputStream newStream() { 37 try { 38 return newStreamNotNull(); 39 } catch (IOException e) { 40 return null; 41 } 42 } 43 44 /** 45 * Tries to create a new stream or throws an exception on failure. 46 */ newStreamNotNull()47 public abstract InputStream newStreamNotNull() throws IOException; 48 49 /** 50 * Returns the size of the image, if the stream represents an image. 51 */ getImageBounds()52 public Point getImageBounds() { 53 InputStream is = newStream(); 54 if (is != null) { 55 BitmapFactory.Options options = new BitmapFactory.Options(); 56 options.inJustDecodeBounds = true; 57 BitmapFactory.decodeStream(is, null, options); 58 Utils.closeSilently(is); 59 if (options.outWidth != 0 && options.outHeight != 0) { 60 return new Point(options.outWidth, options.outHeight); 61 } 62 } 63 return null; 64 } 65 readCroppedBitmap(RectF cropBounds, int outWidth, int outHeight, int rotation)66 public Bitmap readCroppedBitmap(RectF cropBounds, int outWidth, int outHeight, int rotation) { 67 // Find crop bounds (scaled to original image size) 68 Rect roundedTrueCrop = new Rect(); 69 Matrix rotateMatrix = new Matrix(); 70 Point bounds = getImageBounds(); 71 if (bounds == null) { 72 Log.w(TAG, "cannot get bounds for image"); 73 return null; 74 } 75 76 if (rotation > 0) { 77 rotateMatrix.setRotate(rotation); 78 79 Matrix inverseRotateMatrix = new Matrix(); 80 inverseRotateMatrix.setRotate(-rotation); 81 82 cropBounds.roundOut(roundedTrueCrop); 83 cropBounds.set(roundedTrueCrop); 84 85 float[] rotatedBounds = new float[] { bounds.x, bounds.y }; 86 rotateMatrix.mapPoints(rotatedBounds); 87 rotatedBounds[0] = Math.abs(rotatedBounds[0]); 88 rotatedBounds[1] = Math.abs(rotatedBounds[1]); 89 90 cropBounds.offset(-rotatedBounds[0]/2, -rotatedBounds[1]/2); 91 inverseRotateMatrix.mapRect(cropBounds); 92 cropBounds.offset(bounds.x/2, bounds.y/2); 93 } 94 95 cropBounds.roundOut(roundedTrueCrop); 96 if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) { 97 Log.w(TAG, "crop has bad values for full size image"); 98 return null; 99 } 100 101 // See how much we're reducing the size of the image 102 int scaleDownSampleSize = Math.max(1, Math.min(roundedTrueCrop.width() / outWidth, 103 roundedTrueCrop.height() / outHeight)); 104 // Attempt to open a region decoder 105 InputStream is = null; 106 BitmapRegionDecoder decoder = null; 107 try { 108 is = newStreamNotNull(); 109 decoder = BitmapRegionDecoder.newInstance(is, false); 110 } catch (IOException e) { 111 Log.w(TAG, "cannot open region decoder", e); 112 } finally { 113 Utils.closeSilently(is); 114 is = null; 115 } 116 117 Bitmap crop = null; 118 if (decoder != null) { 119 // Do region decoding to get crop bitmap 120 BitmapFactory.Options options = new BitmapFactory.Options(); 121 if (scaleDownSampleSize > 1) { 122 options.inSampleSize = scaleDownSampleSize; 123 } 124 crop = decoder.decodeRegion(roundedTrueCrop, options); 125 decoder.recycle(); 126 } 127 128 if (crop == null) { 129 // BitmapRegionDecoder has failed, try to crop in-memory 130 is = newStream(); 131 Bitmap fullSize = null; 132 if (is != null) { 133 BitmapFactory.Options options = new BitmapFactory.Options(); 134 if (scaleDownSampleSize > 1) { 135 options.inSampleSize = scaleDownSampleSize; 136 } 137 fullSize = BitmapFactory.decodeStream(is, null, options); 138 Utils.closeSilently(is); 139 } 140 if (fullSize != null) { 141 // Find out the true sample size that was used by the decoder 142 scaleDownSampleSize = bounds.x / fullSize.getWidth(); 143 cropBounds.left /= scaleDownSampleSize; 144 cropBounds.top /= scaleDownSampleSize; 145 cropBounds.bottom /= scaleDownSampleSize; 146 cropBounds.right /= scaleDownSampleSize; 147 cropBounds.roundOut(roundedTrueCrop); 148 149 // Adjust values to account for issues related to rounding 150 if (roundedTrueCrop.width() > fullSize.getWidth()) { 151 // Adjust the width 152 roundedTrueCrop.right = roundedTrueCrop.left + fullSize.getWidth(); 153 } 154 if (roundedTrueCrop.right > fullSize.getWidth()) { 155 // Adjust the left and right values. 156 roundedTrueCrop.offset(-(roundedTrueCrop.right - fullSize.getWidth()), 0); 157 } 158 if (roundedTrueCrop.height() > fullSize.getHeight()) { 159 // Adjust the height 160 roundedTrueCrop.bottom = roundedTrueCrop.top + fullSize.getHeight(); 161 } 162 if (roundedTrueCrop.bottom > fullSize.getHeight()) { 163 // Adjust the top and bottom values. 164 roundedTrueCrop.offset(0, -(roundedTrueCrop.bottom - fullSize.getHeight())); 165 } 166 167 crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left, 168 roundedTrueCrop.top, roundedTrueCrop.width(), 169 roundedTrueCrop.height()); 170 } 171 } 172 173 if (crop == null) { 174 return null; 175 } 176 if (outWidth > 0 && outHeight > 0 || rotation > 0) { 177 float[] dimsAfter = new float[] { crop.getWidth(), crop.getHeight() }; 178 rotateMatrix.mapPoints(dimsAfter); 179 dimsAfter[0] = Math.abs(dimsAfter[0]); 180 dimsAfter[1] = Math.abs(dimsAfter[1]); 181 182 if (!(outWidth > 0 && outHeight > 0)) { 183 outWidth = Math.round(dimsAfter[0]); 184 outHeight = Math.round(dimsAfter[1]); 185 } 186 187 RectF cropRect = new RectF(0, 0, dimsAfter[0], dimsAfter[1]); 188 RectF returnRect = new RectF(0, 0, outWidth, outHeight); 189 190 Matrix m = new Matrix(); 191 if (rotation == 0) { 192 m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL); 193 } else { 194 Matrix m1 = new Matrix(); 195 m1.setTranslate(-crop.getWidth() / 2f, -crop.getHeight() / 2f); 196 Matrix m2 = new Matrix(); 197 m2.setRotate(rotation); 198 Matrix m3 = new Matrix(); 199 m3.setTranslate(dimsAfter[0] / 2f, dimsAfter[1] / 2f); 200 Matrix m4 = new Matrix(); 201 m4.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL); 202 203 Matrix c1 = new Matrix(); 204 c1.setConcat(m2, m1); 205 Matrix c2 = new Matrix(); 206 c2.setConcat(m4, m3); 207 m.setConcat(c2, c1); 208 } 209 210 Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(), 211 (int) returnRect.height(), Bitmap.Config.ARGB_8888); 212 if (tmp != null) { 213 Canvas c = new Canvas(tmp); 214 Paint p = new Paint(); 215 p.setFilterBitmap(true); 216 c.drawBitmap(crop, m, p); 217 crop = tmp; 218 } 219 } 220 return crop; 221 } 222 getRotationFromExif(Context context)223 public int getRotationFromExif(Context context) { 224 InputStream is = null; 225 try { 226 is = newStreamNotNull(); 227 return ExifOrientation.readRotation(new BufferedInputStream(is), context); 228 } catch (IOException | NullPointerException e) { 229 Log.w(TAG, "Getting exif data failed", e); 230 } finally { 231 Utils.closeSilently(is); 232 } 233 return 0; 234 } 235 fromUri(final Context context, final Uri uri)236 public static InputStreamProvider fromUri(final Context context, final Uri uri) { 237 return new InputStreamProvider() { 238 @Override 239 public InputStream newStreamNotNull() throws IOException { 240 return new BufferedInputStream(context.getContentResolver().openInputStream(uri)); 241 } 242 }; 243 } 244 245 public static InputStreamProvider fromResource(final Resources resources, final int resId) { 246 return new InputStreamProvider() { 247 @Override 248 public InputStream newStreamNotNull() { 249 return new BufferedInputStream(resources.openRawResource(resId)); 250 } 251 }; 252 } 253 254 public static InputStreamProvider fromBytes(final byte[] bytes) { 255 return new InputStreamProvider() { 256 @Override 257 public InputStream newStreamNotNull() { 258 return new BufferedInputStream(new ByteArrayInputStream(bytes)); 259 } 260 }; 261 } 262 263 } 264