1package com.android.wallpaperpicker.common;
2
3import android.content.Context;
4import android.content.res.Resources;
5import android.graphics.Bitmap;
6import android.graphics.BitmapFactory;
7import android.graphics.BitmapRegionDecoder;
8import android.graphics.Canvas;
9import android.graphics.Matrix;
10import android.graphics.Paint;
11import android.graphics.Point;
12import android.graphics.Rect;
13import android.graphics.RectF;
14import android.net.Uri;
15import android.util.Log;
16
17import com.android.gallery3d.common.ExifOrientation;
18import com.android.gallery3d.common.Utils;
19
20import java.io.BufferedInputStream;
21import java.io.ByteArrayInputStream;
22import java.io.IOException;
23import java.io.InputStream;
24
25/**
26 * An abstraction over input stream creation. Also contains some utility methods
27 * for various bitmap operations.
28 */
29public 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     */
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     */
47    public abstract InputStream newStreamNotNull() throws IOException;
48
49    /**
50     * Returns the size of the image, if the stream represents an image.
51     */
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
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
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
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