2 * Copyright (C) 2009 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package android.media;
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;
37 import java.io.FileInputStream;
38 import java.io.FileDescriptor;
39 import java.io.IOException;
40 import java.io.OutputStream;
43 * Thumbnail generation routines for media provider.
46 public class ThumbnailUtils {
47 private static final String TAG = "ThumbnailUtils";
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 = 128 * 128;
52 private static final int UNCONSTRAINED = -1;
54 /* Options used internally. */
55 private static final int OPTIONS_NONE = 0x0;
56 private static final int OPTIONS_SCALE_UP = 0x1;
59 * Constant used to indicate we should recycle the input in
60 * {@link #extractThumbnail(Bitmap, int, int, int)} unless the output is the input.
62 public static final int OPTIONS_RECYCLE_INPUT = 0x2;
65 * Constant used to indicate the dimension of mini thumbnail.
66 * @hide Only used by media framework and media provider internally.
68 public static final int TARGET_SIZE_MINI_THUMBNAIL = 320;
71 * Constant used to indicate the dimension of micro thumbnail.
72 * @hide Only used by media framework and media provider internally.
74 public static final int TARGET_SIZE_MICRO_THUMBNAIL = 96;
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.
82 * This method always returns a "square thumbnail" for MICRO_KIND thumbnail.
84 * @param filePath the path of image file
85 * @param kind could be MINI_KIND or MICRO_KIND
88 * @hide This method is only used by media framework and media provider internally.
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();
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;
106 if (bitmap == null) {
108 FileDescriptor fd = new FileInputStream(filePath).getFD();
109 BitmapFactory.Options options = new BitmapFactory.Options();
110 options.inSampleSize = 1;
111 options.inJustDecodeBounds = true;
112 BitmapFactory.decodeFileDescriptor(fd, null, options);
113 if (options.mCancel || options.outWidth == -1
114 || options.outHeight == -1) {
117 options.inSampleSize = computeSampleSize(
118 options, targetSize, maxPixels);
119 options.inJustDecodeBounds = false;
121 options.inDither = false;
122 options.inPreferredConfig = Bitmap.Config.ARGB_8888;
123 bitmap = BitmapFactory.decodeFileDescriptor(fd, null, options);
124 } catch (IOException ex) {
129 if (kind == Images.Thumbnails.MICRO_KIND) {
130 // now we make it a "square thumbnail" for MICRO_KIND thumbnail
131 bitmap = extractThumbnail(bitmap,
132 TARGET_SIZE_MICRO_THUMBNAIL,
133 TARGET_SIZE_MICRO_THUMBNAIL, OPTIONS_RECYCLE_INPUT);
139 * Create a video thumbnail for a video. May return null if the video is
140 * corrupt or the format is not supported.
142 * @param filePath the path of video file
143 * @param kind could be MINI_KIND or MICRO_KIND
145 public static Bitmap createVideoThumbnail(String filePath, int kind) {
146 Bitmap bitmap = null;
147 MediaMetadataRetriever retriever = new MediaMetadataRetriever();
149 retriever.setDataSource(filePath);
150 bitmap = retriever.getFrameAtTime(-1);
151 } catch (IllegalArgumentException ex) {
152 // Assume this is a corrupt video file
153 } catch (RuntimeException ex) {
154 // Assume this is a corrupt video file.
158 } catch (RuntimeException ex) {
159 // Ignore failures while cleaning up.
162 if (kind == Images.Thumbnails.MICRO_KIND && bitmap != null) {
163 bitmap = extractThumbnail(bitmap,
164 TARGET_SIZE_MICRO_THUMBNAIL,
165 TARGET_SIZE_MICRO_THUMBNAIL,
166 OPTIONS_RECYCLE_INPUT);
172 * Creates a centered bitmap of the desired size.
174 * @param source original bitmap source
175 * @param width targeted width
176 * @param height targeted height
178 public static Bitmap extractThumbnail(
179 Bitmap source, int width, int height) {
180 return extractThumbnail(source, width, height, OPTIONS_NONE);
184 * Creates a centered bitmap of the desired size.
186 * @param source original bitmap source
187 * @param width targeted width
188 * @param height targeted height
189 * @param options options used during thumbnail extraction
191 public static Bitmap extractThumbnail(
192 Bitmap source, int width, int height, int options) {
193 if (source == null) {
198 if (source.getWidth() < source.getHeight()) {
199 scale = width / (float) source.getWidth();
201 scale = height / (float) source.getHeight();
203 Matrix matrix = new Matrix();
204 matrix.setScale(scale, scale);
205 Bitmap thumbnail = transform(matrix, source, width, height,
206 OPTIONS_SCALE_UP | options);
211 * Compute the sample size as a function of minSideLength
212 * and maxNumOfPixels.
213 * minSideLength is used to specify that minimal width or height of a
215 * maxNumOfPixels is used to specify the maximal size in pixels that is
216 * tolerable in terms of memory usage.
218 * The function returns a sample size based on the constraints.
219 * Both size and minSideLength can be passed in as IImage.UNCONSTRAINED,
220 * which indicates no care of the corresponding constraint.
221 * The functions prefers returning a sample size that
222 * generates a smaller bitmap, unless minSideLength = IImage.UNCONSTRAINED.
224 * Also, the function rounds up the sample size to a power of 2 or multiple
225 * of 8 because BitmapFactory only honors sample size this way.
226 * For example, BitmapFactory downsamples an image by 2 even though the
227 * request is 3. So we round up the sample size to avoid OOM.
229 private static int computeSampleSize(BitmapFactory.Options options,
230 int minSideLength, int maxNumOfPixels) {
231 int initialSize = computeInitialSampleSize(options, minSideLength,
235 if (initialSize <= 8 ) {
237 while (roundedSize < initialSize) {
241 roundedSize = (initialSize + 7) / 8 * 8;
247 private static int computeInitialSampleSize(BitmapFactory.Options options,
248 int minSideLength, int maxNumOfPixels) {
249 double w = options.outWidth;
250 double h = options.outHeight;
252 int lowerBound = (maxNumOfPixels == UNCONSTRAINED) ? 1 :
253 (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels));
254 int upperBound = (minSideLength == UNCONSTRAINED) ? 128 :
255 (int) Math.min(Math.floor(w / minSideLength),
256 Math.floor(h / minSideLength));
258 if (upperBound < lowerBound) {
259 // return the larger one when there is no overlapping zone.
263 if ((maxNumOfPixels == UNCONSTRAINED) &&
264 (minSideLength == UNCONSTRAINED)) {
266 } else if (minSideLength == UNCONSTRAINED) {
274 * Make a bitmap from a given Uri, minimal side length, and maximum number of pixels.
275 * The image data will be read from specified pfd if it's not null, otherwise
276 * a new input stream will be created using specified ContentResolver.
278 * Clients are allowed to pass their own BitmapFactory.Options used for bitmap decoding. A
279 * new BitmapFactory.Options will be created if options is null.
281 private static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels,
282 Uri uri, ContentResolver cr, ParcelFileDescriptor pfd,
283 BitmapFactory.Options options) {
286 if (pfd == null) pfd = makeInputStream(uri, cr);
287 if (pfd == null) return null;
288 if (options == null) options = new BitmapFactory.Options();
290 FileDescriptor fd = pfd.getFileDescriptor();
291 options.inSampleSize = 1;
292 options.inJustDecodeBounds = true;
293 BitmapFactory.decodeFileDescriptor(fd, null, options);
294 if (options.mCancel || options.outWidth == -1
295 || options.outHeight == -1) {
298 options.inSampleSize = computeSampleSize(
299 options, minSideLength, maxNumOfPixels);
300 options.inJustDecodeBounds = false;
302 options.inDither = false;
303 options.inPreferredConfig = Bitmap.Config.ARGB_8888;
304 b = BitmapFactory.decodeFileDescriptor(fd, null, options);
305 } catch (OutOfMemoryError ex) {
306 Log.e(TAG, "Got oom exception ", ex);
314 private static void closeSilently(ParcelFileDescriptor c) {
315 if (c == null) return;
318 } catch (Throwable t) {
323 private static ParcelFileDescriptor makeInputStream(
324 Uri uri, ContentResolver cr) {
326 return cr.openFileDescriptor(uri, "r");
327 } catch (IOException ex) {
333 * Transform source Bitmap to targeted width and height.
335 private static Bitmap transform(Matrix scaler,
340 boolean scaleUp = (options & OPTIONS_SCALE_UP) != 0;
341 boolean recycle = (options & OPTIONS_RECYCLE_INPUT) != 0;
343 int deltaX = source.getWidth() - targetWidth;
344 int deltaY = source.getHeight() - targetHeight;
345 if (!scaleUp && (deltaX < 0 || deltaY < 0)) {
347 * In this case the bitmap is smaller, at least in one dimension,
348 * than the target. Transform it by placing as much of the image
349 * as possible into the target and leaving the top/bottom or
350 * left/right (or both) black.
352 Bitmap b2 = Bitmap.createBitmap(targetWidth, targetHeight,
353 Bitmap.Config.ARGB_8888);
354 Canvas c = new Canvas(b2);
356 int deltaXHalf = Math.max(0, deltaX / 2);
357 int deltaYHalf = Math.max(0, deltaY / 2);
361 deltaXHalf + Math.min(targetWidth, source.getWidth()),
362 deltaYHalf + Math.min(targetHeight, source.getHeight()));
363 int dstX = (targetWidth - src.width()) / 2;
364 int dstY = (targetHeight - src.height()) / 2;
369 targetHeight - dstY);
370 c.drawBitmap(source, src, dst, null);
376 float bitmapWidthF = source.getWidth();
377 float bitmapHeightF = source.getHeight();
379 float bitmapAspect = bitmapWidthF / bitmapHeightF;
380 float viewAspect = (float) targetWidth / targetHeight;
382 if (bitmapAspect > viewAspect) {
383 float scale = targetHeight / bitmapHeightF;
384 if (scale < .9F || scale > 1F) {
385 scaler.setScale(scale, scale);
390 float scale = targetWidth / bitmapWidthF;
391 if (scale < .9F || scale > 1F) {
392 scaler.setScale(scale, scale);
399 if (scaler != null) {
400 // this is used for minithumb and crop, so we want to filter here.
401 b1 = Bitmap.createBitmap(source, 0, 0,
402 source.getWidth(), source.getHeight(), scaler, true);
407 if (recycle && b1 != source) {
411 int dx1 = Math.max(0, b1.getWidth() - targetWidth);
412 int dy1 = Math.max(0, b1.getHeight() - targetHeight);
414 Bitmap b2 = Bitmap.createBitmap(
422 if (recycle || b1 != source) {
431 * SizedThumbnailBitmap contains the bitmap, which is downsampled either from
432 * the thumbnail in exif or the full image.
433 * mThumbnailData, mThumbnailWidth and mThumbnailHeight are set together only if mThumbnail
436 * The width/height of the sized bitmap may be different from mThumbnailWidth/mThumbnailHeight.
438 private static class SizedThumbnailBitmap {
439 public byte[] mThumbnailData;
440 public Bitmap mBitmap;
441 public int mThumbnailWidth;
442 public int mThumbnailHeight;
446 * Creates a bitmap by either downsampling from the thumbnail in EXIF or the full image.
447 * The functions returns a SizedThumbnailBitmap,
448 * which contains a downsampled bitmap and the thumbnail data in EXIF if exists.
450 private static void createThumbnailFromEXIF(String filePath, int targetSize,
451 int maxPixels, SizedThumbnailBitmap sizedThumbBitmap) {
452 if (filePath == null) return;
454 ExifInterface exif = null;
455 byte [] thumbData = null;
457 exif = new ExifInterface(filePath);
459 thumbData = exif.getThumbnail();
461 } catch (IOException ex) {
465 BitmapFactory.Options fullOptions = new BitmapFactory.Options();
466 BitmapFactory.Options exifOptions = new BitmapFactory.Options();
467 int exifThumbWidth = 0;
468 int fullThumbWidth = 0;
470 // Compute exifThumbWidth.
471 if (thumbData != null) {
472 exifOptions.inJustDecodeBounds = true;
473 BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, exifOptions);
474 exifOptions.inSampleSize = computeSampleSize(exifOptions, targetSize, maxPixels);
475 exifThumbWidth = exifOptions.outWidth / exifOptions.inSampleSize;
478 // Compute fullThumbWidth.
479 fullOptions.inJustDecodeBounds = true;
480 BitmapFactory.decodeFile(filePath, fullOptions);
481 fullOptions.inSampleSize = computeSampleSize(fullOptions, targetSize, maxPixels);
482 fullThumbWidth = fullOptions.outWidth / fullOptions.inSampleSize;
484 // Choose the larger thumbnail as the returning sizedThumbBitmap.
485 if (thumbData != null && exifThumbWidth >= fullThumbWidth) {
486 int width = exifOptions.outWidth;
487 int height = exifOptions.outHeight;
488 exifOptions.inJustDecodeBounds = false;
489 sizedThumbBitmap.mBitmap = BitmapFactory.decodeByteArray(thumbData, 0,
490 thumbData.length, exifOptions);
491 if (sizedThumbBitmap.mBitmap != null) {
492 sizedThumbBitmap.mThumbnailData = thumbData;
493 sizedThumbBitmap.mThumbnailWidth = width;
494 sizedThumbBitmap.mThumbnailHeight = height;
497 fullOptions.inJustDecodeBounds = false;
498 sizedThumbBitmap.mBitmap = BitmapFactory.decodeFile(filePath, fullOptions);