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