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