1/*
2 * Copyright (C) 2010 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.gallery3d.common;
18
19import android.graphics.Bitmap;
20import android.graphics.Bitmap.CompressFormat;
21import android.graphics.BitmapFactory;
22import android.graphics.Canvas;
23import android.graphics.Matrix;
24import android.graphics.Paint;
25import android.os.Build;
26import android.util.Log;
27
28import java.io.ByteArrayOutputStream;
29import java.lang.reflect.InvocationTargetException;
30import java.lang.reflect.Method;
31
32public class BitmapUtils {
33    private static final String TAG = "BitmapUtils";
34    private static final int COMPRESS_JPEG_QUALITY = 90;
35    public static final int UNCONSTRAINED = -1;
36
37    private BitmapUtils(){}
38
39    /*
40     * Compute the sample size as a function of minSideLength
41     * and maxNumOfPixels.
42     * minSideLength is used to specify that minimal width or height of a
43     * bitmap.
44     * maxNumOfPixels is used to specify the maximal size in pixels that is
45     * tolerable in terms of memory usage.
46     *
47     * The function returns a sample size based on the constraints.
48     * Both size and minSideLength can be passed in as UNCONSTRAINED,
49     * which indicates no care of the corresponding constraint.
50     * The functions prefers returning a sample size that
51     * generates a smaller bitmap, unless minSideLength = UNCONSTRAINED.
52     *
53     * Also, the function rounds up the sample size to a power of 2 or multiple
54     * of 8 because BitmapFactory only honors sample size this way.
55     * For example, BitmapFactory downsamples an image by 2 even though the
56     * request is 3. So we round up the sample size to avoid OOM.
57     */
58    public static int computeSampleSize(int width, int height,
59            int minSideLength, int maxNumOfPixels) {
60        int initialSize = computeInitialSampleSize(
61                width, height, minSideLength, maxNumOfPixels);
62
63        return initialSize <= 8
64                ? Utils.nextPowerOf2(initialSize)
65                : (initialSize + 7) / 8 * 8;
66    }
67
68    private static int computeInitialSampleSize(int w, int h,
69            int minSideLength, int maxNumOfPixels) {
70        if (maxNumOfPixels == UNCONSTRAINED
71                && minSideLength == UNCONSTRAINED) return 1;
72
73        int lowerBound = (maxNumOfPixels == UNCONSTRAINED) ? 1 :
74                (int) Math.ceil(Math.sqrt((double) (w * h) / maxNumOfPixels));
75
76        if (minSideLength == UNCONSTRAINED) {
77            return lowerBound;
78        } else {
79            int sampleSize = Math.min(w / minSideLength, h / minSideLength);
80            return Math.max(sampleSize, lowerBound);
81        }
82    }
83
84    // This computes a sample size which makes the longer side at least
85    // minSideLength long. If that's not possible, return 1.
86    public static int computeSampleSizeLarger(int w, int h,
87            int minSideLength) {
88        int initialSize = Math.max(w / minSideLength, h / minSideLength);
89        if (initialSize <= 1) return 1;
90
91        return initialSize <= 8
92                ? Utils.prevPowerOf2(initialSize)
93                : initialSize / 8 * 8;
94    }
95
96    // Fin the min x that 1 / x <= scale
97    public static int computeSampleSizeLarger(float scale) {
98        int initialSize = (int) Math.floor(1f / scale);
99        if (initialSize <= 1) return 1;
100
101        return initialSize <= 8
102                ? Utils.prevPowerOf2(initialSize)
103                : initialSize / 8 * 8;
104    }
105
106    // Find the max x that 1 / x >= scale.
107    public static int computeSampleSize(float scale) {
108        Utils.assertTrue(scale > 0);
109        int initialSize = Math.max(1, (int) Math.ceil(1 / scale));
110        return initialSize <= 8
111                ? Utils.nextPowerOf2(initialSize)
112                : (initialSize + 7) / 8 * 8;
113    }
114
115    public static Bitmap resizeDownToPixels(
116            Bitmap bitmap, int targetPixels, boolean recycle) {
117        int width = bitmap.getWidth();
118        int height = bitmap.getHeight();
119        float scale = (float) Math.sqrt(
120                (double) targetPixels / (width * height));
121        if (scale >= 1.0f) return bitmap;
122        return resizeBitmapByScale(bitmap, scale, recycle);
123    }
124
125    public static Bitmap resizeBitmapByScale(
126            Bitmap bitmap, float scale, boolean recycle) {
127        int width = Math.round(bitmap.getWidth() * scale);
128        int height = Math.round(bitmap.getHeight() * scale);
129        if (width == bitmap.getWidth()
130                && height == bitmap.getHeight()) return bitmap;
131        Bitmap target = Bitmap.createBitmap(width, height, getConfig(bitmap));
132        Canvas canvas = new Canvas(target);
133        canvas.scale(scale, scale);
134        Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
135        canvas.drawBitmap(bitmap, 0, 0, paint);
136        if (recycle) bitmap.recycle();
137        return target;
138    }
139
140    private static Bitmap.Config getConfig(Bitmap bitmap) {
141        Bitmap.Config config = bitmap.getConfig();
142        if (config == null) {
143            config = Bitmap.Config.ARGB_8888;
144        }
145        return config;
146    }
147
148    public static Bitmap resizeDownBySideLength(
149            Bitmap bitmap, int maxLength, boolean recycle) {
150        int srcWidth = bitmap.getWidth();
151        int srcHeight = bitmap.getHeight();
152        float scale = Math.min(
153                (float) maxLength / srcWidth, (float) maxLength / srcHeight);
154        if (scale >= 1.0f) return bitmap;
155        return resizeBitmapByScale(bitmap, scale, recycle);
156    }
157
158    // Resize the bitmap if each side is >= targetSize * 2
159    public static Bitmap resizeDownIfTooBig(
160            Bitmap bitmap, int targetSize, boolean recycle) {
161        int srcWidth = bitmap.getWidth();
162        int srcHeight = bitmap.getHeight();
163        float scale = Math.max(
164                (float) targetSize / srcWidth, (float) targetSize / srcHeight);
165        if (scale > 0.5f) return bitmap;
166        return resizeBitmapByScale(bitmap, scale, recycle);
167    }
168
169    // Crops a square from the center of the original image.
170    public static Bitmap cropCenter(Bitmap bitmap, boolean recycle) {
171        int width = bitmap.getWidth();
172        int height = bitmap.getHeight();
173        if (width == height) return bitmap;
174        int size = Math.min(width, height);
175
176        Bitmap target = Bitmap.createBitmap(size, size, getConfig(bitmap));
177        Canvas canvas = new Canvas(target);
178        canvas.translate((size - width) / 2, (size - height) / 2);
179        Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
180        canvas.drawBitmap(bitmap, 0, 0, paint);
181        if (recycle) bitmap.recycle();
182        return target;
183    }
184
185    public static Bitmap resizeDownAndCropCenter(Bitmap bitmap, int size,
186            boolean recycle) {
187        int w = bitmap.getWidth();
188        int h = bitmap.getHeight();
189        int minSide = Math.min(w, h);
190        if (w == h && minSide <= size) return bitmap;
191        size = Math.min(size, minSide);
192
193        float scale = Math.max((float) size / bitmap.getWidth(),
194                (float) size / bitmap.getHeight());
195        Bitmap target = Bitmap.createBitmap(size, size, getConfig(bitmap));
196        int width = Math.round(scale * bitmap.getWidth());
197        int height = Math.round(scale * bitmap.getHeight());
198        Canvas canvas = new Canvas(target);
199        canvas.translate((size - width) / 2f, (size - height) / 2f);
200        canvas.scale(scale, scale);
201        Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
202        canvas.drawBitmap(bitmap, 0, 0, paint);
203        if (recycle) bitmap.recycle();
204        return target;
205    }
206
207    public static void recycleSilently(Bitmap bitmap) {
208        if (bitmap == null) return;
209        try {
210            bitmap.recycle();
211        } catch (Throwable t) {
212            Log.w(TAG, "unable recycle bitmap", t);
213        }
214    }
215
216    public static Bitmap rotateBitmap(Bitmap source, int rotation, boolean recycle) {
217        if (rotation == 0) return source;
218        int w = source.getWidth();
219        int h = source.getHeight();
220        Matrix m = new Matrix();
221        m.postRotate(rotation);
222        Bitmap bitmap = Bitmap.createBitmap(source, 0, 0, w, h, m, true);
223        if (recycle) source.recycle();
224        return bitmap;
225    }
226
227    public static Bitmap createVideoThumbnail(String filePath) {
228        // MediaMetadataRetriever is available on API Level 8
229        // but is hidden until API Level 10
230        Class<?> clazz = null;
231        Object instance = null;
232        try {
233            clazz = Class.forName("android.media.MediaMetadataRetriever");
234            instance = clazz.newInstance();
235
236            Method method = clazz.getMethod("setDataSource", String.class);
237            method.invoke(instance, filePath);
238
239            // The method name changes between API Level 9 and 10.
240            if (Build.VERSION.SDK_INT <= 9) {
241                return (Bitmap) clazz.getMethod("captureFrame").invoke(instance);
242            } else {
243                byte[] data = (byte[]) clazz.getMethod("getEmbeddedPicture").invoke(instance);
244                if (data != null) {
245                    Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
246                    if (bitmap != null) return bitmap;
247                }
248                return (Bitmap) clazz.getMethod("getFrameAtTime").invoke(instance);
249            }
250        } catch (IllegalArgumentException ex) {
251            // Assume this is a corrupt video file
252        } catch (RuntimeException ex) {
253            // Assume this is a corrupt video file.
254        } catch (InstantiationException e) {
255            Log.e(TAG, "createVideoThumbnail", e);
256        } catch (InvocationTargetException e) {
257            Log.e(TAG, "createVideoThumbnail", e);
258        } catch (ClassNotFoundException e) {
259            Log.e(TAG, "createVideoThumbnail", e);
260        } catch (NoSuchMethodException e) {
261            Log.e(TAG, "createVideoThumbnail", e);
262        } catch (IllegalAccessException e) {
263            Log.e(TAG, "createVideoThumbnail", e);
264        } finally {
265            try {
266                if (instance != null) {
267                    clazz.getMethod("release").invoke(instance);
268                }
269            } catch (Exception ignored) {
270            }
271        }
272        return null;
273    }
274
275    public static byte[] compressBitmap(Bitmap bitmap) {
276        ByteArrayOutputStream os = new ByteArrayOutputStream();
277        bitmap.compress(Bitmap.CompressFormat.JPEG,
278                COMPRESS_JPEG_QUALITY, os);
279        return os.toByteArray();
280    }
281
282    public static boolean isSupportedByRegionDecoder(String mimeType) {
283        if (mimeType == null) return false;
284        mimeType = mimeType.toLowerCase();
285        return mimeType.startsWith("image/") &&
286                (!mimeType.equals("image/gif") && !mimeType.endsWith("bmp"));
287    }
288
289    public static boolean isRotationSupported(String mimeType) {
290        if (mimeType == null) return false;
291        mimeType = mimeType.toLowerCase();
292        return mimeType.equals("image/jpeg");
293    }
294
295    public static byte[] compressToBytes(Bitmap bitmap, int quality) {
296        ByteArrayOutputStream baos = new ByteArrayOutputStream(65536);
297        bitmap.compress(CompressFormat.JPEG, quality, baos);
298        return baos.toByteArray();
299    }
300}
301