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 && (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     */
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     */
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     */
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     */
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
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     */
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
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
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     */
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     */
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