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