1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.mail.utils;
18import android.graphics.Bitmap;
19import android.graphics.BitmapFactory;
20import android.graphics.Canvas;
21import android.graphics.Color;
22import android.graphics.Matrix;
23import android.graphics.Paint;
24import android.graphics.PorterDuff;
25import android.graphics.PorterDuffXfermode;
26
27/**
28 * Provides static functions to decode bitmaps at the optimal size
29 */
30public class BitmapUtil {
31
32    private static final String TAG = LogTag.getLogTag();
33    private static final boolean DEBUG = false;
34
35    private BitmapUtil() {
36    }
37
38    /**
39     * Decode an image into a Bitmap, using sub-sampling if the hinted dimensions call for it.
40     * Does not crop to fit the hinted dimensions.
41     *
42     * @param src an encoded image
43     * @param w hint width in px
44     * @param h hint height in px
45     * @return a decoded Bitmap that is not exactly sized to the hinted dimensions.
46     */
47    public static Bitmap decodeByteArray(byte[] src, int w, int h) {
48        try {
49            // calculate sample size based on w/h
50            final BitmapFactory.Options opts = new BitmapFactory.Options();
51            opts.inJustDecodeBounds = true;
52            BitmapFactory.decodeByteArray(src, 0, src.length, opts);
53            if (opts.mCancel || opts.outWidth == -1 || opts.outHeight == -1) {
54                return null;
55            }
56            opts.inSampleSize = Math.min(opts.outWidth / w, opts.outHeight / h);
57            opts.inJustDecodeBounds = false;
58            return BitmapFactory.decodeByteArray(src, 0, src.length, opts);
59        } catch (Throwable t) {
60            LogUtils.w(TAG, t, "BitmapUtils unable to decode image");
61            return null;
62        }
63    }
64
65    /**
66     * Decode an image into a Bitmap, using sub-sampling if the desired dimensions call for it.
67     * Also applies a center-crop a la {@link android.widget.ImageView.ScaleType#CENTER_CROP}.
68     *
69     * @param src an encoded image
70     * @param w desired width in px
71     * @param h desired height in px
72     * @return an exactly-sized decoded Bitmap that is center-cropped.
73     */
74    public static Bitmap decodeByteArrayWithCenterCrop(byte[] src, int w, int h) {
75        try {
76            final Bitmap decoded = decodeByteArray(src, w, h);
77            return centerCrop(decoded, w, h);
78
79        } catch (Throwable t) {
80            LogUtils.w(TAG, t, "BitmapUtils unable to crop image");
81            return null;
82        }
83    }
84
85    /**
86     * Returns a new Bitmap copy with a center-crop effect a la
87     * {@link android.widget.ImageView.ScaleType#CENTER_CROP}. May return the input bitmap if no
88     * scaling is necessary.
89     *
90     * @param src original bitmap of any size
91     * @param w desired width in px
92     * @param h desired height in px
93     * @return a copy of src conforming to the given width and height, or src itself if it already
94     *         matches the given width and height
95     */
96    public static Bitmap centerCrop(final Bitmap src, final int w, final int h) {
97        return crop(src, w, h, 0.5f, 0.5f);
98    }
99
100    /**
101     * Returns a new Bitmap copy with a crop effect depending on the crop anchor given. 0.5f is like
102     * {@link android.widget.ImageView.ScaleType#CENTER_CROP}. The crop anchor will be be nudged
103     * so the entire cropped bitmap will fit inside the src. May return the input bitmap if no
104     * scaling is necessary.
105     *
106     *
107     * Example of changing verticalCenterPercent:
108     *   _________            _________
109     *  |         |          |         |
110     *  |         |          |_________|
111     *  |         |          |         |/___0.3f
112     *  |---------|          |_________|\
113     *  |         |<---0.5f  |         |
114     *  |---------|          |         |
115     *  |         |          |         |
116     *  |         |          |         |
117     *  |_________|          |_________|
118     *
119     * @param src original bitmap of any size
120     * @param w desired width in px
121     * @param h desired height in px
122     * @param horizontalCenterPercent determines which part of the src to crop from. Range from 0
123     *                                .0f to 1.0f. The value determines which part of the src
124     *                                maps to the horizontal center of the resulting bitmap.
125     * @param verticalCenterPercent determines which part of the src to crop from. Range from 0
126     *                              .0f to 1.0f. The value determines which part of the src maps
127     *                              to the vertical center of the resulting bitmap.
128     * @return a copy of src conforming to the given width and height, or src itself if it already
129     *         matches the given width and height
130     */
131    public static Bitmap crop(final Bitmap src, final int w, final int h,
132            final float horizontalCenterPercent, final float verticalCenterPercent) {
133        if (horizontalCenterPercent < 0 || horizontalCenterPercent > 1 || verticalCenterPercent < 0
134                || verticalCenterPercent > 1) {
135            throw new IllegalArgumentException(
136                    "horizontalCenterPercent and verticalCenterPercent must be between 0.0f and "
137                            + "1.0f, inclusive.");
138        }
139        final int srcWidth = src.getWidth();
140        final int srcHeight = src.getHeight();
141
142        // exit early if no resize/crop needed
143        if (w == srcWidth && h == srcHeight) {
144            return src;
145        }
146
147        final Matrix m = new Matrix();
148        final float scale = Math.max(
149                (float) w / srcWidth,
150                (float) h / srcHeight);
151        m.setScale(scale, scale);
152
153        final int srcCroppedW, srcCroppedH;
154        int srcX, srcY;
155
156        srcCroppedW = Math.round(w / scale);
157        srcCroppedH = Math.round(h / scale);
158        srcX = (int) (srcWidth * horizontalCenterPercent - srcCroppedW / 2);
159        srcY = (int) (srcHeight * verticalCenterPercent - srcCroppedH / 2);
160
161        // Nudge srcX and srcY to be within the bounds of src
162        srcX = Math.max(Math.min(srcX, srcWidth - srcCroppedW), 0);
163        srcY = Math.max(Math.min(srcY, srcHeight - srcCroppedH), 0);
164
165        final Bitmap cropped = Bitmap.createBitmap(src, srcX, srcY, srcCroppedW, srcCroppedH, m,
166                true /* filter */);
167
168        if (DEBUG) LogUtils.i(TAG,
169                "BitmapUtils IN centerCrop, srcW/H=%s/%s desiredW/H=%s/%s srcX/Y=%s/%s" +
170                " innerW/H=%s/%s scale=%s resultW/H=%s/%s",
171                srcWidth, srcHeight, w, h, srcX, srcY, srcCroppedW, srcCroppedH, scale,
172                cropped.getWidth(), cropped.getHeight());
173        if (DEBUG && (w != cropped.getWidth() || h != cropped.getHeight())) {
174            LogUtils.e(TAG, new Error(), "BitmapUtils last center crop violated assumptions.");
175        }
176
177        return cropped;
178    }
179
180    /**
181     * Frames the input bitmap in a circle.
182     */
183    public static Bitmap frameBitmapInCircle(Bitmap input) {
184        if (input == null) {
185            return null;
186        }
187
188        // Crop the image if not squared.
189        int inputWidth = input.getWidth();
190        int inputHeight = input.getHeight();
191        int targetX, targetY, targetSize;
192        if (inputWidth >= inputHeight) {
193            targetX = inputWidth / 2 - inputHeight / 2;
194            targetY = 0;
195            targetSize = inputHeight;
196        } else {
197            targetX = 0;
198            targetY = inputHeight / 2 - inputWidth / 2;
199            targetSize = inputWidth;
200        }
201
202        // Create an output bitmap and a canvas to draw on it.
203        Bitmap output = Bitmap.createBitmap(targetSize, targetSize, Bitmap.Config.ARGB_8888);
204        Canvas canvas = new Canvas(output);
205
206        // Create a black paint to draw the mask.
207        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
208        paint.setColor(Color.BLACK);
209
210        // Draw a circle.
211        canvas.drawCircle(targetSize / 2, targetSize / 2, targetSize / 2, paint);
212
213        // Replace the black parts of the mask with the input image.
214        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
215        canvas.drawBitmap(input, targetX /* left */, targetY /* top */, paint);
216
217        return output;
218    }
219}
220