Home | History | Annotate | Download | only in contacts
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * 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, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License
     15  */
     16 package com.android.providers.contacts;
     17 
     18 import android.graphics.Bitmap;
     19 import android.graphics.BitmapFactory;
     20 import android.graphics.Canvas;
     21 import android.graphics.Color;
     22 import android.graphics.Paint;
     23 import android.graphics.Rect;
     24 import android.graphics.RectF;
     25 import android.os.SystemProperties;
     26 
     27 import com.android.providers.contacts.util.MemoryUtils;
     28 import com.google.common.annotations.VisibleForTesting;
     29 
     30 import java.io.ByteArrayOutputStream;
     31 import java.io.IOException;
     32 
     33 /**
     34  * Class that converts a bitmap (or byte array representing a bitmap) into a display
     35  * photo and a thumbnail photo.
     36  */
     37 /* package-protected */ final class PhotoProcessor {
     38 
     39     /** Compression for display photos. They are very big, so we can use a strong compression */
     40     private static final int COMPRESSION_DISPLAY_PHOTO = 75;
     41 
     42     /**
     43      * Compression for thumbnails that don't have a full size photo. Those can be blown up
     44      * full-screen, so we want to make sure we don't introduce JPEG artifacts here
     45      */
     46     private static final int COMPRESSION_THUMBNAIL_HIGH = 95;
     47 
     48     /** Compression for thumbnails that also have a display photo */
     49     private static final int COMPRESSION_THUMBNAIL_LOW = 90;
     50 
     51     private static final Paint WHITE_PAINT = new Paint();
     52 
     53     static {
     54         WHITE_PAINT.setColor(Color.WHITE);
     55     }
     56 
     57     private static int sMaxThumbnailDim;
     58     private static int sMaxDisplayPhotoDim;
     59 
     60     static {
     61         final boolean isExpensiveDevice =
     62                 MemoryUtils.getTotalMemorySize() >= PhotoSizes.LARGE_RAM_THRESHOLD;
     63 
     64         sMaxThumbnailDim = SystemProperties.getInt(
     65                 PhotoSizes.SYS_PROPERTY_THUMBNAIL_SIZE, PhotoSizes.DEFAULT_THUMBNAIL);
     66 
     67         sMaxDisplayPhotoDim = SystemProperties.getInt(
     68                 PhotoSizes.SYS_PROPERTY_DISPLAY_PHOTO_SIZE,
     69                 isExpensiveDevice
     70                         ? PhotoSizes.DEFAULT_DISPLAY_PHOTO_LARGE_MEMORY
     71                         : PhotoSizes.DEFAULT_DISPLAY_PHOTO_MEMORY_CONSTRAINED);
     72     }
     73 
     74     /**
     75      * The default sizes of a thumbnail/display picture. This is used in {@link #initialize()}
     76      */
     77     private interface PhotoSizes {
     78         /** Size of a thumbnail */
     79         public static final int DEFAULT_THUMBNAIL = 96;
     80 
     81         /**
     82          * Size of a display photo on memory constrained devices (those are devices with less than
     83          * {@link #DEFAULT_LARGE_RAM_THRESHOLD} of reported RAM
     84          */
     85         public static final int DEFAULT_DISPLAY_PHOTO_MEMORY_CONSTRAINED = 480;
     86 
     87         /**
     88          * Size of a display photo on devices with enough ram (those are devices with at least
     89          * {@link #DEFAULT_LARGE_RAM_THRESHOLD} of reported RAM
     90          */
     91         public static final int DEFAULT_DISPLAY_PHOTO_LARGE_MEMORY = 720;
     92 
     93         /**
     94          * If the device has less than this amount of RAM, it is considered RAM constrained for
     95          * photos
     96          */
     97         public static final int LARGE_RAM_THRESHOLD = 640 * 1024 * 1024;
     98 
     99         /** If present, overrides the size given in {@link #DEFAULT_THUMBNAIL} */
    100         public static final String SYS_PROPERTY_THUMBNAIL_SIZE = "contacts.thumbnail_size";
    101 
    102         /** If present, overrides the size determined for the display photo */
    103         public static final String SYS_PROPERTY_DISPLAY_PHOTO_SIZE = "contacts.display_photo_size";
    104     }
    105 
    106     private final int mMaxDisplayPhotoDim;
    107     private final int mMaxThumbnailPhotoDim;
    108     private final boolean mForceCropToSquare;
    109     private final Bitmap mOriginal;
    110     private Bitmap mDisplayPhoto;
    111     private Bitmap mThumbnailPhoto;
    112 
    113     /**
    114      * Initializes a photo processor for the given bitmap.
    115      * @param original The bitmap to process.
    116      * @param maxDisplayPhotoDim The maximum height and width for the display photo.
    117      * @param maxThumbnailPhotoDim The maximum height and width for the thumbnail photo.
    118      * @throws IOException If bitmap decoding or scaling fails.
    119      */
    120     public PhotoProcessor(Bitmap original, int maxDisplayPhotoDim, int maxThumbnailPhotoDim)
    121             throws IOException {
    122         this(original, maxDisplayPhotoDim, maxThumbnailPhotoDim, false);
    123     }
    124 
    125     /**
    126      * Initializes a photo processor for the given bitmap.
    127      * @param originalBytes A byte array to decode into a bitmap to process.
    128      * @param maxDisplayPhotoDim The maximum height and width for the display photo.
    129      * @param maxThumbnailPhotoDim The maximum height and width for the thumbnail photo.
    130      * @throws IOException If bitmap decoding or scaling fails.
    131      */
    132     public PhotoProcessor(byte[] originalBytes, int maxDisplayPhotoDim, int maxThumbnailPhotoDim)
    133             throws IOException {
    134         this(BitmapFactory.decodeByteArray(originalBytes, 0, originalBytes.length),
    135                 maxDisplayPhotoDim, maxThumbnailPhotoDim, false);
    136     }
    137 
    138     /**
    139      * Initializes a photo processor for the given bitmap.
    140      * @param original The bitmap to process.
    141      * @param maxDisplayPhotoDim The maximum height and width for the display photo.
    142      * @param maxThumbnailPhotoDim The maximum height and width for the thumbnail photo.
    143      * @param forceCropToSquare Whether to force the processed images to be square.  If the source
    144      *     photo is not square, this will crop to the square at the center of the image's rectangle.
    145      *     If this is not set to true, the image will simply be downscaled to fit in the given
    146      *     dimensions, retaining its original aspect ratio.
    147      * @throws IOException If bitmap decoding or scaling fails.
    148      */
    149     public PhotoProcessor(Bitmap original, int maxDisplayPhotoDim, int maxThumbnailPhotoDim,
    150             boolean forceCropToSquare) throws IOException {
    151         mOriginal = original;
    152         mMaxDisplayPhotoDim = maxDisplayPhotoDim;
    153         mMaxThumbnailPhotoDim = maxThumbnailPhotoDim;
    154         mForceCropToSquare = forceCropToSquare;
    155         process();
    156     }
    157 
    158     /**
    159      * Initializes a photo processor for the given bitmap.
    160      * @param originalBytes A byte array to decode into a bitmap to process.
    161      * @param maxDisplayPhotoDim The maximum height and width for the display photo.
    162      * @param maxThumbnailPhotoDim The maximum height and width for the thumbnail photo.
    163      * @param forceCropToSquare Whether to force the processed images to be square.  If the source
    164      *     photo is not square, this will crop to the square at the center of the image's rectangle.
    165      *     If this is not set to true, the image will simply be downscaled to fit in the given
    166      *     dimensions, retaining its original aspect ratio.
    167      * @throws IOException If bitmap decoding or scaling fails.
    168      */
    169     public PhotoProcessor(byte[] originalBytes, int maxDisplayPhotoDim, int maxThumbnailPhotoDim,
    170             boolean forceCropToSquare) throws IOException {
    171         this(BitmapFactory.decodeByteArray(originalBytes, 0, originalBytes.length),
    172                 maxDisplayPhotoDim, maxThumbnailPhotoDim, forceCropToSquare);
    173     }
    174 
    175     /**
    176      * Processes the original image, producing a scaled-down display photo and thumbnail photo.
    177      * @throws IOException If bitmap decoding or scaling fails.
    178      */
    179     private void process() throws IOException {
    180         if (mOriginal == null) {
    181             throw new IOException("Invalid image file");
    182         }
    183         mDisplayPhoto = getNormalizedBitmap(mOriginal, mMaxDisplayPhotoDim, mForceCropToSquare);
    184         mThumbnailPhoto = getNormalizedBitmap(mOriginal,mMaxThumbnailPhotoDim, mForceCropToSquare);
    185     }
    186 
    187     /**
    188      * Scales down the original bitmap to fit within the given maximum width and height.
    189      * If the bitmap already fits in those dimensions, the original bitmap will be
    190      * returned unmodified unless the photo processor is set up to crop it to a square.
    191      *
    192      * Also, if the image has transparency, conevrt it to white.
    193      *
    194      * @param original Original bitmap
    195      * @param maxDim Maximum width and height (in pixels) for the image.
    196      * @param forceCropToSquare See {@link #PhotoProcessor(Bitmap, int, int, boolean)}
    197      * @return A bitmap that fits the maximum dimensions.
    198      * @throws IOException If bitmap decoding or scaling fails.
    199      */
    200     @SuppressWarnings({"SuspiciousNameCombination"})
    201     @VisibleForTesting
    202     static Bitmap getNormalizedBitmap(Bitmap original, int maxDim, boolean forceCropToSquare)
    203             throws IOException {
    204         final boolean originalHasAlpha = original.hasAlpha();
    205 
    206         // All cropXxx's are in the original coordinate.
    207         int cropWidth = original.getWidth();
    208         int cropHeight = original.getHeight();
    209         int cropLeft = 0;
    210         int cropTop = 0;
    211         if (forceCropToSquare && cropWidth != cropHeight) {
    212             // Crop the image to the square at its center.
    213             if (cropHeight > cropWidth) {
    214                 cropTop = (cropHeight - cropWidth) / 2;
    215                 cropHeight = cropWidth;
    216             } else {
    217                 cropLeft = (cropWidth - cropHeight) / 2;
    218                 cropWidth = cropHeight;
    219             }
    220         }
    221         // Calculate the scale factor.  We don't want to scale up, so the max scale is 1f.
    222         final float scaleFactor = Math.min(1f, ((float) maxDim) / Math.max(cropWidth, cropHeight));
    223 
    224         if (scaleFactor < 1.0f || cropLeft != 0 || cropTop != 0 || originalHasAlpha) {
    225             final int newWidth = (int) (cropWidth * scaleFactor);
    226             final int newHeight = (int) (cropHeight * scaleFactor);
    227             if (newWidth <= 0 || newHeight <= 0) {
    228                 throw new IOException("Invalid bitmap dimensions");
    229             }
    230             final Bitmap scaledBitmap = Bitmap.createBitmap(newWidth, newHeight,
    231                     Bitmap.Config.ARGB_8888);
    232             final Canvas c = new Canvas(scaledBitmap);
    233 
    234             if (originalHasAlpha) {
    235                 c.drawRect(0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight(), WHITE_PAINT);
    236             }
    237 
    238             final Rect src = new Rect(cropLeft, cropTop,
    239                     cropLeft + cropWidth, cropTop + cropHeight);
    240             final RectF dst = new RectF(0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight());
    241 
    242             c.drawBitmap(original, src, dst, null);
    243             return scaledBitmap;
    244         } else {
    245             return original;
    246         }
    247     }
    248 
    249     /**
    250      * Helper method to compress the given bitmap as a JPEG and return the resulting byte array.
    251      */
    252     private byte[] getCompressedBytes(Bitmap b, int quality) throws IOException {
    253         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    254         final boolean compressed = b.compress(Bitmap.CompressFormat.JPEG, quality, baos);
    255         baos.flush();
    256         baos.close();
    257         byte[] result = baos.toByteArray();
    258 
    259         if (!compressed) {
    260             throw new IOException("Unable to compress image");
    261         }
    262         return result;
    263     }
    264 
    265     /**
    266      * Retrieves the uncompressed display photo.
    267      */
    268     public Bitmap getDisplayPhoto() {
    269         return mDisplayPhoto;
    270     }
    271 
    272     /**
    273      * Retrieves the uncompressed thumbnail photo.
    274      */
    275     public Bitmap getThumbnailPhoto() {
    276         return mThumbnailPhoto;
    277     }
    278 
    279     /**
    280      * Retrieves the compressed display photo as a byte array.
    281      */
    282     public byte[] getDisplayPhotoBytes() throws IOException {
    283         return getCompressedBytes(mDisplayPhoto, COMPRESSION_DISPLAY_PHOTO);
    284     }
    285 
    286     /**
    287      * Retrieves the compressed thumbnail photo as a byte array.
    288      */
    289     public byte[] getThumbnailPhotoBytes() throws IOException {
    290         // If there is a higher-resolution picture, we can assume we won't need to upscale the
    291         // thumbnail often, so we can compress stronger
    292         final boolean hasDisplayPhoto = mDisplayPhoto != null &&
    293                 (mDisplayPhoto.getWidth() > mThumbnailPhoto.getWidth() ||
    294                 mDisplayPhoto.getHeight() > mThumbnailPhoto.getHeight());
    295         return getCompressedBytes(mThumbnailPhoto,
    296                 hasDisplayPhoto ? COMPRESSION_THUMBNAIL_LOW : COMPRESSION_THUMBNAIL_HIGH);
    297     }
    298 
    299     /**
    300      * Retrieves the maximum width or height (in pixels) of the display photo.
    301      */
    302     public int getMaxDisplayPhotoDim() {
    303         return mMaxDisplayPhotoDim;
    304     }
    305 
    306     /**
    307      * Retrieves the maximum width or height (in pixels) of the thumbnail.
    308      */
    309     public int getMaxThumbnailPhotoDim() {
    310         return mMaxThumbnailPhotoDim;
    311     }
    312 
    313     /**
    314      * Returns the maximum size in pixel of a thumbnail (which has a default that can be overriden
    315      * using a system-property)
    316      */
    317     public static int getMaxThumbnailSize() {
    318         return sMaxThumbnailDim;
    319     }
    320 
    321     /**
    322      * Returns the maximum size in pixel of a display photo (which is determined based
    323      * on available RAM or configured using a system-property)
    324      */
    325     public static int getMaxDisplayPhotoSize() {
    326         return sMaxDisplayPhotoDim;
    327     }
    328 }
    329