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.Log;
27 
28 import java.io.ByteArrayOutputStream;
29 import java.lang.reflect.InvocationTargetException;
30 import java.lang.reflect.Method;
31 
32 public 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 
BitmapUtils()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      */
computeSampleSize(int width, int height, int minSideLength, int maxNumOfPixels)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 
computeInitialSampleSize(int w, int h, int minSideLength, int maxNumOfPixels)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.
computeSampleSizeLarger(int w, int h, int minSideLength)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
computeSampleSizeLarger(float scale)97     public static int computeSampleSizeLarger(float scale) {
98         int initialSize = (int) Math.floor(1 / 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.
computeSampleSize(float 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 
resizeBitmapByScale( Bitmap bitmap, float scale, boolean recycle)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 
getConfig(Bitmap bitmap)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 
resizeDownBySideLength( Bitmap bitmap, int maxLength, boolean recycle)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 
resizeAndCropCenter(Bitmap bitmap, int size, boolean recycle)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 
recycleSilently(Bitmap bitmap)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 
rotateBitmap(Bitmap source, int rotation, boolean recycle)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 
createVideoThumbnail(String filePath)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 
compressToBytes(Bitmap bitmap)237     public static byte[] compressToBytes(Bitmap bitmap) {
238         return compressToBytes(bitmap, DEFAULT_JPEG_QUALITY);
239     }
240 
compressToBytes(Bitmap bitmap, int quality)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 
isSupportedByRegionDecoder(String mimeType)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 
isRotationSupported(String mimeType)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