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