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