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      */
PhotoProcessor(Bitmap original, int maxDisplayPhotoDim, int maxThumbnailPhotoDim)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      */
PhotoProcessor(byte[] originalBytes, int maxDisplayPhotoDim, int maxThumbnailPhotoDim)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      */
PhotoProcessor(Bitmap original, int maxDisplayPhotoDim, int maxThumbnailPhotoDim, boolean forceCropToSquare)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      */
PhotoProcessor(byte[] originalBytes, int maxDisplayPhotoDim, int maxThumbnailPhotoDim, boolean forceCropToSquare)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      */
process()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
getNormalizedBitmap(Bitmap original, int maxDim, boolean forceCropToSquare)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      */
getCompressedBytes(Bitmap b, int quality)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      */
getDisplayPhoto()268     public Bitmap getDisplayPhoto() {
269         return mDisplayPhoto;
270     }
271 
272     /**
273      * Retrieves the uncompressed thumbnail photo.
274      */
getThumbnailPhoto()275     public Bitmap getThumbnailPhoto() {
276         return mThumbnailPhoto;
277     }
278 
279     /**
280      * Retrieves the compressed display photo as a byte array.
281      */
getDisplayPhotoBytes()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      */
getThumbnailPhotoBytes()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      */
getMaxDisplayPhotoDim()302     public int getMaxDisplayPhotoDim() {
303         return mMaxDisplayPhotoDim;
304     }
305 
306     /**
307      * Retrieves the maximum width or height (in pixels) of the thumbnail.
308      */
getMaxThumbnailPhotoDim()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      */
getMaxThumbnailSize()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      */
getMaxDisplayPhotoSize()325     public static int getMaxDisplayPhotoSize() {
326         return sMaxDisplayPhotoDim;
327     }
328 }
329