blob: 643d92df524ac364bd0e7d04fd73347339217601 [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License
*/
package com.android.providers.contacts;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.sysprop.ContactsProperties;
import com.android.providers.contacts.util.MemoryUtils;
import com.google.common.annotations.VisibleForTesting;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
* Class that converts a bitmap (or byte array representing a bitmap) into a display
* photo and a thumbnail photo.
*/
/* package-protected */ final class PhotoProcessor {
/** Compression for display photos. They are very big, so we can use a strong compression */
private static final int COMPRESSION_DISPLAY_PHOTO = 75;
/**
* Compression for thumbnails that don't have a full size photo. Those can be blown up
* full-screen, so we want to make sure we don't introduce JPEG artifacts here
*/
private static final int COMPRESSION_THUMBNAIL_HIGH = 95;
/** Compression for thumbnails that also have a display photo */
private static final int COMPRESSION_THUMBNAIL_LOW = 90;
private static final Paint WHITE_PAINT = new Paint();
static {
WHITE_PAINT.setColor(Color.WHITE);
}
private static int sMaxThumbnailDim;
private static int sMaxDisplayPhotoDim;
static {
final boolean isExpensiveDevice =
MemoryUtils.getTotalMemorySize() >= PhotoSizes.LARGE_RAM_THRESHOLD;
sMaxThumbnailDim = ContactsProperties.thumbnail_size().orElse(
PhotoSizes.DEFAULT_THUMBNAIL);
sMaxDisplayPhotoDim = ContactsProperties.display_photo_size().orElse(
isExpensiveDevice
? PhotoSizes.DEFAULT_DISPLAY_PHOTO_LARGE_MEMORY
: PhotoSizes.DEFAULT_DISPLAY_PHOTO_MEMORY_CONSTRAINED);
}
/**
* The default sizes of a thumbnail/display picture. This is used in {@link #initialize()}
*/
private interface PhotoSizes {
/** Size of a thumbnail */
public static final int DEFAULT_THUMBNAIL = 96;
/**
* Size of a display photo on memory constrained devices (those are devices with less than
* {@link #DEFAULT_LARGE_RAM_THRESHOLD} of reported RAM
*/
public static final int DEFAULT_DISPLAY_PHOTO_MEMORY_CONSTRAINED = 480;
/**
* Size of a display photo on devices with enough ram (those are devices with at least
* {@link #DEFAULT_LARGE_RAM_THRESHOLD} of reported RAM
*/
public static final int DEFAULT_DISPLAY_PHOTO_LARGE_MEMORY = 720;
/**
* If the device has less than this amount of RAM, it is considered RAM constrained for
* photos
*/
public static final int LARGE_RAM_THRESHOLD = 640 * 1024 * 1024;
}
private final int mMaxDisplayPhotoDim;
private final int mMaxThumbnailPhotoDim;
private final boolean mForceCropToSquare;
private final Bitmap mOriginal;
private Bitmap mDisplayPhoto;
private Bitmap mThumbnailPhoto;
/**
* Initializes a photo processor for the given bitmap.
* @param original The bitmap to process.
* @param maxDisplayPhotoDim The maximum height and width for the display photo.
* @param maxThumbnailPhotoDim The maximum height and width for the thumbnail photo.
* @throws IOException If bitmap decoding or scaling fails.
*/
public PhotoProcessor(Bitmap original, int maxDisplayPhotoDim, int maxThumbnailPhotoDim)
throws IOException {
this(original, maxDisplayPhotoDim, maxThumbnailPhotoDim, false);
}
/**
* Initializes a photo processor for the given bitmap.
* @param originalBytes A byte array to decode into a bitmap to process.
* @param maxDisplayPhotoDim The maximum height and width for the display photo.
* @param maxThumbnailPhotoDim The maximum height and width for the thumbnail photo.
* @throws IOException If bitmap decoding or scaling fails.
*/
public PhotoProcessor(byte[] originalBytes, int maxDisplayPhotoDim, int maxThumbnailPhotoDim)
throws IOException {
this(BitmapFactory.decodeByteArray(originalBytes, 0, originalBytes.length),
maxDisplayPhotoDim, maxThumbnailPhotoDim, false);
}
/**
* Initializes a photo processor for the given bitmap.
* @param original The bitmap to process.
* @param maxDisplayPhotoDim The maximum height and width for the display photo.
* @param maxThumbnailPhotoDim The maximum height and width for the thumbnail photo.
* @param forceCropToSquare Whether to force the processed images to be square. If the source
* photo is not square, this will crop to the square at the center of the image's rectangle.
* If this is not set to true, the image will simply be downscaled to fit in the given
* dimensions, retaining its original aspect ratio.
* @throws IOException If bitmap decoding or scaling fails.
*/
public PhotoProcessor(Bitmap original, int maxDisplayPhotoDim, int maxThumbnailPhotoDim,
boolean forceCropToSquare) throws IOException {
mOriginal = original;
mMaxDisplayPhotoDim = maxDisplayPhotoDim;
mMaxThumbnailPhotoDim = maxThumbnailPhotoDim;
mForceCropToSquare = forceCropToSquare;
process();
}
/**
* Initializes a photo processor for the given bitmap.
* @param originalBytes A byte array to decode into a bitmap to process.
* @param maxDisplayPhotoDim The maximum height and width for the display photo.
* @param maxThumbnailPhotoDim The maximum height and width for the thumbnail photo.
* @param forceCropToSquare Whether to force the processed images to be square. If the source
* photo is not square, this will crop to the square at the center of the image's rectangle.
* If this is not set to true, the image will simply be downscaled to fit in the given
* dimensions, retaining its original aspect ratio.
* @throws IOException If bitmap decoding or scaling fails.
*/
public PhotoProcessor(byte[] originalBytes, int maxDisplayPhotoDim, int maxThumbnailPhotoDim,
boolean forceCropToSquare) throws IOException {
this(BitmapFactory.decodeByteArray(originalBytes, 0, originalBytes.length),
maxDisplayPhotoDim, maxThumbnailPhotoDim, forceCropToSquare);
}
/**
* Processes the original image, producing a scaled-down display photo and thumbnail photo.
* @throws IOException If bitmap decoding or scaling fails.
*/
private void process() throws IOException {
if (mOriginal == null) {
throw new IOException("Invalid image file");
}
mDisplayPhoto = getNormalizedBitmap(mOriginal, mMaxDisplayPhotoDim, mForceCropToSquare);
mThumbnailPhoto = getNormalizedBitmap(mOriginal,mMaxThumbnailPhotoDim, mForceCropToSquare);
}
/**
* Scales down the original bitmap to fit within the given maximum width and height.
* If the bitmap already fits in those dimensions, the original bitmap will be
* returned unmodified unless the photo processor is set up to crop it to a square.
*
* Also, if the image has transparency, conevrt it to white.
*
* @param original Original bitmap
* @param maxDim Maximum width and height (in pixels) for the image.
* @param forceCropToSquare See {@link #PhotoProcessor(Bitmap, int, int, boolean)}
* @return A bitmap that fits the maximum dimensions.
* @throws IOException If bitmap decoding or scaling fails.
*/
@SuppressWarnings({"SuspiciousNameCombination"})
@VisibleForTesting
static Bitmap getNormalizedBitmap(Bitmap original, int maxDim, boolean forceCropToSquare)
throws IOException {
final boolean originalHasAlpha = original.hasAlpha();
// All cropXxx's are in the original coordinate.
int cropWidth = original.getWidth();
int cropHeight = original.getHeight();
int cropLeft = 0;
int cropTop = 0;
if (forceCropToSquare && cropWidth != cropHeight) {
// Crop the image to the square at its center.
if (cropHeight > cropWidth) {
cropTop = (cropHeight - cropWidth) / 2;
cropHeight = cropWidth;
} else {
cropLeft = (cropWidth - cropHeight) / 2;
cropWidth = cropHeight;
}
}
// Calculate the scale factor. We don't want to scale up, so the max scale is 1f.
final float scaleFactor = Math.min(1f, ((float) maxDim) / Math.max(cropWidth, cropHeight));
if (scaleFactor < 1.0f || cropLeft != 0 || cropTop != 0 || originalHasAlpha) {
final int newWidth = (int) (cropWidth * scaleFactor);
final int newHeight = (int) (cropHeight * scaleFactor);
if (newWidth <= 0 || newHeight <= 0) {
throw new IOException("Invalid bitmap dimensions");
}
final Bitmap scaledBitmap = Bitmap.createBitmap(newWidth, newHeight,
Bitmap.Config.ARGB_8888);
final Canvas c = new Canvas(scaledBitmap);
if (originalHasAlpha) {
c.drawRect(0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight(), WHITE_PAINT);
}
final Rect src = new Rect(cropLeft, cropTop,
cropLeft + cropWidth, cropTop + cropHeight);
final RectF dst = new RectF(0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight());
c.drawBitmap(original, src, dst, null);
return scaledBitmap;
} else {
return original;
}
}
/**
* Helper method to compress the given bitmap as a JPEG and return the resulting byte array.
*/
private byte[] getCompressedBytes(Bitmap b, int quality) throws IOException {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final boolean compressed = b.compress(Bitmap.CompressFormat.JPEG, quality, baos);
baos.flush();
baos.close();
byte[] result = baos.toByteArray();
if (!compressed) {
throw new IOException("Unable to compress image");
}
return result;
}
/**
* Retrieves the uncompressed display photo.
*/
public Bitmap getDisplayPhoto() {
return mDisplayPhoto;
}
/**
* Retrieves the uncompressed thumbnail photo.
*/
public Bitmap getThumbnailPhoto() {
return mThumbnailPhoto;
}
/**
* Retrieves the compressed display photo as a byte array.
*/
public byte[] getDisplayPhotoBytes() throws IOException {
return getCompressedBytes(mDisplayPhoto, COMPRESSION_DISPLAY_PHOTO);
}
/**
* Retrieves the compressed thumbnail photo as a byte array.
*/
public byte[] getThumbnailPhotoBytes() throws IOException {
// If there is a higher-resolution picture, we can assume we won't need to upscale the
// thumbnail often, so we can compress stronger
final boolean hasDisplayPhoto = mDisplayPhoto != null &&
(mDisplayPhoto.getWidth() > mThumbnailPhoto.getWidth() ||
mDisplayPhoto.getHeight() > mThumbnailPhoto.getHeight());
return getCompressedBytes(mThumbnailPhoto,
hasDisplayPhoto ? COMPRESSION_THUMBNAIL_LOW : COMPRESSION_THUMBNAIL_HIGH);
}
/**
* Retrieves the maximum width or height (in pixels) of the display photo.
*/
public int getMaxDisplayPhotoDim() {
return mMaxDisplayPhotoDim;
}
/**
* Retrieves the maximum width or height (in pixels) of the thumbnail.
*/
public int getMaxThumbnailPhotoDim() {
return mMaxThumbnailPhotoDim;
}
/**
* Returns the maximum size in pixel of a thumbnail (which has a default that can be overriden
* using a system-property)
*/
public static int getMaxThumbnailSize() {
return sMaxThumbnailDim;
}
/**
* Returns the maximum size in pixel of a display photo (which is determined based
* on available RAM or configured using a system-property)
*/
public static int getMaxDisplayPhotoSize() {
return sMaxDisplayPhotoDim;
}
}