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