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