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