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