/* * 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.os.SystemProperties; 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 = SystemProperties.getInt( PhotoSizes.SYS_PROPERTY_THUMBNAIL_SIZE, PhotoSizes.DEFAULT_THUMBNAIL); sMaxDisplayPhotoDim = SystemProperties.getInt( PhotoSizes.SYS_PROPERTY_DISPLAY_PHOTO_SIZE, 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; /** If present, overrides the size given in {@link #DEFAULT_THUMBNAIL} */ public static final String SYS_PROPERTY_THUMBNAIL_SIZE = "contacts.thumbnail_size"; /** If present, overrides the size determined for the display photo */ public static final String SYS_PROPERTY_DISPLAY_PHOTO_SIZE = "contacts.display_photo_size"; } 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; } }