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