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 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