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