• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.net.Uri;
20 import android.os.ParcelFileDescriptor;
21 import android.provider.BaseColumns;
22 import android.provider.MediaStore.Images;
23 import android.provider.MediaStore.Images.Thumbnails;
24 import android.util.Log;
25 
26 import android.content.ContentResolver;
27 import android.content.ContentUris;
28 import android.content.ContentValues;
29 import android.database.Cursor;
30 import android.graphics.Bitmap;
31 import android.graphics.BitmapFactory;
32 import android.graphics.Canvas;
33 import android.graphics.Matrix;
34 import android.graphics.Rect;
35 import android.media.MediaMetadataRetriever;
36 
37 import java.io.ByteArrayOutputStream;
38 import java.io.FileDescriptor;
39 import java.io.FileNotFoundException;
40 import java.io.IOException;
41 import java.io.OutputStream;
42 
43 /**
44  * Thumbnail generation routines for media provider. This class should only be used internaly.
45  * {@hide} THIS IS NOT FOR PUBLIC API.
46  */
47 
48 public class ThumbnailUtil {
49     private static final String TAG = "ThumbnailUtil";
50     //Whether we should recycle the input (unless the output is the input).
51     public static final boolean RECYCLE_INPUT = true;
52     public static final boolean NO_RECYCLE_INPUT = false;
53     public static final boolean ROTATE_AS_NEEDED = true;
54     public static final boolean NO_ROTATE = false;
55     public static final boolean USE_NATIVE = true;
56     public static final boolean NO_NATIVE = false;
57 
58     public static final int THUMBNAIL_TARGET_SIZE = 320;
59     public static final int MINI_THUMB_TARGET_SIZE = 96;
60     public static final int THUMBNAIL_MAX_NUM_PIXELS = 512 * 384;
61     public static final int MINI_THUMB_MAX_NUM_PIXELS = 128 * 128;
62     public static final int UNCONSTRAINED = -1;
63 
64     // Returns Options that set the native alloc flag for Bitmap decode.
createNativeAllocOptions()65     public static BitmapFactory.Options createNativeAllocOptions() {
66         BitmapFactory.Options options = new BitmapFactory.Options();
67         options.inNativeAlloc = true;
68         return options;
69     }
70     /**
71      * Make a bitmap from a given Uri.
72      *
73      * @param uri
74      */
makeBitmap(int minSideLength, int maxNumOfPixels, Uri uri, ContentResolver cr)75     public static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels,
76             Uri uri, ContentResolver cr) {
77         return makeBitmap(minSideLength, maxNumOfPixels, uri, cr,
78                 NO_NATIVE);
79     }
80 
81     /*
82      * Compute the sample size as a function of minSideLength
83      * and maxNumOfPixels.
84      * minSideLength is used to specify that minimal width or height of a
85      * bitmap.
86      * maxNumOfPixels is used to specify the maximal size in pixels that is
87      * tolerable in terms of memory usage.
88      *
89      * The function returns a sample size based on the constraints.
90      * Both size and minSideLength can be passed in as IImage.UNCONSTRAINED,
91      * which indicates no care of the corresponding constraint.
92      * The functions prefers returning a sample size that
93      * generates a smaller bitmap, unless minSideLength = IImage.UNCONSTRAINED.
94      *
95      * Also, the function rounds up the sample size to a power of 2 or multiple
96      * of 8 because BitmapFactory only honors sample size this way.
97      * For example, BitmapFactory downsamples an image by 2 even though the
98      * request is 3. So we round up the sample size to avoid OOM.
99      */
computeSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels)100     public static int computeSampleSize(BitmapFactory.Options options,
101             int minSideLength, int maxNumOfPixels) {
102         int initialSize = computeInitialSampleSize(options, minSideLength,
103                 maxNumOfPixels);
104 
105         int roundedSize;
106         if (initialSize <= 8 ) {
107             roundedSize = 1;
108             while (roundedSize < initialSize) {
109                 roundedSize <<= 1;
110             }
111         } else {
112             roundedSize = (initialSize + 7) / 8 * 8;
113         }
114 
115         return roundedSize;
116     }
117 
computeInitialSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels)118     private static int computeInitialSampleSize(BitmapFactory.Options options,
119             int minSideLength, int maxNumOfPixels) {
120         double w = options.outWidth;
121         double h = options.outHeight;
122 
123         int lowerBound = (maxNumOfPixels == UNCONSTRAINED) ? 1 :
124                 (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels));
125         int upperBound = (minSideLength == UNCONSTRAINED) ? 128 :
126                 (int) Math.min(Math.floor(w / minSideLength),
127                 Math.floor(h / minSideLength));
128 
129         if (upperBound < lowerBound) {
130             // return the larger one when there is no overlapping zone.
131             return lowerBound;
132         }
133 
134         if ((maxNumOfPixels == UNCONSTRAINED) &&
135                 (minSideLength == UNCONSTRAINED)) {
136             return 1;
137         } else if (minSideLength == UNCONSTRAINED) {
138             return lowerBound;
139         } else {
140             return upperBound;
141         }
142     }
143 
makeBitmap(int minSideLength, int maxNumOfPixels, Uri uri, ContentResolver cr, boolean useNative)144     public static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels,
145             Uri uri, ContentResolver cr, boolean useNative) {
146         ParcelFileDescriptor input = null;
147         try {
148             input = cr.openFileDescriptor(uri, "r");
149             BitmapFactory.Options options = null;
150             if (useNative) {
151                 options = createNativeAllocOptions();
152             }
153             return makeBitmap(minSideLength, maxNumOfPixels, uri, cr, input,
154                     options);
155         } catch (IOException ex) {
156             Log.e(TAG, "", ex);
157             return null;
158         } finally {
159             closeSilently(input);
160         }
161     }
162 
163     // Rotates the bitmap by the specified degree.
164     // If a new bitmap is created, the original bitmap is recycled.
rotate(Bitmap b, int degrees)165     public static Bitmap rotate(Bitmap b, int degrees) {
166         if (degrees != 0 && b != null) {
167             Matrix m = new Matrix();
168             m.setRotate(degrees,
169                     (float) b.getWidth() / 2, (float) b.getHeight() / 2);
170             try {
171                 Bitmap b2 = Bitmap.createBitmap(
172                         b, 0, 0, b.getWidth(), b.getHeight(), m, true);
173                 if (b != b2) {
174                     b.recycle();
175                     b = b2;
176                 }
177             } catch (OutOfMemoryError ex) {
178                 // We have no memory to rotate. Return the original bitmap.
179             }
180         }
181         return b;
182     }
183 
closeSilently(ParcelFileDescriptor c)184     private static void closeSilently(ParcelFileDescriptor c) {
185       if (c == null) return;
186       try {
187           c.close();
188       } catch (Throwable t) {
189           // do nothing
190       }
191     }
192 
makeInputStream( Uri uri, ContentResolver cr)193     private static ParcelFileDescriptor makeInputStream(
194             Uri uri, ContentResolver cr) {
195         try {
196             return cr.openFileDescriptor(uri, "r");
197         } catch (IOException ex) {
198             return null;
199         }
200     }
201 
makeBitmap(int minSideLength, int maxNumOfPixels, Uri uri, ContentResolver cr, ParcelFileDescriptor pfd, BitmapFactory.Options options)202     public static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels,
203         Uri uri, ContentResolver cr, ParcelFileDescriptor pfd,
204         BitmapFactory.Options options) {
205         Bitmap b = null;
206         try {
207             if (pfd == null) pfd = makeInputStream(uri, cr);
208             if (pfd == null) return null;
209             if (options == null) options = new BitmapFactory.Options();
210 
211             FileDescriptor fd = pfd.getFileDescriptor();
212             options.inSampleSize = 1;
213             options.inJustDecodeBounds = true;
214             BitmapFactory.decodeFileDescriptor(fd, null, options);
215             if (options.mCancel || options.outWidth == -1
216                     || options.outHeight == -1) {
217                 return null;
218             }
219             options.inSampleSize = computeSampleSize(
220                     options, minSideLength, maxNumOfPixels);
221             options.inJustDecodeBounds = false;
222 
223             options.inDither = false;
224             options.inPreferredConfig = Bitmap.Config.ARGB_8888;
225             b = BitmapFactory.decodeFileDescriptor(fd, null, options);
226         } catch (OutOfMemoryError ex) {
227             Log.e(TAG, "Got oom exception ", ex);
228             return null;
229         } finally {
230             closeSilently(pfd);
231         }
232         return b;
233     }
234 
235     /**
236      * Creates a centered bitmap of the desired size.
237      * @param source
238      * @param recycle whether we want to recycle the input
239      */
extractMiniThumb( Bitmap source, int width, int height, boolean recycle)240     public static Bitmap extractMiniThumb(
241             Bitmap source, int width, int height, boolean recycle) {
242         if (source == null) {
243             return null;
244         }
245 
246         float scale;
247         if (source.getWidth() < source.getHeight()) {
248             scale = width / (float) source.getWidth();
249         } else {
250             scale = height / (float) source.getHeight();
251         }
252         Matrix matrix = new Matrix();
253         matrix.setScale(scale, scale);
254         Bitmap miniThumbnail = transform(matrix, source, width, height, true, recycle);
255         return miniThumbnail;
256     }
257 
258     /**
259      * Create a video thumbnail for a video. May return null if the video is
260      * corrupt.
261      *
262      * @param filePath
263      */
createVideoThumbnail(String filePath)264     public static Bitmap createVideoThumbnail(String filePath) {
265         Bitmap bitmap = null;
266         MediaMetadataRetriever retriever = new MediaMetadataRetriever();
267         try {
268             retriever.setMode(MediaMetadataRetriever.MODE_CAPTURE_FRAME_ONLY);
269             retriever.setDataSource(filePath);
270             bitmap = retriever.captureFrame();
271         } catch (IllegalArgumentException ex) {
272             // Assume this is a corrupt video file
273         } catch (RuntimeException ex) {
274             // Assume this is a corrupt video file.
275         } finally {
276             try {
277                 retriever.release();
278             } catch (RuntimeException ex) {
279                 // Ignore failures while cleaning up.
280             }
281         }
282         return bitmap;
283     }
284 
285     /**
286      * This method first examines if the thumbnail embedded in EXIF is bigger than our target
287      * size. If not, then it'll create a thumbnail from original image. Due to efficiency
288      * consideration, we want to let MediaThumbRequest avoid calling this method twice for
289      * both kinds, so it only requests for MICRO_KIND and set saveImage to true.
290      *
291      * This method always returns a "square thumbnail" for MICRO_KIND thumbnail.
292      *
293      * @param cr ContentResolver
294      * @param filePath file path needed by EXIF interface
295      * @param uri URI of original image
296      * @param origId image id
297      * @param kind either MINI_KIND or MICRO_KIND
298      * @param saveImage Whether to save MINI_KIND thumbnail obtained in this method.
299      * @return Bitmap
300      */
createImageThumbnail(ContentResolver cr, String filePath, Uri uri, long origId, int kind, boolean saveMini)301     public static Bitmap createImageThumbnail(ContentResolver cr, String filePath, Uri uri,
302             long origId, int kind, boolean saveMini) {
303         boolean wantMini = (kind == Images.Thumbnails.MINI_KIND || saveMini);
304         int targetSize = wantMini ?
305                 ThumbnailUtil.THUMBNAIL_TARGET_SIZE : ThumbnailUtil.MINI_THUMB_TARGET_SIZE;
306         int maxPixels = wantMini ?
307                 ThumbnailUtil.THUMBNAIL_MAX_NUM_PIXELS : ThumbnailUtil.MINI_THUMB_MAX_NUM_PIXELS;
308         byte[] thumbData = createThumbnailFromEXIF(filePath, targetSize);
309         Bitmap bitmap = null;
310 
311         if (thumbData != null) {
312             BitmapFactory.Options options = new BitmapFactory.Options();
313             options.inSampleSize = computeSampleSize(options, targetSize, maxPixels);
314             options.inDither = false;
315             options.inPreferredConfig = Bitmap.Config.ARGB_8888;
316             options.inJustDecodeBounds = false;
317             bitmap = BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, options);
318         }
319 
320         if (bitmap == null) {
321             bitmap = ThumbnailUtil.makeBitmap(targetSize, maxPixels, uri, cr);
322         }
323 
324         if (bitmap == null) {
325             return null;
326         }
327 
328         if (saveMini) {
329             if (thumbData != null) {
330                 ThumbnailUtil.storeThumbnail(cr, origId, thumbData, bitmap.getWidth(),
331                         bitmap.getHeight());
332             } else {
333                 ThumbnailUtil.storeThumbnail(cr, origId, bitmap);
334             }
335         }
336 
337         if (kind == Images.Thumbnails.MICRO_KIND) {
338             // now we make it a "square thumbnail" for MICRO_KIND thumbnail
339             bitmap = ThumbnailUtil.extractMiniThumb(bitmap,
340                     ThumbnailUtil.MINI_THUMB_TARGET_SIZE,
341                     ThumbnailUtil.MINI_THUMB_TARGET_SIZE, ThumbnailUtil.RECYCLE_INPUT);
342         }
343         return bitmap;
344     }
345 
transform(Matrix scaler, Bitmap source, int targetWidth, int targetHeight, boolean scaleUp, boolean recycle)346     public static Bitmap transform(Matrix scaler,
347             Bitmap source,
348             int targetWidth,
349             int targetHeight,
350             boolean scaleUp,
351             boolean recycle) {
352 
353         int deltaX = source.getWidth() - targetWidth;
354         int deltaY = source.getHeight() - targetHeight;
355         if (!scaleUp && (deltaX < 0 || deltaY < 0)) {
356             /*
357             * In this case the bitmap is smaller, at least in one dimension,
358             * than the target.  Transform it by placing as much of the image
359             * as possible into the target and leaving the top/bottom or
360             * left/right (or both) black.
361             */
362             Bitmap b2 = Bitmap.createBitmap(targetWidth, targetHeight,
363             Bitmap.Config.ARGB_8888);
364             Canvas c = new Canvas(b2);
365 
366             int deltaXHalf = Math.max(0, deltaX / 2);
367             int deltaYHalf = Math.max(0, deltaY / 2);
368             Rect src = new Rect(
369             deltaXHalf,
370             deltaYHalf,
371             deltaXHalf + Math.min(targetWidth, source.getWidth()),
372             deltaYHalf + Math.min(targetHeight, source.getHeight()));
373             int dstX = (targetWidth  - src.width())  / 2;
374             int dstY = (targetHeight - src.height()) / 2;
375             Rect dst = new Rect(
376                     dstX,
377                     dstY,
378                     targetWidth - dstX,
379                     targetHeight - dstY);
380             c.drawBitmap(source, src, dst, null);
381             if (recycle) {
382                 source.recycle();
383             }
384             return b2;
385         }
386         float bitmapWidthF = source.getWidth();
387         float bitmapHeightF = source.getHeight();
388 
389         float bitmapAspect = bitmapWidthF / bitmapHeightF;
390         float viewAspect   = (float) targetWidth / targetHeight;
391 
392         if (bitmapAspect > viewAspect) {
393             float scale = targetHeight / bitmapHeightF;
394             if (scale < .9F || scale > 1F) {
395                 scaler.setScale(scale, scale);
396             } else {
397                 scaler = null;
398             }
399         } else {
400             float scale = targetWidth / bitmapWidthF;
401             if (scale < .9F || scale > 1F) {
402                 scaler.setScale(scale, scale);
403             } else {
404                 scaler = null;
405             }
406         }
407 
408         Bitmap b1;
409         if (scaler != null) {
410             // this is used for minithumb and crop, so we want to filter here.
411             b1 = Bitmap.createBitmap(source, 0, 0,
412             source.getWidth(), source.getHeight(), scaler, true);
413         } else {
414             b1 = source;
415         }
416 
417         if (recycle && b1 != source) {
418             source.recycle();
419         }
420 
421         int dx1 = Math.max(0, b1.getWidth() - targetWidth);
422         int dy1 = Math.max(0, b1.getHeight() - targetHeight);
423 
424         Bitmap b2 = Bitmap.createBitmap(
425                 b1,
426                 dx1 / 2,
427                 dy1 / 2,
428                 targetWidth,
429                 targetHeight);
430 
431         if (b2 != b1) {
432             if (recycle || b1 != source) {
433                 b1.recycle();
434             }
435         }
436 
437         return b2;
438     }
439 
440     private static final String[] THUMB_PROJECTION = new String[] {
441         BaseColumns._ID // 0
442     };
443 
444     /**
445      * Look up thumbnail uri by given imageId, it will be automatically created if it's not created
446      * yet. Most of the time imageId is identical to thumbId, but it's not always true.
447      * @param req
448      * @param width
449      * @param height
450      * @return Uri Thumbnail uri
451      */
getImageThumbnailUri(ContentResolver cr, long origId, int width, int height)452     private static Uri getImageThumbnailUri(ContentResolver cr, long origId, int width, int height) {
453         Uri thumbUri = Images.Thumbnails.EXTERNAL_CONTENT_URI;
454         Cursor c = cr.query(thumbUri, THUMB_PROJECTION,
455               Thumbnails.IMAGE_ID + "=?",
456               new String[]{String.valueOf(origId)}, null);
457         try {
458             if (c.moveToNext()) {
459                 return ContentUris.withAppendedId(thumbUri, c.getLong(0));
460             }
461         } finally {
462             if (c != null) c.close();
463         }
464 
465         ContentValues values = new ContentValues(4);
466         values.put(Thumbnails.KIND, Thumbnails.MINI_KIND);
467         values.put(Thumbnails.IMAGE_ID, origId);
468         values.put(Thumbnails.HEIGHT, height);
469         values.put(Thumbnails.WIDTH, width);
470         try {
471             return cr.insert(thumbUri, values);
472         } catch (Exception ex) {
473             Log.w(TAG, ex);
474             return null;
475         }
476     }
477 
478     /**
479      * Store a given thumbnail in the database. (Bitmap)
480      */
storeThumbnail(ContentResolver cr, long origId, Bitmap thumb)481     private static boolean storeThumbnail(ContentResolver cr, long origId, Bitmap thumb) {
482         if (thumb == null) return false;
483         try {
484             Uri uri = getImageThumbnailUri(cr, origId, thumb.getWidth(), thumb.getHeight());
485             OutputStream thumbOut = cr.openOutputStream(uri);
486             thumb.compress(Bitmap.CompressFormat.JPEG, 85, thumbOut);
487             thumbOut.close();
488             return true;
489         } catch (Throwable t) {
490             Log.e(TAG, "Unable to store thumbnail", t);
491             return false;
492         }
493     }
494 
495     /**
496      * Store a given thumbnail in the database. (byte array)
497      */
storeThumbnail(ContentResolver cr, long origId, byte[] jpegThumbnail, int width, int height)498     private static boolean storeThumbnail(ContentResolver cr, long origId, byte[] jpegThumbnail,
499             int width, int height) {
500         if (jpegThumbnail == null) return false;
501 
502         Uri uri = getImageThumbnailUri(cr, origId, width, height);
503         if (uri == null) {
504             return false;
505         }
506         try {
507             OutputStream thumbOut = cr.openOutputStream(uri);
508             thumbOut.write(jpegThumbnail);
509             thumbOut.close();
510             return true;
511         } catch (Throwable t) {
512             Log.e(TAG, "Unable to store thumbnail", t);
513             return false;
514         }
515     }
516 
517     // Extract thumbnail in image that meets the targetSize criteria.
createThumbnailFromEXIF(String filePath, int targetSize)518     static byte[] createThumbnailFromEXIF(String filePath, int targetSize) {
519         if (filePath == null) return null;
520 
521         try {
522             ExifInterface exif = new ExifInterface(filePath);
523             if (exif == null) return null;
524             byte [] thumbData = exif.getThumbnail();
525             if (thumbData == null) return null;
526             // Sniff the size of the EXIF thumbnail before decoding it. Photos
527             // from the device will pass, but images that are side loaded from
528             // other cameras may not.
529             BitmapFactory.Options options = new BitmapFactory.Options();
530             options.inJustDecodeBounds = true;
531             BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, options);
532 
533             int width = options.outWidth;
534             int height = options.outHeight;
535 
536             if (width >= targetSize && height >= targetSize) {
537                 return thumbData;
538             }
539         } catch (IOException ex) {
540             Log.w(TAG, ex);
541         }
542         return null;
543     }
544 }
545