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 DEFAULT_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    // Find the min x that 1 / x >= scale
97    public static int computeSampleSizeLarger(float scale) {
98        int initialSize = (int) Math.floor(1d / 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 resizeBitmapByScale(
116            Bitmap bitmap, float scale, boolean recycle) {
117        int width = Math.round(bitmap.getWidth() * scale);
118        int height = Math.round(bitmap.getHeight() * scale);
119        if (width == bitmap.getWidth()
120                && height == bitmap.getHeight()) return bitmap;
121        Bitmap target = Bitmap.createBitmap(width, height, getConfig(bitmap));
122        Canvas canvas = new Canvas(target);
123        canvas.scale(scale, scale);
124        Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
125        canvas.drawBitmap(bitmap, 0, 0, paint);
126        if (recycle) bitmap.recycle();
127        return target;
128    }
129
130    private static Bitmap.Config getConfig(Bitmap bitmap) {
131        Bitmap.Config config = bitmap.getConfig();
132        if (config == null) {
133            config = Bitmap.Config.ARGB_8888;
134        }
135        return config;
136    }
137
138    public static Bitmap resizeDownBySideLength(
139            Bitmap bitmap, int maxLength, boolean recycle) {
140        int srcWidth = bitmap.getWidth();
141        int srcHeight = bitmap.getHeight();
142        float scale = Math.min(
143                (float) maxLength / srcWidth, (float) maxLength / srcHeight);
144        if (scale >= 1.0f) return bitmap;
145        return resizeBitmapByScale(bitmap, scale, recycle);
146    }
147
148    public static Bitmap resizeAndCropCenter(Bitmap bitmap, int size, boolean recycle) {
149        int w = bitmap.getWidth();
150        int h = bitmap.getHeight();
151        if (w == size && h == size) return bitmap;
152
153        // scale the image so that the shorter side equals to the target;
154        // the longer side will be center-cropped.
155        float scale = (float) size / Math.min(w,  h);
156
157        Bitmap target = Bitmap.createBitmap(size, size, getConfig(bitmap));
158        int width = Math.round(scale * bitmap.getWidth());
159        int height = Math.round(scale * bitmap.getHeight());
160        Canvas canvas = new Canvas(target);
161        canvas.translate((size - width) / 2f, (size - height) / 2f);
162        canvas.scale(scale, scale);
163        Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
164        canvas.drawBitmap(bitmap, 0, 0, paint);
165        if (recycle) bitmap.recycle();
166        return target;
167    }
168
169    public static void recycleSilently(Bitmap bitmap) {
170        if (bitmap == null) return;
171        try {
172            bitmap.recycle();
173        } catch (Throwable t) {
174            Log.w(TAG, "unable recycle bitmap", t);
175        }
176    }
177
178    public static Bitmap rotateBitmap(Bitmap source, int rotation, boolean recycle) {
179        if (rotation == 0) return source;
180        int w = source.getWidth();
181        int h = source.getHeight();
182        Matrix m = new Matrix();
183        m.postRotate(rotation);
184        Bitmap bitmap = Bitmap.createBitmap(source, 0, 0, w, h, m, true);
185        if (recycle) source.recycle();
186        return bitmap;
187    }
188
189    public static Bitmap createVideoThumbnail(String filePath) {
190        // MediaMetadataRetriever is available on API Level 8
191        // but is hidden until API Level 10
192        Class<?> clazz = null;
193        Object instance = null;
194        try {
195            clazz = Class.forName("android.media.MediaMetadataRetriever");
196            instance = clazz.newInstance();
197
198            Method method = clazz.getMethod("setDataSource", String.class);
199            method.invoke(instance, filePath);
200
201            // The method name changes between API Level 9 and 10.
202            if (Build.VERSION.SDK_INT <= 9) {
203                return (Bitmap) clazz.getMethod("captureFrame").invoke(instance);
204            } else {
205                byte[] data = (byte[]) clazz.getMethod("getEmbeddedPicture").invoke(instance);
206                if (data != null) {
207                    Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
208                    if (bitmap != null) return bitmap;
209                }
210                return (Bitmap) clazz.getMethod("getFrameAtTime").invoke(instance);
211            }
212        } catch (IllegalArgumentException ex) {
213            // Assume this is a corrupt video file
214        } catch (RuntimeException ex) {
215            // Assume this is a corrupt video file.
216        } catch (InstantiationException e) {
217            Log.e(TAG, "createVideoThumbnail", e);
218        } catch (InvocationTargetException e) {
219            Log.e(TAG, "createVideoThumbnail", e);
220        } catch (ClassNotFoundException e) {
221            Log.e(TAG, "createVideoThumbnail", e);
222        } catch (NoSuchMethodException e) {
223            Log.e(TAG, "createVideoThumbnail", e);
224        } catch (IllegalAccessException e) {
225            Log.e(TAG, "createVideoThumbnail", e);
226        } finally {
227            try {
228                if (instance != null) {
229                    clazz.getMethod("release").invoke(instance);
230                }
231            } catch (Exception ignored) {
232            }
233        }
234        return null;
235    }
236
237    public static byte[] compressToBytes(Bitmap bitmap) {
238        return compressToBytes(bitmap, DEFAULT_JPEG_QUALITY);
239    }
240
241    public static byte[] compressToBytes(Bitmap bitmap, int quality) {
242        ByteArrayOutputStream baos = new ByteArrayOutputStream(65536);
243        bitmap.compress(CompressFormat.JPEG, quality, baos);
244        return baos.toByteArray();
245    }
246
247    public static boolean isSupportedByRegionDecoder(String mimeType) {
248        if (mimeType == null) return false;
249        mimeType = mimeType.toLowerCase();
250        return mimeType.startsWith("image/") &&
251                (!mimeType.equals("image/gif") && !mimeType.endsWith("bmp"));
252    }
253
254    public static boolean isRotationSupported(String mimeType) {
255        if (mimeType == null) return false;
256        mimeType = mimeType.toLowerCase();
257        return mimeType.equals("image/jpeg");
258    }
259}
260