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.sysprop.ContactsProperties; 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 = ContactsProperties.thumbnail_size().orElse( 65 PhotoSizes.DEFAULT_THUMBNAIL); 66 67 sMaxDisplayPhotoDim = ContactsProperties.display_photo_size().orElse( 68 isExpensiveDevice 69 ? PhotoSizes.DEFAULT_DISPLAY_PHOTO_LARGE_MEMORY 70 : PhotoSizes.DEFAULT_DISPLAY_PHOTO_MEMORY_CONSTRAINED); 71 } 72 73 /** 74 * The default sizes of a thumbnail/display picture. This is used in {@link #initialize()} 75 */ 76 private interface PhotoSizes { 77 /** Size of a thumbnail */ 78 public static final int DEFAULT_THUMBNAIL = 96; 79 80 /** 81 * Size of a display photo on memory constrained devices (those are devices with less than 82 * {@link #DEFAULT_LARGE_RAM_THRESHOLD} of reported RAM 83 */ 84 public static final int DEFAULT_DISPLAY_PHOTO_MEMORY_CONSTRAINED = 480; 85 86 /** 87 * Size of a display photo on devices with enough ram (those are devices with at least 88 * {@link #DEFAULT_LARGE_RAM_THRESHOLD} of reported RAM 89 */ 90 public static final int DEFAULT_DISPLAY_PHOTO_LARGE_MEMORY = 720; 91 92 /** 93 * If the device has less than this amount of RAM, it is considered RAM constrained for 94 * photos 95 */ 96 public static final int LARGE_RAM_THRESHOLD = 640 * 1024 * 1024; 97 } 98 99 private final int mMaxDisplayPhotoDim; 100 private final int mMaxThumbnailPhotoDim; 101 private final boolean mForceCropToSquare; 102 private final Bitmap mOriginal; 103 private Bitmap mDisplayPhoto; 104 private Bitmap mThumbnailPhoto; 105 106 /** 107 * Initializes a photo processor for the given bitmap. 108 * @param original The bitmap to process. 109 * @param maxDisplayPhotoDim The maximum height and width for the display photo. 110 * @param maxThumbnailPhotoDim The maximum height and width for the thumbnail photo. 111 * @throws IOException If bitmap decoding or scaling fails. 112 */ PhotoProcessor(Bitmap original, int maxDisplayPhotoDim, int maxThumbnailPhotoDim)113 public PhotoProcessor(Bitmap original, int maxDisplayPhotoDim, int maxThumbnailPhotoDim) 114 throws IOException { 115 this(original, maxDisplayPhotoDim, maxThumbnailPhotoDim, false); 116 } 117 118 /** 119 * Initializes a photo processor for the given bitmap. 120 * @param originalBytes A byte array to decode into a bitmap to process. 121 * @param maxDisplayPhotoDim The maximum height and width for the display photo. 122 * @param maxThumbnailPhotoDim The maximum height and width for the thumbnail photo. 123 * @throws IOException If bitmap decoding or scaling fails. 124 */ PhotoProcessor(byte[] originalBytes, int maxDisplayPhotoDim, int maxThumbnailPhotoDim)125 public PhotoProcessor(byte[] originalBytes, int maxDisplayPhotoDim, int maxThumbnailPhotoDim) 126 throws IOException { 127 this(BitmapFactory.decodeByteArray(originalBytes, 0, originalBytes.length), 128 maxDisplayPhotoDim, maxThumbnailPhotoDim, false); 129 } 130 131 /** 132 * Initializes a photo processor for the given bitmap. 133 * @param original The bitmap to process. 134 * @param maxDisplayPhotoDim The maximum height and width for the display photo. 135 * @param maxThumbnailPhotoDim The maximum height and width for the thumbnail photo. 136 * @param forceCropToSquare Whether to force the processed images to be square. If the source 137 * photo is not square, this will crop to the square at the center of the image's rectangle. 138 * If this is not set to true, the image will simply be downscaled to fit in the given 139 * dimensions, retaining its original aspect ratio. 140 * @throws IOException If bitmap decoding or scaling fails. 141 */ PhotoProcessor(Bitmap original, int maxDisplayPhotoDim, int maxThumbnailPhotoDim, boolean forceCropToSquare)142 public PhotoProcessor(Bitmap original, int maxDisplayPhotoDim, int maxThumbnailPhotoDim, 143 boolean forceCropToSquare) throws IOException { 144 mOriginal = original; 145 mMaxDisplayPhotoDim = maxDisplayPhotoDim; 146 mMaxThumbnailPhotoDim = maxThumbnailPhotoDim; 147 mForceCropToSquare = forceCropToSquare; 148 process(); 149 } 150 151 /** 152 * Initializes a photo processor for the given bitmap. 153 * @param originalBytes A byte array to decode into a bitmap to process. 154 * @param maxDisplayPhotoDim The maximum height and width for the display photo. 155 * @param maxThumbnailPhotoDim The maximum height and width for the thumbnail photo. 156 * @param forceCropToSquare Whether to force the processed images to be square. If the source 157 * photo is not square, this will crop to the square at the center of the image's rectangle. 158 * If this is not set to true, the image will simply be downscaled to fit in the given 159 * dimensions, retaining its original aspect ratio. 160 * @throws IOException If bitmap decoding or scaling fails. 161 */ PhotoProcessor(byte[] originalBytes, int maxDisplayPhotoDim, int maxThumbnailPhotoDim, boolean forceCropToSquare)162 public PhotoProcessor(byte[] originalBytes, int maxDisplayPhotoDim, int maxThumbnailPhotoDim, 163 boolean forceCropToSquare) throws IOException { 164 this(BitmapFactory.decodeByteArray(originalBytes, 0, originalBytes.length), 165 maxDisplayPhotoDim, maxThumbnailPhotoDim, forceCropToSquare); 166 } 167 168 /** 169 * Processes the original image, producing a scaled-down display photo and thumbnail photo. 170 * @throws IOException If bitmap decoding or scaling fails. 171 */ process()172 private void process() throws IOException { 173 if (mOriginal == null) { 174 throw new IOException("Invalid image file"); 175 } 176 mDisplayPhoto = getNormalizedBitmap(mOriginal, mMaxDisplayPhotoDim, mForceCropToSquare); 177 mThumbnailPhoto = getNormalizedBitmap(mOriginal,mMaxThumbnailPhotoDim, mForceCropToSquare); 178 } 179 180 /** 181 * Scales down the original bitmap to fit within the given maximum width and height. 182 * If the bitmap already fits in those dimensions, the original bitmap will be 183 * returned unmodified unless the photo processor is set up to crop it to a square. 184 * 185 * Also, if the image has transparency, conevrt it to white. 186 * 187 * @param original Original bitmap 188 * @param maxDim Maximum width and height (in pixels) for the image. 189 * @param forceCropToSquare See {@link #PhotoProcessor(Bitmap, int, int, boolean)} 190 * @return A bitmap that fits the maximum dimensions. 191 * @throws IOException If bitmap decoding or scaling fails. 192 */ 193 @SuppressWarnings({"SuspiciousNameCombination"}) 194 @VisibleForTesting getNormalizedBitmap(Bitmap original, int maxDim, boolean forceCropToSquare)195 static Bitmap getNormalizedBitmap(Bitmap original, int maxDim, boolean forceCropToSquare) 196 throws IOException { 197 final boolean originalHasAlpha = original.hasAlpha(); 198 199 // All cropXxx's are in the original coordinate. 200 int cropWidth = original.getWidth(); 201 int cropHeight = original.getHeight(); 202 int cropLeft = 0; 203 int cropTop = 0; 204 if (forceCropToSquare && cropWidth != cropHeight) { 205 // Crop the image to the square at its center. 206 if (cropHeight > cropWidth) { 207 cropTop = (cropHeight - cropWidth) / 2; 208 cropHeight = cropWidth; 209 } else { 210 cropLeft = (cropWidth - cropHeight) / 2; 211 cropWidth = cropHeight; 212 } 213 } 214 // Calculate the scale factor. We don't want to scale up, so the max scale is 1f. 215 final float scaleFactor = Math.min(1f, ((float) maxDim) / Math.max(cropWidth, cropHeight)); 216 217 if (scaleFactor < 1.0f || cropLeft != 0 || cropTop != 0 || originalHasAlpha) { 218 final int newWidth = (int) (cropWidth * scaleFactor); 219 final int newHeight = (int) (cropHeight * scaleFactor); 220 if (newWidth <= 0 || newHeight <= 0) { 221 throw new IOException("Invalid bitmap dimensions"); 222 } 223 final Bitmap scaledBitmap = Bitmap.createBitmap(newWidth, newHeight, 224 Bitmap.Config.ARGB_8888); 225 final Canvas c = new Canvas(scaledBitmap); 226 227 if (originalHasAlpha) { 228 c.drawRect(0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight(), WHITE_PAINT); 229 } 230 231 final Rect src = new Rect(cropLeft, cropTop, 232 cropLeft + cropWidth, cropTop + cropHeight); 233 final RectF dst = new RectF(0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight()); 234 235 c.drawBitmap(original, src, dst, null); 236 return scaledBitmap; 237 } else { 238 return original; 239 } 240 } 241 242 /** 243 * Helper method to compress the given bitmap as a JPEG and return the resulting byte array. 244 */ getCompressedBytes(Bitmap b, int quality)245 private byte[] getCompressedBytes(Bitmap b, int quality) throws IOException { 246 final ByteArrayOutputStream baos = new ByteArrayOutputStream(); 247 final boolean compressed = b.compress(Bitmap.CompressFormat.JPEG, quality, baos); 248 baos.flush(); 249 baos.close(); 250 byte[] result = baos.toByteArray(); 251 252 if (!compressed) { 253 throw new IOException("Unable to compress image"); 254 } 255 return result; 256 } 257 258 /** 259 * Retrieves the uncompressed display photo. 260 */ getDisplayPhoto()261 public Bitmap getDisplayPhoto() { 262 return mDisplayPhoto; 263 } 264 265 /** 266 * Retrieves the uncompressed thumbnail photo. 267 */ getThumbnailPhoto()268 public Bitmap getThumbnailPhoto() { 269 return mThumbnailPhoto; 270 } 271 272 /** 273 * Retrieves the compressed display photo as a byte array. 274 */ getDisplayPhotoBytes()275 public byte[] getDisplayPhotoBytes() throws IOException { 276 return getCompressedBytes(mDisplayPhoto, COMPRESSION_DISPLAY_PHOTO); 277 } 278 279 /** 280 * Retrieves the compressed thumbnail photo as a byte array. 281 */ getThumbnailPhotoBytes()282 public byte[] getThumbnailPhotoBytes() throws IOException { 283 // If there is a higher-resolution picture, we can assume we won't need to upscale the 284 // thumbnail often, so we can compress stronger 285 final boolean hasDisplayPhoto = mDisplayPhoto != null && 286 (mDisplayPhoto.getWidth() > mThumbnailPhoto.getWidth() || 287 mDisplayPhoto.getHeight() > mThumbnailPhoto.getHeight()); 288 return getCompressedBytes(mThumbnailPhoto, 289 hasDisplayPhoto ? COMPRESSION_THUMBNAIL_LOW : COMPRESSION_THUMBNAIL_HIGH); 290 } 291 292 /** 293 * Retrieves the maximum width or height (in pixels) of the display photo. 294 */ getMaxDisplayPhotoDim()295 public int getMaxDisplayPhotoDim() { 296 return mMaxDisplayPhotoDim; 297 } 298 299 /** 300 * Retrieves the maximum width or height (in pixels) of the thumbnail. 301 */ getMaxThumbnailPhotoDim()302 public int getMaxThumbnailPhotoDim() { 303 return mMaxThumbnailPhotoDim; 304 } 305 306 /** 307 * Returns the maximum size in pixel of a thumbnail (which has a default that can be overriden 308 * using a system-property) 309 */ getMaxThumbnailSize()310 public static int getMaxThumbnailSize() { 311 return sMaxThumbnailDim; 312 } 313 314 /** 315 * Returns the maximum size in pixel of a display photo (which is determined based 316 * on available RAM or configured using a system-property) 317 */ getMaxDisplayPhotoSize()318 public static int getMaxDisplayPhotoSize() { 319 return sMaxDisplayPhotoDim; 320 } 321 } 322