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