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