Home | History | Annotate | Download | only in media
      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.content.ContentUris;
     21 import android.content.ContentValues;
     22 import android.database.Cursor;
     23 import android.graphics.Bitmap;
     24 import android.graphics.BitmapFactory;
     25 import android.graphics.Canvas;
     26 import android.graphics.Matrix;
     27 import android.graphics.Rect;
     28 import android.media.MediaMetadataRetriever;
     29 import android.media.MediaFile.MediaFileType;
     30 import android.net.Uri;
     31 import android.os.ParcelFileDescriptor;
     32 import android.provider.BaseColumns;
     33 import android.provider.MediaStore.Images;
     34 import android.provider.MediaStore.Images.Thumbnails;
     35 import android.util.Log;
     36 
     37 import java.io.FileInputStream;
     38 import java.io.FileDescriptor;
     39 import java.io.IOException;
     40 import java.io.OutputStream;
     41 
     42 /**
     43  * Thumbnail generation routines for media provider.
     44  */
     45 
     46 public 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 = 160 * 120;
     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