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 */
16package com.android.providers.contacts;
17
18import android.graphics.Bitmap;
19import android.graphics.BitmapFactory;
20import android.graphics.Canvas;
21import android.graphics.Color;
22import android.graphics.Paint;
23import android.graphics.Rect;
24import android.graphics.RectF;
25import android.os.SystemProperties;
26
27import com.android.providers.contacts.util.MemoryUtils;
28import com.google.common.annotations.VisibleForTesting;
29
30import java.io.ByteArrayOutputStream;
31import 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