ThumbnailUtils.java revision 0ddcc8bc60ffa00e103f420f95b7f8a7c77639b6
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, or null on failures 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 } catch (OutOfMemoryError oom) { 127 Log.e(TAG, "Unable to decode file " + filePath + ". OutOfMemoryError.", oom); 128 } 129 } 130 131 if (kind == Images.Thumbnails.MICRO_KIND) { 132 // now we make it a "square thumbnail" for MICRO_KIND thumbnail 133 bitmap = extractThumbnail(bitmap, 134 TARGET_SIZE_MICRO_THUMBNAIL, 135 TARGET_SIZE_MICRO_THUMBNAIL, OPTIONS_RECYCLE_INPUT); 136 } 137 return bitmap; 138 } 139 140 /** 141 * Create a video thumbnail for a video. May return null if the video is 142 * corrupt or the format is not supported. 143 * 144 * @param filePath the path of video file 145 * @param kind could be MINI_KIND or MICRO_KIND 146 */ 147 public static Bitmap createVideoThumbnail(String filePath, int kind) { 148 Bitmap bitmap = null; 149 MediaMetadataRetriever retriever = new MediaMetadataRetriever(); 150 try { 151 retriever.setDataSource(filePath); 152 bitmap = retriever.getFrameAtTime(-1); 153 } catch (IllegalArgumentException ex) { 154 // Assume this is a corrupt video file 155 } catch (RuntimeException ex) { 156 // Assume this is a corrupt video file. 157 } finally { 158 try { 159 retriever.release(); 160 } catch (RuntimeException ex) { 161 // Ignore failures while cleaning up. 162 } 163 } 164 165 if (bitmap == null) return null; 166 167 if (kind == Images.Thumbnails.MINI_KIND) { 168 // Scale down the bitmap if it's too large. 169 int width = bitmap.getWidth(); 170 int height = bitmap.getHeight(); 171 int max = Math.max(width, height); 172 if (max > 512) { 173 float scale = 512f / max; 174 int w = Math.round(scale * width); 175 int h = Math.round(scale * height); 176 bitmap = Bitmap.createScaledBitmap(bitmap, w, h, true); 177 } 178 } else if (kind == Images.Thumbnails.MICRO_KIND) { 179 bitmap = extractThumbnail(bitmap, 180 TARGET_SIZE_MICRO_THUMBNAIL, 181 TARGET_SIZE_MICRO_THUMBNAIL, 182 OPTIONS_RECYCLE_INPUT); 183 } 184 return bitmap; 185 } 186 187 /** 188 * Creates a centered bitmap of the desired size. 189 * 190 * @param source original bitmap source 191 * @param width targeted width 192 * @param height targeted height 193 */ 194 public static Bitmap extractThumbnail( 195 Bitmap source, int width, int height) { 196 return extractThumbnail(source, width, height, OPTIONS_NONE); 197 } 198 199 /** 200 * Creates a centered bitmap of the desired size. 201 * 202 * @param source original bitmap source 203 * @param width targeted width 204 * @param height targeted height 205 * @param options options used during thumbnail extraction 206 */ 207 public static Bitmap extractThumbnail( 208 Bitmap source, int width, int height, int options) { 209 if (source == null) { 210 return null; 211 } 212 213 float scale; 214 if (source.getWidth() < source.getHeight()) { 215 scale = width / (float) source.getWidth(); 216 } else { 217 scale = height / (float) source.getHeight(); 218 } 219 Matrix matrix = new Matrix(); 220 matrix.setScale(scale, scale); 221 Bitmap thumbnail = transform(matrix, source, width, height, 222 OPTIONS_SCALE_UP | options); 223 return thumbnail; 224 } 225 226 /* 227 * Compute the sample size as a function of minSideLength 228 * and maxNumOfPixels. 229 * minSideLength is used to specify that minimal width or height of a 230 * bitmap. 231 * maxNumOfPixels is used to specify the maximal size in pixels that is 232 * tolerable in terms of memory usage. 233 * 234 * The function returns a sample size based on the constraints. 235 * Both size and minSideLength can be passed in as IImage.UNCONSTRAINED, 236 * which indicates no care of the corresponding constraint. 237 * The functions prefers returning a sample size that 238 * generates a smaller bitmap, unless minSideLength = IImage.UNCONSTRAINED. 239 * 240 * Also, the function rounds up the sample size to a power of 2 or multiple 241 * of 8 because BitmapFactory only honors sample size this way. 242 * For example, BitmapFactory downsamples an image by 2 even though the 243 * request is 3. So we round up the sample size to avoid OOM. 244 */ 245 private static int computeSampleSize(BitmapFactory.Options options, 246 int minSideLength, int maxNumOfPixels) { 247 int initialSize = computeInitialSampleSize(options, minSideLength, 248 maxNumOfPixels); 249 250 int roundedSize; 251 if (initialSize <= 8 ) { 252 roundedSize = 1; 253 while (roundedSize < initialSize) { 254 roundedSize <<= 1; 255 } 256 } else { 257 roundedSize = (initialSize + 7) / 8 * 8; 258 } 259 260 return roundedSize; 261 } 262 263 private static int computeInitialSampleSize(BitmapFactory.Options options, 264 int minSideLength, int maxNumOfPixels) { 265 double w = options.outWidth; 266 double h = options.outHeight; 267 268 int lowerBound = (maxNumOfPixels == UNCONSTRAINED) ? 1 : 269 (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels)); 270 int upperBound = (minSideLength == UNCONSTRAINED) ? 128 : 271 (int) Math.min(Math.floor(w / minSideLength), 272 Math.floor(h / minSideLength)); 273 274 if (upperBound < lowerBound) { 275 // return the larger one when there is no overlapping zone. 276 return lowerBound; 277 } 278 279 if ((maxNumOfPixels == UNCONSTRAINED) && 280 (minSideLength == UNCONSTRAINED)) { 281 return 1; 282 } else if (minSideLength == UNCONSTRAINED) { 283 return lowerBound; 284 } else { 285 return upperBound; 286 } 287 } 288 289 /** 290 * Make a bitmap from a given Uri, minimal side length, and maximum number of pixels. 291 * The image data will be read from specified pfd if it's not null, otherwise 292 * a new input stream will be created using specified ContentResolver. 293 * 294 * Clients are allowed to pass their own BitmapFactory.Options used for bitmap decoding. A 295 * new BitmapFactory.Options will be created if options is null. 296 */ 297 private static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels, 298 Uri uri, ContentResolver cr, ParcelFileDescriptor pfd, 299 BitmapFactory.Options options) { 300 Bitmap b = null; 301 try { 302 if (pfd == null) pfd = makeInputStream(uri, cr); 303 if (pfd == null) return null; 304 if (options == null) options = new BitmapFactory.Options(); 305 306 FileDescriptor fd = pfd.getFileDescriptor(); 307 options.inSampleSize = 1; 308 options.inJustDecodeBounds = true; 309 BitmapFactory.decodeFileDescriptor(fd, null, options); 310 if (options.mCancel || options.outWidth == -1 311 || options.outHeight == -1) { 312 return null; 313 } 314 options.inSampleSize = computeSampleSize( 315 options, minSideLength, maxNumOfPixels); 316 options.inJustDecodeBounds = false; 317 318 options.inDither = false; 319 options.inPreferredConfig = Bitmap.Config.ARGB_8888; 320 b = BitmapFactory.decodeFileDescriptor(fd, null, options); 321 } catch (OutOfMemoryError ex) { 322 Log.e(TAG, "Got oom exception ", ex); 323 return null; 324 } finally { 325 closeSilently(pfd); 326 } 327 return b; 328 } 329 330 private static void closeSilently(ParcelFileDescriptor c) { 331 if (c == null) return; 332 try { 333 c.close(); 334 } catch (Throwable t) { 335 // do nothing 336 } 337 } 338 339 private static ParcelFileDescriptor makeInputStream( 340 Uri uri, ContentResolver cr) { 341 try { 342 return cr.openFileDescriptor(uri, "r"); 343 } catch (IOException ex) { 344 return null; 345 } 346 } 347 348 /** 349 * Transform source Bitmap to targeted width and height. 350 */ 351 private static Bitmap transform(Matrix scaler, 352 Bitmap source, 353 int targetWidth, 354 int targetHeight, 355 int options) { 356 boolean scaleUp = (options & OPTIONS_SCALE_UP) != 0; 357 boolean recycle = (options & OPTIONS_RECYCLE_INPUT) != 0; 358 359 int deltaX = source.getWidth() - targetWidth; 360 int deltaY = source.getHeight() - targetHeight; 361 if (!scaleUp && (deltaX < 0 || deltaY < 0)) { 362 /* 363 * In this case the bitmap is smaller, at least in one dimension, 364 * than the target. Transform it by placing as much of the image 365 * as possible into the target and leaving the top/bottom or 366 * left/right (or both) black. 367 */ 368 Bitmap b2 = Bitmap.createBitmap(targetWidth, targetHeight, 369 Bitmap.Config.ARGB_8888); 370 Canvas c = new Canvas(b2); 371 372 int deltaXHalf = Math.max(0, deltaX / 2); 373 int deltaYHalf = Math.max(0, deltaY / 2); 374 Rect src = new Rect( 375 deltaXHalf, 376 deltaYHalf, 377 deltaXHalf + Math.min(targetWidth, source.getWidth()), 378 deltaYHalf + Math.min(targetHeight, source.getHeight())); 379 int dstX = (targetWidth - src.width()) / 2; 380 int dstY = (targetHeight - src.height()) / 2; 381 Rect dst = new Rect( 382 dstX, 383 dstY, 384 targetWidth - dstX, 385 targetHeight - dstY); 386 c.drawBitmap(source, src, dst, null); 387 if (recycle) { 388 source.recycle(); 389 } 390 return b2; 391 } 392 float bitmapWidthF = source.getWidth(); 393 float bitmapHeightF = source.getHeight(); 394 395 float bitmapAspect = bitmapWidthF / bitmapHeightF; 396 float viewAspect = (float) targetWidth / targetHeight; 397 398 if (bitmapAspect > viewAspect) { 399 float scale = targetHeight / bitmapHeightF; 400 if (scale < .9F || scale > 1F) { 401 scaler.setScale(scale, scale); 402 } else { 403 scaler = null; 404 } 405 } else { 406 float scale = targetWidth / bitmapWidthF; 407 if (scale < .9F || scale > 1F) { 408 scaler.setScale(scale, scale); 409 } else { 410 scaler = null; 411 } 412 } 413 414 Bitmap b1; 415 if (scaler != null) { 416 // this is used for minithumb and crop, so we want to filter here. 417 b1 = Bitmap.createBitmap(source, 0, 0, 418 source.getWidth(), source.getHeight(), scaler, true); 419 } else { 420 b1 = source; 421 } 422 423 if (recycle && b1 != source) { 424 source.recycle(); 425 } 426 427 int dx1 = Math.max(0, b1.getWidth() - targetWidth); 428 int dy1 = Math.max(0, b1.getHeight() - targetHeight); 429 430 Bitmap b2 = Bitmap.createBitmap( 431 b1, 432 dx1 / 2, 433 dy1 / 2, 434 targetWidth, 435 targetHeight); 436 437 if (b2 != b1) { 438 if (recycle || b1 != source) { 439 b1.recycle(); 440 } 441 } 442 443 return b2; 444 } 445 446 /** 447 * SizedThumbnailBitmap contains the bitmap, which is downsampled either from 448 * the thumbnail in exif or the full image. 449 * mThumbnailData, mThumbnailWidth and mThumbnailHeight are set together only if mThumbnail 450 * is not null. 451 * 452 * The width/height of the sized bitmap may be different from mThumbnailWidth/mThumbnailHeight. 453 */ 454 private static class SizedThumbnailBitmap { 455 public byte[] mThumbnailData; 456 public Bitmap mBitmap; 457 public int mThumbnailWidth; 458 public int mThumbnailHeight; 459 } 460 461 /** 462 * Creates a bitmap by either downsampling from the thumbnail in EXIF or the full image. 463 * The functions returns a SizedThumbnailBitmap, 464 * which contains a downsampled bitmap and the thumbnail data in EXIF if exists. 465 */ 466 private static void createThumbnailFromEXIF(String filePath, int targetSize, 467 int maxPixels, SizedThumbnailBitmap sizedThumbBitmap) { 468 if (filePath == null) return; 469 470 ExifInterface exif = null; 471 byte [] thumbData = null; 472 try { 473 exif = new ExifInterface(filePath); 474 if (exif != null) { 475 thumbData = exif.getThumbnail(); 476 } 477 } catch (IOException ex) { 478 Log.w(TAG, ex); 479 } 480 481 BitmapFactory.Options fullOptions = new BitmapFactory.Options(); 482 BitmapFactory.Options exifOptions = new BitmapFactory.Options(); 483 int exifThumbWidth = 0; 484 int fullThumbWidth = 0; 485 486 // Compute exifThumbWidth. 487 if (thumbData != null) { 488 exifOptions.inJustDecodeBounds = true; 489 BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, exifOptions); 490 exifOptions.inSampleSize = computeSampleSize(exifOptions, targetSize, maxPixels); 491 exifThumbWidth = exifOptions.outWidth / exifOptions.inSampleSize; 492 } 493 494 // Compute fullThumbWidth. 495 fullOptions.inJustDecodeBounds = true; 496 BitmapFactory.decodeFile(filePath, fullOptions); 497 fullOptions.inSampleSize = computeSampleSize(fullOptions, targetSize, maxPixels); 498 fullThumbWidth = fullOptions.outWidth / fullOptions.inSampleSize; 499 500 // Choose the larger thumbnail as the returning sizedThumbBitmap. 501 if (thumbData != null && exifThumbWidth >= fullThumbWidth) { 502 int width = exifOptions.outWidth; 503 int height = exifOptions.outHeight; 504 exifOptions.inJustDecodeBounds = false; 505 sizedThumbBitmap.mBitmap = BitmapFactory.decodeByteArray(thumbData, 0, 506 thumbData.length, exifOptions); 507 if (sizedThumbBitmap.mBitmap != null) { 508 sizedThumbBitmap.mThumbnailData = thumbData; 509 sizedThumbBitmap.mThumbnailWidth = width; 510 sizedThumbBitmap.mThumbnailHeight = height; 511 } 512 } else { 513 fullOptions.inJustDecodeBounds = false; 514 sizedThumbBitmap.mBitmap = BitmapFactory.decodeFile(filePath, fullOptions); 515 } 516 } 517} 518