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