ThumbnailUtils.java revision 49ffc0ff72a29f000b56deb34b0706cbfc5624bf
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.FileDescriptor; 38import java.io.IOException; 39import java.io.OutputStream; 40 41/** 42 * Thumbnail generation routines for media provider. 43 */ 44 45public class ThumbnailUtils { 46 private static final String TAG = "ThumbnailUtils"; 47 48 /* Maximum pixels size for created bitmap. */ 49 private static final int MAX_NUM_PIXELS_THUMBNAIL = 512 * 384; 50 private static final int MAX_NUM_PIXELS_MICRO_THUMBNAIL = 128 * 128; 51 private static final int UNCONSTRAINED = -1; 52 53 /* Options used internally. */ 54 private static final int OPTIONS_NONE = 0x0; 55 private static final int OPTIONS_DO_NOT_USE_NATIVE = 0x1; 56 private static final int OPTIONS_SCALE_UP = 0x2; 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 = 0x4; 63 64 /** 65 * Constant used to indicate the dimension of normal thumbnail in 66 * {@link #extractThumbnail(Bitmap, int, int, int)}. 67 */ 68 public static final int TARGET_SIZE_NORMAL_THUMBNAIL = 320; 69 70 /** 71 * Constant used to indicate the dimension of micro thumbnail in 72 * {@link #extractThumbnail(Bitmap, int, int, int)}. 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 cr ContentResolver 85 * @param filePath file path needed by EXIF interface 86 * @param uri URI of original image 87 * @param origId image id 88 * @param kind either MINI_KIND or MICRO_KIND 89 * @param saveMini Whether to save MINI_KIND thumbnail obtained in this method. 90 * @return Bitmap 91 * 92 * @hide This method is only used by media framework and media provider internally. 93 */ 94 public static Bitmap createImageThumbnail(ContentResolver cr, String filePath, Uri uri, 95 long origId, int kind, boolean saveMini) { 96 boolean wantMini = (kind == Images.Thumbnails.MINI_KIND || saveMini); 97 int targetSize = wantMini ? 98 TARGET_SIZE_NORMAL_THUMBNAIL : TARGET_SIZE_MICRO_THUMBNAIL; 99 int maxPixels = wantMini ? 100 MAX_NUM_PIXELS_THUMBNAIL : MAX_NUM_PIXELS_MICRO_THUMBNAIL; 101 SizedThumbnailBitmap sizedThumbnailBitmap = new SizedThumbnailBitmap(); 102 Bitmap bitmap = null; 103 MediaFileType fileType = MediaFile.getFileType(filePath); 104 if (fileType != null && fileType.fileType == MediaFile.FILE_TYPE_JPEG) { 105 createThumbnailFromEXIF(filePath, targetSize, maxPixels, sizedThumbnailBitmap); 106 bitmap = sizedThumbnailBitmap.mBitmap; 107 } 108 109 if (bitmap == null) { 110 bitmap = makeBitmap(targetSize, maxPixels, uri, cr); 111 } 112 113 if (bitmap == null) { 114 return null; 115 } 116 117 if (saveMini) { 118 if (sizedThumbnailBitmap.mThumbnailData != null) { 119 storeThumbnail(cr, origId, 120 sizedThumbnailBitmap.mThumbnailData, 121 sizedThumbnailBitmap.mThumbnailWidth, 122 sizedThumbnailBitmap.mThumbnailHeight); 123 } else { 124 storeThumbnail(cr, origId, bitmap); 125 } 126 } 127 128 if (kind == Images.Thumbnails.MICRO_KIND) { 129 // now we make it a "square thumbnail" for MICRO_KIND thumbnail 130 bitmap = extractThumbnail(bitmap, 131 TARGET_SIZE_MICRO_THUMBNAIL, 132 TARGET_SIZE_MICRO_THUMBNAIL, OPTIONS_RECYCLE_INPUT); 133 } 134 return bitmap; 135 } 136 137 /** 138 * Create a video thumbnail for a video. May return null if the video is 139 * corrupt or the format is not supported. 140 * 141 * @param filePath 142 */ 143 public static Bitmap createVideoThumbnail(String filePath) { 144 Bitmap bitmap = null; 145 MediaMetadataRetriever retriever = new MediaMetadataRetriever(); 146 try { 147 retriever.setMode(MediaMetadataRetriever.MODE_CAPTURE_FRAME_ONLY); 148 retriever.setDataSource(filePath); 149 bitmap = retriever.captureFrame(); 150 } catch (IllegalArgumentException ex) { 151 // Assume this is a corrupt video file 152 } catch (RuntimeException ex) { 153 // Assume this is a corrupt video file. 154 } finally { 155 try { 156 retriever.release(); 157 } catch (RuntimeException ex) { 158 // Ignore failures while cleaning up. 159 } 160 } 161 return bitmap; 162 } 163 164 /** 165 * Creates a centered bitmap of the desired size. 166 * 167 * @param source original bitmap source 168 * @param width targeted width 169 * @param height targeted height 170 */ 171 public static Bitmap extractThumbnail( 172 Bitmap source, int width, int height) { 173 return extractThumbnail(source, width, height, OPTIONS_NONE); 174 } 175 176 /** 177 * Creates a centered bitmap of the desired size. 178 * 179 * @param source original bitmap source 180 * @param width targeted width 181 * @param height targeted height 182 * @param options options used during thumbnail extraction 183 */ 184 public static Bitmap extractThumbnail( 185 Bitmap source, int width, int height, int options) { 186 if (source == null) { 187 return null; 188 } 189 190 float scale; 191 if (source.getWidth() < source.getHeight()) { 192 scale = width / (float) source.getWidth(); 193 } else { 194 scale = height / (float) source.getHeight(); 195 } 196 Matrix matrix = new Matrix(); 197 matrix.setScale(scale, scale); 198 Bitmap thumbnail = transform(matrix, source, width, height, 199 OPTIONS_SCALE_UP | options); 200 return thumbnail; 201 } 202 203 /* 204 * Compute the sample size as a function of minSideLength 205 * and maxNumOfPixels. 206 * minSideLength is used to specify that minimal width or height of a 207 * bitmap. 208 * maxNumOfPixels is used to specify the maximal size in pixels that is 209 * tolerable in terms of memory usage. 210 * 211 * The function returns a sample size based on the constraints. 212 * Both size and minSideLength can be passed in as IImage.UNCONSTRAINED, 213 * which indicates no care of the corresponding constraint. 214 * The functions prefers returning a sample size that 215 * generates a smaller bitmap, unless minSideLength = IImage.UNCONSTRAINED. 216 * 217 * Also, the function rounds up the sample size to a power of 2 or multiple 218 * of 8 because BitmapFactory only honors sample size this way. 219 * For example, BitmapFactory downsamples an image by 2 even though the 220 * request is 3. So we round up the sample size to avoid OOM. 221 */ 222 private static int computeSampleSize(BitmapFactory.Options options, 223 int minSideLength, int maxNumOfPixels) { 224 int initialSize = computeInitialSampleSize(options, minSideLength, 225 maxNumOfPixels); 226 227 int roundedSize; 228 if (initialSize <= 8 ) { 229 roundedSize = 1; 230 while (roundedSize < initialSize) { 231 roundedSize <<= 1; 232 } 233 } else { 234 roundedSize = (initialSize + 7) / 8 * 8; 235 } 236 237 return roundedSize; 238 } 239 240 private static int computeInitialSampleSize(BitmapFactory.Options options, 241 int minSideLength, int maxNumOfPixels) { 242 double w = options.outWidth; 243 double h = options.outHeight; 244 245 int lowerBound = (maxNumOfPixels == UNCONSTRAINED) ? 1 : 246 (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels)); 247 int upperBound = (minSideLength == UNCONSTRAINED) ? 128 : 248 (int) Math.min(Math.floor(w / minSideLength), 249 Math.floor(h / minSideLength)); 250 251 if (upperBound < lowerBound) { 252 // return the larger one when there is no overlapping zone. 253 return lowerBound; 254 } 255 256 if ((maxNumOfPixels == UNCONSTRAINED) && 257 (minSideLength == UNCONSTRAINED)) { 258 return 1; 259 } else if (minSideLength == UNCONSTRAINED) { 260 return lowerBound; 261 } else { 262 return upperBound; 263 } 264 } 265 266 /** 267 * Returns Options that set the native alloc flag for Bitmap decode. 268 */ 269 private static BitmapFactory.Options createNativeAllocOptions() { 270 BitmapFactory.Options options = new BitmapFactory.Options(); 271 options.inNativeAlloc = true; 272 return options; 273 } 274 275 /** 276 * Make a bitmap from a given Uri, minimal side length, and maximum number of pixels. 277 */ 278 private static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels, 279 Uri uri, ContentResolver cr) { 280 return makeBitmap(minSideLength, maxNumOfPixels, uri, cr, 281 OPTIONS_DO_NOT_USE_NATIVE); 282 } 283 284 /** 285 * Make a bitmap from a given Uri, minimal side length, and maximum number of pixels. 286 * The image data will be read from specified ContentResolver and clients are allowed to specify 287 * whether they want the Bitmap be created in native memory. 288 */ 289 private static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels, 290 Uri uri, ContentResolver cr, int opt) { 291 boolean useNative = (opt & OPTIONS_DO_NOT_USE_NATIVE) != 0; 292 ParcelFileDescriptor input = null; 293 try { 294 input = cr.openFileDescriptor(uri, "r"); 295 BitmapFactory.Options options = null; 296 if (useNative) { 297 options = createNativeAllocOptions(); 298 } 299 return makeBitmap(minSideLength, maxNumOfPixels, uri, cr, input, 300 options); 301 } catch (IOException ex) { 302 Log.e(TAG, "", ex); 303 return null; 304 } finally { 305 closeSilently(input); 306 } 307 } 308 309 /** 310 * Make a bitmap from a given Uri, minimal side length, and maximum number of pixels. 311 * The image data will be read from specified pfd if it's not null, otherwise 312 * a new input stream will be created using specified ContentResolver. 313 * 314 * Clients are allowed to pass their own BitmapFactory.Options used for bitmap decoding. A 315 * new BitmapFactory.Options will be created if options is null. 316 */ 317 private static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels, 318 Uri uri, ContentResolver cr, ParcelFileDescriptor pfd, 319 BitmapFactory.Options options) { 320 Bitmap b = null; 321 try { 322 if (pfd == null) pfd = makeInputStream(uri, cr); 323 if (pfd == null) return null; 324 if (options == null) options = new BitmapFactory.Options(); 325 326 FileDescriptor fd = pfd.getFileDescriptor(); 327 options.inSampleSize = 1; 328 options.inJustDecodeBounds = true; 329 BitmapFactory.decodeFileDescriptor(fd, null, options); 330 if (options.mCancel || options.outWidth == -1 331 || options.outHeight == -1) { 332 return null; 333 } 334 options.inSampleSize = computeSampleSize( 335 options, minSideLength, maxNumOfPixels); 336 options.inJustDecodeBounds = false; 337 338 options.inDither = false; 339 options.inPreferredConfig = Bitmap.Config.ARGB_8888; 340 b = BitmapFactory.decodeFileDescriptor(fd, null, options); 341 } catch (OutOfMemoryError ex) { 342 Log.e(TAG, "Got oom exception ", ex); 343 return null; 344 } finally { 345 closeSilently(pfd); 346 } 347 return b; 348 } 349 350 private static void closeSilently(ParcelFileDescriptor c) { 351 if (c == null) return; 352 try { 353 c.close(); 354 } catch (Throwable t) { 355 // do nothing 356 } 357 } 358 359 private static ParcelFileDescriptor makeInputStream( 360 Uri uri, ContentResolver cr) { 361 try { 362 return cr.openFileDescriptor(uri, "r"); 363 } catch (IOException ex) { 364 return null; 365 } 366 } 367 368 /** 369 * Transform source Bitmap to targeted width and height. 370 */ 371 private static Bitmap transform(Matrix scaler, 372 Bitmap source, 373 int targetWidth, 374 int targetHeight, 375 int options) { 376 boolean scaleUp = (options & OPTIONS_SCALE_UP) != 0; 377 boolean recycle = (options & OPTIONS_RECYCLE_INPUT) != 0; 378 379 int deltaX = source.getWidth() - targetWidth; 380 int deltaY = source.getHeight() - targetHeight; 381 if (!scaleUp && (deltaX < 0 || deltaY < 0)) { 382 /* 383 * In this case the bitmap is smaller, at least in one dimension, 384 * than the target. Transform it by placing as much of the image 385 * as possible into the target and leaving the top/bottom or 386 * left/right (or both) black. 387 */ 388 Bitmap b2 = Bitmap.createBitmap(targetWidth, targetHeight, 389 Bitmap.Config.ARGB_8888); 390 Canvas c = new Canvas(b2); 391 392 int deltaXHalf = Math.max(0, deltaX / 2); 393 int deltaYHalf = Math.max(0, deltaY / 2); 394 Rect src = new Rect( 395 deltaXHalf, 396 deltaYHalf, 397 deltaXHalf + Math.min(targetWidth, source.getWidth()), 398 deltaYHalf + Math.min(targetHeight, source.getHeight())); 399 int dstX = (targetWidth - src.width()) / 2; 400 int dstY = (targetHeight - src.height()) / 2; 401 Rect dst = new Rect( 402 dstX, 403 dstY, 404 targetWidth - dstX, 405 targetHeight - dstY); 406 c.drawBitmap(source, src, dst, null); 407 if (recycle) { 408 source.recycle(); 409 } 410 return b2; 411 } 412 float bitmapWidthF = source.getWidth(); 413 float bitmapHeightF = source.getHeight(); 414 415 float bitmapAspect = bitmapWidthF / bitmapHeightF; 416 float viewAspect = (float) targetWidth / targetHeight; 417 418 if (bitmapAspect > viewAspect) { 419 float scale = targetHeight / bitmapHeightF; 420 if (scale < .9F || scale > 1F) { 421 scaler.setScale(scale, scale); 422 } else { 423 scaler = null; 424 } 425 } else { 426 float scale = targetWidth / bitmapWidthF; 427 if (scale < .9F || scale > 1F) { 428 scaler.setScale(scale, scale); 429 } else { 430 scaler = null; 431 } 432 } 433 434 Bitmap b1; 435 if (scaler != null) { 436 // this is used for minithumb and crop, so we want to filter here. 437 b1 = Bitmap.createBitmap(source, 0, 0, 438 source.getWidth(), source.getHeight(), scaler, true); 439 } else { 440 b1 = source; 441 } 442 443 if (recycle && b1 != source) { 444 source.recycle(); 445 } 446 447 int dx1 = Math.max(0, b1.getWidth() - targetWidth); 448 int dy1 = Math.max(0, b1.getHeight() - targetHeight); 449 450 Bitmap b2 = Bitmap.createBitmap( 451 b1, 452 dx1 / 2, 453 dy1 / 2, 454 targetWidth, 455 targetHeight); 456 457 if (b2 != b1) { 458 if (recycle || b1 != source) { 459 b1.recycle(); 460 } 461 } 462 463 return b2; 464 } 465 466 private static final String[] THUMB_PROJECTION = new String[] { 467 BaseColumns._ID // 0 468 }; 469 470 /** 471 * Look up thumbnail uri by given imageId, it will be automatically created if it's not created 472 * yet. Most of the time imageId is identical to thumbId, but it's not always true. 473 */ 474 private static Uri getImageThumbnailUri(ContentResolver cr, long origId, int width, int height) { 475 Uri thumbUri = Images.Thumbnails.EXTERNAL_CONTENT_URI; 476 Cursor c = cr.query(thumbUri, THUMB_PROJECTION, 477 Thumbnails.IMAGE_ID + "=?", 478 new String[]{String.valueOf(origId)}, null); 479 if (c == null) return null; 480 try { 481 if (c.moveToNext()) { 482 return ContentUris.withAppendedId(thumbUri, c.getLong(0)); 483 } 484 } finally { 485 if (c != null) c.close(); 486 } 487 488 ContentValues values = new ContentValues(4); 489 values.put(Thumbnails.KIND, Thumbnails.MINI_KIND); 490 values.put(Thumbnails.IMAGE_ID, origId); 491 values.put(Thumbnails.HEIGHT, height); 492 values.put(Thumbnails.WIDTH, width); 493 try { 494 return cr.insert(thumbUri, values); 495 } catch (Exception ex) { 496 Log.w(TAG, ex); 497 return null; 498 } 499 } 500 501 /** 502 * Store a given thumbnail in the database. (Bitmap) 503 */ 504 private static boolean storeThumbnail(ContentResolver cr, long origId, Bitmap thumb) { 505 if (thumb == null) return false; 506 try { 507 Uri uri = getImageThumbnailUri(cr, origId, thumb.getWidth(), thumb.getHeight()); 508 if (uri == null) return false; 509 OutputStream thumbOut = cr.openOutputStream(uri); 510 thumb.compress(Bitmap.CompressFormat.JPEG, 85, thumbOut); 511 thumbOut.close(); 512 return true; 513 } catch (Throwable t) { 514 Log.e(TAG, "Unable to store thumbnail", t); 515 return false; 516 } 517 } 518 519 /** 520 * Store a given thumbnail in the database. (byte array) 521 */ 522 private static boolean storeThumbnail(ContentResolver cr, long origId, byte[] jpegThumbnail, 523 int width, int height) { 524 if (jpegThumbnail == null) return false; 525 526 Uri uri = getImageThumbnailUri(cr, origId, width, height); 527 if (uri == null) { 528 return false; 529 } 530 try { 531 OutputStream thumbOut = cr.openOutputStream(uri); 532 thumbOut.write(jpegThumbnail); 533 thumbOut.close(); 534 return true; 535 } catch (Throwable t) { 536 Log.e(TAG, "Unable to store thumbnail", t); 537 return false; 538 } 539 } 540 541 /** 542 * SizedThumbnailBitmap contains the bitmap, which is downsampled either from 543 * the thumbnail in exif or the full image. 544 * mThumbnailData, mThumbnailWidth and mThumbnailHeight are set together only if mThumbnail 545 * is not null. 546 * 547 * The width/height of the sized bitmap may be different from mThumbnailWidth/mThumbnailHeight. 548 */ 549 private static class SizedThumbnailBitmap { 550 public byte[] mThumbnailData; 551 public Bitmap mBitmap; 552 public int mThumbnailWidth; 553 public int mThumbnailHeight; 554 } 555 556 /** 557 * Creates a bitmap by either downsampling from the thumbnail in EXIF or the full image. 558 * The functions returns a SizedThumbnailBitmap, 559 * which contains a downsampled bitmap and the thumbnail data in EXIF if exists. 560 */ 561 private static void createThumbnailFromEXIF(String filePath, int targetSize, 562 int maxPixels, SizedThumbnailBitmap sizedThumbBitmap) { 563 if (filePath == null) return; 564 565 ExifInterface exif = null; 566 byte [] thumbData = null; 567 try { 568 exif = new ExifInterface(filePath); 569 if (exif != null) { 570 thumbData = exif.getThumbnail(); 571 } 572 } catch (IOException ex) { 573 Log.w(TAG, ex); 574 } 575 576 BitmapFactory.Options fullOptions = new BitmapFactory.Options(); 577 BitmapFactory.Options exifOptions = new BitmapFactory.Options(); 578 int exifThumbWidth = 0; 579 int fullThumbWidth = 0; 580 581 // Compute exifThumbWidth. 582 if (thumbData != null) { 583 exifOptions.inJustDecodeBounds = true; 584 BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, exifOptions); 585 exifOptions.inSampleSize = computeSampleSize(exifOptions, targetSize, maxPixels); 586 exifThumbWidth = exifOptions.outWidth / exifOptions.inSampleSize; 587 } 588 589 // Compute fullThumbWidth. 590 fullOptions.inJustDecodeBounds = true; 591 BitmapFactory.decodeFile(filePath, fullOptions); 592 fullOptions.inSampleSize = computeSampleSize(fullOptions, targetSize, maxPixels); 593 fullThumbWidth = fullOptions.outWidth / fullOptions.inSampleSize; 594 595 // Choose the larger thumbnail as the returning sizedThumbBitmap. 596 if (exifThumbWidth >= fullThumbWidth) { 597 int width = exifOptions.outWidth; 598 int height = exifOptions.outHeight; 599 exifOptions.inJustDecodeBounds = false; 600 sizedThumbBitmap.mBitmap = BitmapFactory.decodeByteArray(thumbData, 0, 601 thumbData.length, exifOptions); 602 if (sizedThumbBitmap.mBitmap != null) { 603 sizedThumbBitmap.mThumbnailData = thumbData; 604 sizedThumbBitmap.mThumbnailWidth = width; 605 sizedThumbBitmap.mThumbnailHeight = height; 606 } 607 } else { 608 fullOptions.inJustDecodeBounds = false; 609 sizedThumbBitmap.mBitmap = BitmapFactory.decodeFile(filePath, fullOptions); 610 } 611 } 612} 613