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