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