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.net.Uri; 20 import android.os.ParcelFileDescriptor; 21 import android.provider.BaseColumns; 22 import android.provider.MediaStore.Images; 23 import android.provider.MediaStore.Images.Thumbnails; 24 import android.util.Log; 25 26 import android.content.ContentResolver; 27 import android.content.ContentUris; 28 import android.content.ContentValues; 29 import android.database.Cursor; 30 import android.graphics.Bitmap; 31 import android.graphics.BitmapFactory; 32 import android.graphics.Canvas; 33 import android.graphics.Matrix; 34 import android.graphics.Rect; 35 import android.media.MediaMetadataRetriever; 36 37 import java.io.ByteArrayOutputStream; 38 import java.io.FileDescriptor; 39 import java.io.FileNotFoundException; 40 import java.io.IOException; 41 import 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 48 public 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. createNativeAllocOptions()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 */ makeBitmap(int minSideLength, int maxNumOfPixels, Uri uri, ContentResolver cr)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 */ computeSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels)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 computeInitialSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels)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 makeBitmap(int minSideLength, int maxNumOfPixels, Uri uri, ContentResolver cr, boolean useNative)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. rotate(Bitmap b, int degrees)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 closeSilently(ParcelFileDescriptor c)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 makeInputStream( Uri uri, ContentResolver cr)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 makeBitmap(int minSideLength, int maxNumOfPixels, Uri uri, ContentResolver cr, ParcelFileDescriptor pfd, BitmapFactory.Options options)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 */ extractMiniThumb( Bitmap source, int width, int height, boolean recycle)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 */ createVideoThumbnail(String filePath)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 */ createImageThumbnail(ContentResolver cr, String filePath, Uri uri, long origId, int kind, boolean saveMini)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 transform(Matrix scaler, Bitmap source, int targetWidth, int targetHeight, boolean scaleUp, boolean recycle)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 */ getImageThumbnailUri(ContentResolver cr, long origId, int width, int height)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 */ storeThumbnail(ContentResolver cr, long origId, Bitmap thumb)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 */ storeThumbnail(ContentResolver cr, long origId, byte[] jpegThumbnail, int width, int height)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. createThumbnailFromEXIF(String filePath, int targetSize)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