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
17package android.media;
18
19import android.content.ContentResolver;
20import android.graphics.Bitmap;
21import android.graphics.BitmapFactory;
22import android.graphics.Canvas;
23import android.graphics.Matrix;
24import android.graphics.Rect;
25import android.media.MediaMetadataRetriever;
26import android.media.MediaFile.MediaFileType;
27import android.net.Uri;
28import android.os.ParcelFileDescriptor;
29import android.provider.MediaStore.Images;
30import android.util.Log;
31
32import java.io.FileInputStream;
33import java.io.FileDescriptor;
34import java.io.IOException;
35
36/**
37 * Thumbnail generation routines for media provider.
38 */
39
40public 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     */
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     */
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     */
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     */
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     */
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
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     */
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
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
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     */
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     */
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
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