ThumbnailUtils.java revision 9efe47374b61afd0ce84afa64e9fa5b41dfaef22
1/* 2 * Copyright (C) 2009 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 android.media; 18 19import android.content.ContentResolver; 20import android.content.ContentUris; 21import android.content.ContentValues; 22import android.database.Cursor; 23import android.graphics.Bitmap; 24import android.graphics.BitmapFactory; 25import android.graphics.Canvas; 26import android.graphics.Matrix; 27import android.graphics.Rect; 28import android.media.MediaMetadataRetriever; 29import android.media.MediaFile.MediaFileType; 30import android.net.Uri; 31import android.os.ParcelFileDescriptor; 32import android.provider.BaseColumns; 33import android.provider.MediaStore.Images; 34import android.provider.MediaStore.Images.Thumbnails; 35import android.util.Log; 36 37import java.io.FileInputStream; 38import java.io.FileDescriptor; 39import java.io.IOException; 40import java.io.OutputStream; 41 42/** 43 * Thumbnail generation routines for media provider. 44 */ 45 46public class ThumbnailUtils { 47 private static final String TAG = "ThumbnailUtils"; 48 49 /* Maximum pixels size for created bitmap. */ 50 private static final int MAX_NUM_PIXELS_THUMBNAIL = 512 * 384; 51 private static final int MAX_NUM_PIXELS_MICRO_THUMBNAIL = 128 * 128; 52 private static final int UNCONSTRAINED = -1; 53 54 /* Options used internally. */ 55 private static final int OPTIONS_NONE = 0x0; 56 private static final int OPTIONS_SCALE_UP = 0x1; 57 58 /** 59 * Constant used to indicate we should recycle the input in 60 * {@link #extractThumbnail(Bitmap, int, int, int)} unless the output is the input. 61 */ 62 public static final int OPTIONS_RECYCLE_INPUT = 0x2; 63 64 /** 65 * Constant used to indicate the dimension of mini thumbnail. 66 * @hide Only used by media framework and media provider internally. 67 */ 68 public static final int TARGET_SIZE_MINI_THUMBNAIL = 320; 69 70 /** 71 * Constant used to indicate the dimension of micro thumbnail. 72 * @hide Only used by media framework and media provider internally. 73 */ 74 public static final int TARGET_SIZE_MICRO_THUMBNAIL = 96; 75 76 /** 77 * This method first examines if the thumbnail embedded in EXIF is bigger than our target 78 * size. If not, then it'll create a thumbnail from original image. Due to efficiency 79 * consideration, we want to let MediaThumbRequest avoid calling this method twice for 80 * both kinds, so it only requests for MICRO_KIND and set saveImage to true. 81 * 82 * This method always returns a "square thumbnail" for MICRO_KIND thumbnail. 83 * 84 * @param filePath the path of image file 85 * @param kind could be MINI_KIND or MICRO_KIND 86 * @return Bitmap 87 * 88 * @hide This method is only used by media framework and media provider internally. 89 */ 90 public static Bitmap createImageThumbnail(String filePath, int kind) { 91 boolean wantMini = (kind == Images.Thumbnails.MINI_KIND); 92 int targetSize = wantMini 93 ? TARGET_SIZE_MINI_THUMBNAIL 94 : TARGET_SIZE_MICRO_THUMBNAIL; 95 int maxPixels = wantMini 96 ? MAX_NUM_PIXELS_THUMBNAIL 97 : MAX_NUM_PIXELS_MICRO_THUMBNAIL; 98 SizedThumbnailBitmap sizedThumbnailBitmap = new SizedThumbnailBitmap(); 99 Bitmap bitmap = null; 100 MediaFileType fileType = MediaFile.getFileType(filePath); 101 if (fileType != null && fileType.fileType == MediaFile.FILE_TYPE_JPEG) { 102 createThumbnailFromEXIF(filePath, targetSize, maxPixels, sizedThumbnailBitmap); 103 bitmap = sizedThumbnailBitmap.mBitmap; 104 } 105 106 if (bitmap == null) { 107 try { 108 FileDescriptor fd = new FileInputStream(filePath).getFD(); 109 BitmapFactory.Options options = new BitmapFactory.Options(); 110 options.inSampleSize = 1; 111 options.inJustDecodeBounds = true; 112 BitmapFactory.decodeFileDescriptor(fd, null, options); 113 if (options.mCancel || options.outWidth == -1 114 || options.outHeight == -1) { 115 return null; 116 } 117 options.inSampleSize = computeSampleSize( 118 options, targetSize, maxPixels); 119 options.inJustDecodeBounds = false; 120 121 options.inDither = false; 122 options.inPreferredConfig = Bitmap.Config.ARGB_8888; 123 bitmap = BitmapFactory.decodeFileDescriptor(fd, null, options); 124 } catch (IOException ex) { 125 Log.e(TAG, "", ex); 126 } 127 } 128 129 if (kind == Images.Thumbnails.MICRO_KIND) { 130 // now we make it a "square thumbnail" for MICRO_KIND thumbnail 131 bitmap = extractThumbnail(bitmap, 132 TARGET_SIZE_MICRO_THUMBNAIL, 133 TARGET_SIZE_MICRO_THUMBNAIL, OPTIONS_RECYCLE_INPUT); 134 } 135 return bitmap; 136 } 137 138 /** 139 * Create a video thumbnail for a video. May return null if the video is 140 * corrupt or the format is not supported. 141 * 142 * @param filePath the path of video file 143 * @param kind could be MINI_KIND or MICRO_KIND 144 */ 145 public static Bitmap createVideoThumbnail(String filePath, int kind) { 146 Bitmap bitmap = null; 147 MediaMetadataRetriever retriever = new MediaMetadataRetriever(); 148 try { 149 retriever.setDataSource(filePath); 150 bitmap = retriever.getFrameAtTime(-1); 151 } catch (IllegalArgumentException ex) { 152 // Assume this is a corrupt video file 153 } catch (RuntimeException ex) { 154 // Assume this is a corrupt video file. 155 } finally { 156 try { 157 retriever.release(); 158 } catch (RuntimeException ex) { 159 // Ignore failures while cleaning up. 160 } 161 } 162 if (kind == Images.Thumbnails.MICRO_KIND && bitmap != null) { 163 bitmap = extractThumbnail(bitmap, 164 TARGET_SIZE_MICRO_THUMBNAIL, 165 TARGET_SIZE_MICRO_THUMBNAIL, 166 OPTIONS_RECYCLE_INPUT); 167 } 168 return bitmap; 169 } 170 171 /** 172 * Creates a centered bitmap of the desired size. 173 * 174 * @param source original bitmap source 175 * @param width targeted width 176 * @param height targeted height 177 */ 178 public static Bitmap extractThumbnail( 179 Bitmap source, int width, int height) { 180 return extractThumbnail(source, width, height, OPTIONS_NONE); 181 } 182 183 /** 184 * Creates a centered bitmap of the desired size. 185 * 186 * @param source original bitmap source 187 * @param width targeted width 188 * @param height targeted height 189 * @param options options used during thumbnail extraction 190 */ 191 public static Bitmap extractThumbnail( 192 Bitmap source, int width, int height, int options) { 193 if (source == null) { 194 return null; 195 } 196 197 float scale; 198 if (source.getWidth() < source.getHeight()) { 199 scale = width / (float) source.getWidth(); 200 } else { 201 scale = height / (float) source.getHeight(); 202 } 203 Matrix matrix = new Matrix(); 204 matrix.setScale(scale, scale); 205 Bitmap thumbnail = transform(matrix, source, width, height, 206 OPTIONS_SCALE_UP | options); 207 return thumbnail; 208 } 209 210 /* 211 * Compute the sample size as a function of minSideLength 212 * and maxNumOfPixels. 213 * minSideLength is used to specify that minimal width or height of a 214 * bitmap. 215 * maxNumOfPixels is used to specify the maximal size in pixels that is 216 * tolerable in terms of memory usage. 217 * 218 * The function returns a sample size based on the constraints. 219 * Both size and minSideLength can be passed in as IImage.UNCONSTRAINED, 220 * which indicates no care of the corresponding constraint. 221 * The functions prefers returning a sample size that 222 * generates a smaller bitmap, unless minSideLength = IImage.UNCONSTRAINED. 223 * 224 * Also, the function rounds up the sample size to a power of 2 or multiple 225 * of 8 because BitmapFactory only honors sample size this way. 226 * For example, BitmapFactory downsamples an image by 2 even though the 227 * request is 3. So we round up the sample size to avoid OOM. 228 */ 229 private static int computeSampleSize(BitmapFactory.Options options, 230 int minSideLength, int maxNumOfPixels) { 231 int initialSize = computeInitialSampleSize(options, minSideLength, 232 maxNumOfPixels); 233 234 int roundedSize; 235 if (initialSize <= 8 ) { 236 roundedSize = 1; 237 while (roundedSize < initialSize) { 238 roundedSize <<= 1; 239 } 240 } else { 241 roundedSize = (initialSize + 7) / 8 * 8; 242 } 243 244 return roundedSize; 245 } 246 247 private static int computeInitialSampleSize(BitmapFactory.Options options, 248 int minSideLength, int maxNumOfPixels) { 249 double w = options.outWidth; 250 double h = options.outHeight; 251 252 int lowerBound = (maxNumOfPixels == UNCONSTRAINED) ? 1 : 253 (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels)); 254 int upperBound = (minSideLength == UNCONSTRAINED) ? 128 : 255 (int) Math.min(Math.floor(w / minSideLength), 256 Math.floor(h / minSideLength)); 257 258 if (upperBound < lowerBound) { 259 // return the larger one when there is no overlapping zone. 260 return lowerBound; 261 } 262 263 if ((maxNumOfPixels == UNCONSTRAINED) && 264 (minSideLength == UNCONSTRAINED)) { 265 return 1; 266 } else if (minSideLength == UNCONSTRAINED) { 267 return lowerBound; 268 } else { 269 return upperBound; 270 } 271 } 272 273 /** 274 * Make a bitmap from a given Uri, minimal side length, and maximum number of pixels. 275 * The image data will be read from specified pfd if it's not null, otherwise 276 * a new input stream will be created using specified ContentResolver. 277 * 278 * Clients are allowed to pass their own BitmapFactory.Options used for bitmap decoding. A 279 * new BitmapFactory.Options will be created if options is null. 280 */ 281 private static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels, 282 Uri uri, ContentResolver cr, ParcelFileDescriptor pfd, 283 BitmapFactory.Options options) { 284 Bitmap b = null; 285 try { 286 if (pfd == null) pfd = makeInputStream(uri, cr); 287 if (pfd == null) return null; 288 if (options == null) options = new BitmapFactory.Options(); 289 290 FileDescriptor fd = pfd.getFileDescriptor(); 291 options.inSampleSize = 1; 292 options.inJustDecodeBounds = true; 293 BitmapFactory.decodeFileDescriptor(fd, null, options); 294 if (options.mCancel || options.outWidth == -1 295 || options.outHeight == -1) { 296 return null; 297 } 298 options.inSampleSize = computeSampleSize( 299 options, minSideLength, maxNumOfPixels); 300 options.inJustDecodeBounds = false; 301 302 options.inDither = false; 303 options.inPreferredConfig = Bitmap.Config.ARGB_8888; 304 b = BitmapFactory.decodeFileDescriptor(fd, null, options); 305 } catch (OutOfMemoryError ex) { 306 Log.e(TAG, "Got oom exception ", ex); 307 return null; 308 } finally { 309 closeSilently(pfd); 310 } 311 return b; 312 } 313 314 private static void closeSilently(ParcelFileDescriptor c) { 315 if (c == null) return; 316 try { 317 c.close(); 318 } catch (Throwable t) { 319 // do nothing 320 } 321 } 322 323 private static ParcelFileDescriptor makeInputStream( 324 Uri uri, ContentResolver cr) { 325 try { 326 return cr.openFileDescriptor(uri, "r"); 327 } catch (IOException ex) { 328 return null; 329 } 330 } 331 332 /** 333 * Transform source Bitmap to targeted width and height. 334 */ 335 private static Bitmap transform(Matrix scaler, 336 Bitmap source, 337 int targetWidth, 338 int targetHeight, 339 int options) { 340 boolean scaleUp = (options & OPTIONS_SCALE_UP) != 0; 341 boolean recycle = (options & OPTIONS_RECYCLE_INPUT) != 0; 342 343 int deltaX = source.getWidth() - targetWidth; 344 int deltaY = source.getHeight() - targetHeight; 345 if (!scaleUp && (deltaX < 0 || deltaY < 0)) { 346 /* 347 * In this case the bitmap is smaller, at least in one dimension, 348 * than the target. Transform it by placing as much of the image 349 * as possible into the target and leaving the top/bottom or 350 * left/right (or both) black. 351 */ 352 Bitmap b2 = Bitmap.createBitmap(targetWidth, targetHeight, 353 Bitmap.Config.ARGB_8888); 354 Canvas c = new Canvas(b2); 355 356 int deltaXHalf = Math.max(0, deltaX / 2); 357 int deltaYHalf = Math.max(0, deltaY / 2); 358 Rect src = new Rect( 359 deltaXHalf, 360 deltaYHalf, 361 deltaXHalf + Math.min(targetWidth, source.getWidth()), 362 deltaYHalf + Math.min(targetHeight, source.getHeight())); 363 int dstX = (targetWidth - src.width()) / 2; 364 int dstY = (targetHeight - src.height()) / 2; 365 Rect dst = new Rect( 366 dstX, 367 dstY, 368 targetWidth - dstX, 369 targetHeight - dstY); 370 c.drawBitmap(source, src, dst, null); 371 if (recycle) { 372 source.recycle(); 373 } 374 return b2; 375 } 376 float bitmapWidthF = source.getWidth(); 377 float bitmapHeightF = source.getHeight(); 378 379 float bitmapAspect = bitmapWidthF / bitmapHeightF; 380 float viewAspect = (float) targetWidth / targetHeight; 381 382 if (bitmapAspect > viewAspect) { 383 float scale = targetHeight / bitmapHeightF; 384 if (scale < .9F || scale > 1F) { 385 scaler.setScale(scale, scale); 386 } else { 387 scaler = null; 388 } 389 } else { 390 float scale = targetWidth / bitmapWidthF; 391 if (scale < .9F || scale > 1F) { 392 scaler.setScale(scale, scale); 393 } else { 394 scaler = null; 395 } 396 } 397 398 Bitmap b1; 399 if (scaler != null) { 400 // this is used for minithumb and crop, so we want to filter here. 401 b1 = Bitmap.createBitmap(source, 0, 0, 402 source.getWidth(), source.getHeight(), scaler, true); 403 } else { 404 b1 = source; 405 } 406 407 if (recycle && b1 != source) { 408 source.recycle(); 409 } 410 411 int dx1 = Math.max(0, b1.getWidth() - targetWidth); 412 int dy1 = Math.max(0, b1.getHeight() - targetHeight); 413 414 Bitmap b2 = Bitmap.createBitmap( 415 b1, 416 dx1 / 2, 417 dy1 / 2, 418 targetWidth, 419 targetHeight); 420 421 if (b2 != b1) { 422 if (recycle || b1 != source) { 423 b1.recycle(); 424 } 425 } 426 427 return b2; 428 } 429 430 /** 431 * SizedThumbnailBitmap contains the bitmap, which is downsampled either from 432 * the thumbnail in exif or the full image. 433 * mThumbnailData, mThumbnailWidth and mThumbnailHeight are set together only if mThumbnail 434 * is not null. 435 * 436 * The width/height of the sized bitmap may be different from mThumbnailWidth/mThumbnailHeight. 437 */ 438 private static class SizedThumbnailBitmap { 439 public byte[] mThumbnailData; 440 public Bitmap mBitmap; 441 public int mThumbnailWidth; 442 public int mThumbnailHeight; 443 } 444 445 /** 446 * Creates a bitmap by either downsampling from the thumbnail in EXIF or the full image. 447 * The functions returns a SizedThumbnailBitmap, 448 * which contains a downsampled bitmap and the thumbnail data in EXIF if exists. 449 */ 450 private static void createThumbnailFromEXIF(String filePath, int targetSize, 451 int maxPixels, SizedThumbnailBitmap sizedThumbBitmap) { 452 if (filePath == null) return; 453 454 ExifInterface exif = null; 455 byte [] thumbData = null; 456 try { 457 exif = new ExifInterface(filePath); 458 if (exif != null) { 459 thumbData = exif.getThumbnail(); 460 } 461 } catch (IOException ex) { 462 Log.w(TAG, ex); 463 } 464 465 BitmapFactory.Options fullOptions = new BitmapFactory.Options(); 466 BitmapFactory.Options exifOptions = new BitmapFactory.Options(); 467 int exifThumbWidth = 0; 468 int fullThumbWidth = 0; 469 470 // Compute exifThumbWidth. 471 if (thumbData != null) { 472 exifOptions.inJustDecodeBounds = true; 473 BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, exifOptions); 474 exifOptions.inSampleSize = computeSampleSize(exifOptions, targetSize, maxPixels); 475 exifThumbWidth = exifOptions.outWidth / exifOptions.inSampleSize; 476 } 477 478 // Compute fullThumbWidth. 479 fullOptions.inJustDecodeBounds = true; 480 BitmapFactory.decodeFile(filePath, fullOptions); 481 fullOptions.inSampleSize = computeSampleSize(fullOptions, targetSize, maxPixels); 482 fullThumbWidth = fullOptions.outWidth / fullOptions.inSampleSize; 483 484 // Choose the larger thumbnail as the returning sizedThumbBitmap. 485 if (thumbData != null && exifThumbWidth >= fullThumbWidth) { 486 int width = exifOptions.outWidth; 487 int height = exifOptions.outHeight; 488 exifOptions.inJustDecodeBounds = false; 489 sizedThumbBitmap.mBitmap = BitmapFactory.decodeByteArray(thumbData, 0, 490 thumbData.length, exifOptions); 491 if (sizedThumbBitmap.mBitmap != null) { 492 sizedThumbBitmap.mThumbnailData = thumbData; 493 sizedThumbBitmap.mThumbnailWidth = width; 494 sizedThumbBitmap.mThumbnailHeight = height; 495 } 496 } else { 497 fullOptions.inJustDecodeBounds = false; 498 sizedThumbBitmap.mBitmap = BitmapFactory.decodeFile(filePath, fullOptions); 499 } 500 } 501} 502