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