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