diff options
Diffstat (limited to 'src/com/android/messaging/util/ImageUtils.java')
-rw-r--r-- | src/com/android/messaging/util/ImageUtils.java | 908 |
1 files changed, 0 insertions, 908 deletions
diff --git a/src/com/android/messaging/util/ImageUtils.java b/src/com/android/messaging/util/ImageUtils.java deleted file mode 100644 index 05d3678..0000000 --- a/src/com/android/messaging/util/ImageUtils.java +++ /dev/null @@ -1,908 +0,0 @@ -/* - * Copyright (C) 2015 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.messaging.util; - -import android.app.ActivityManager; -import android.content.ContentResolver; -import android.content.Context; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.BitmapShader; -import android.graphics.Canvas; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.graphics.PorterDuff; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.Shader.TileMode; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.provider.MediaStore; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import android.view.View; - -import com.android.messaging.Factory; -import com.android.messaging.datamodel.MediaScratchFileProvider; -import com.android.messaging.datamodel.MessagingContentProvider; -import com.android.messaging.datamodel.media.ImageRequest; -import com.android.messaging.util.Assert.DoesNotRunOnMainThread; -import com.android.messaging.util.exif.ExifInterface; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.io.Files; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.Arrays; - -public class ImageUtils { - private static final String TAG = LogUtil.BUGLE_TAG; - private static final int MAX_OOM_COUNT = 1; - private static final byte[] GIF87_HEADER = "GIF87a".getBytes(Charset.forName("US-ASCII")); - private static final byte[] GIF89_HEADER = "GIF89a".getBytes(Charset.forName("US-ASCII")); - - // Used for drawBitmapWithCircleOnCanvas. - // Default color is transparent for both circle background and stroke. - public static final int DEFAULT_CIRCLE_BACKGROUND_COLOR = 0; - public static final int DEFAULT_CIRCLE_STROKE_COLOR = 0; - - private static volatile ImageUtils sInstance; - - public static ImageUtils get() { - if (sInstance == null) { - synchronized (ImageUtils.class) { - if (sInstance == null) { - sInstance = new ImageUtils(); - } - } - } - return sInstance; - } - - @VisibleForTesting - public static void set(final ImageUtils imageUtils) { - sInstance = imageUtils; - } - - /** - * Transforms a bitmap into a byte array. - * - * @param quality Value between 0 and 100 that the compressor uses to discern what quality the - * resulting bytes should be - * @param bitmap Bitmap to convert into bytes - * @return byte array of bitmap - */ - public static byte[] bitmapToBytes(final Bitmap bitmap, final int quality) - throws OutOfMemoryError { - boolean done = false; - int oomCount = 0; - byte[] imageBytes = null; - while (!done) { - try { - final ByteArrayOutputStream os = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.JPEG, quality, os); - imageBytes = os.toByteArray(); - done = true; - } catch (final OutOfMemoryError e) { - LogUtil.w(TAG, "OutOfMemory converting bitmap to bytes."); - oomCount++; - if (oomCount <= MAX_OOM_COUNT) { - Factory.get().reclaimMemory(); - } else { - done = true; - LogUtil.w(TAG, "Failed to convert bitmap to bytes. Out of Memory."); - } - throw e; - } - } - return imageBytes; - } - - /** - * Given the source bitmap and a canvas, draws the bitmap through a circular - * mask. Only draws a circle with diameter equal to the destination width. - * - * @param bitmap The source bitmap to draw. - * @param canvas The canvas to draw it on. - * @param source The source bound of the bitmap. - * @param dest The destination bound on the canvas. - * @param bitmapPaint Optional Paint object for the bitmap - * @param fillBackground when set, fill the circle with backgroundColor - * @param strokeColor draw a border outside the circle with strokeColor - */ - public static void drawBitmapWithCircleOnCanvas(final Bitmap bitmap, final Canvas canvas, - final RectF source, final RectF dest, @Nullable Paint bitmapPaint, - final boolean fillBackground, final int backgroundColor, int strokeColor) { - // Draw bitmap through shader first. - final BitmapShader shader = new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP); - final Matrix matrix = new Matrix(); - - // Fit bitmap to bounds. - matrix.setRectToRect(source, dest, Matrix.ScaleToFit.CENTER); - - shader.setLocalMatrix(matrix); - - if (bitmapPaint == null) { - bitmapPaint = new Paint(); - } - - bitmapPaint.setAntiAlias(true); - if (fillBackground) { - bitmapPaint.setColor(backgroundColor); - canvas.drawCircle(dest.centerX(), dest.centerX(), dest.width() / 2f, bitmapPaint); - } - - bitmapPaint.setShader(shader); - canvas.drawCircle(dest.centerX(), dest.centerX(), dest.width() / 2f, bitmapPaint); - bitmapPaint.setShader(null); - - if (strokeColor != 0) { - final Paint stroke = new Paint(); - stroke.setAntiAlias(true); - stroke.setColor(strokeColor); - stroke.setStyle(Paint.Style.STROKE); - final float strokeWidth = 6f; - stroke.setStrokeWidth(strokeWidth); - canvas.drawCircle(dest.centerX(), - dest.centerX(), - dest.width() / 2f - stroke.getStrokeWidth() / 2f, - stroke); - } - } - - /** - * Sets a drawable to the background of a view. setBackgroundDrawable() is deprecated since - * JB and replaced by setBackground(). - */ - @SuppressWarnings("deprecation") - public static void setBackgroundDrawableOnView(final View view, final Drawable drawable) { - if (OsUtil.isAtLeastJB()) { - view.setBackground(drawable); - } else { - view.setBackgroundDrawable(drawable); - } - } - - /** - * Based on the input bitmap bounds given by BitmapFactory.Options, compute the required - * sub-sampling size for loading a scaled down version of the bitmap to the required size - * @param options a BitmapFactory.Options instance containing the bounds info of the bitmap - * @param reqWidth the desired width of the bitmap. Can be ImageRequest.UNSPECIFIED_SIZE. - * @param reqHeight the desired height of the bitmap. Can be ImageRequest.UNSPECIFIED_SIZE. - * @return - */ - public int calculateInSampleSize( - final BitmapFactory.Options options, final int reqWidth, final int reqHeight) { - // Raw height and width of image - final int height = options.outHeight; - final int width = options.outWidth; - int inSampleSize = 1; - - final boolean checkHeight = reqHeight != ImageRequest.UNSPECIFIED_SIZE; - final boolean checkWidth = reqWidth != ImageRequest.UNSPECIFIED_SIZE; - if ((checkHeight && height > reqHeight) || - (checkWidth && width > reqWidth)) { - - final int halfHeight = height / 2; - final int halfWidth = width / 2; - - // Calculate the largest inSampleSize value that is a power of 2 and keeps both - // height and width larger than the requested height and width. - while ((!checkHeight || (halfHeight / inSampleSize) > reqHeight) - && (!checkWidth || (halfWidth / inSampleSize) > reqWidth)) { - inSampleSize *= 2; - } - } - - return inSampleSize; - } - - private static final String[] MEDIA_CONTENT_PROJECTION = new String[] { - MediaStore.MediaColumns.MIME_TYPE - }; - - private static final int INDEX_CONTENT_TYPE = 0; - - @DoesNotRunOnMainThread - public static String getContentType(final ContentResolver cr, final Uri uri) { - // Figure out the content type of media. - String contentType = null; - Cursor cursor = null; - if (UriUtil.isMediaStoreUri(uri)) { - try { - cursor = cr.query(uri, MEDIA_CONTENT_PROJECTION, null, null, null); - - if (cursor != null && cursor.moveToFirst()) { - contentType = cursor.getString(INDEX_CONTENT_TYPE); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - if (contentType == null) { - // Last ditch effort to get the content type. Look at the file extension. - contentType = ContentType.getContentTypeFromExtension(uri.toString(), - ContentType.IMAGE_UNSPECIFIED); - } - return contentType; - } - - /** - * @param context Android context - * @param uri Uri to the image data - * @return The exif orientation value for the image in the specified uri - */ - public static int getOrientation(final Context context, final Uri uri) { - try { - return getOrientation(context.getContentResolver().openInputStream(uri)); - } catch (FileNotFoundException e) { - LogUtil.e(TAG, "getOrientation couldn't open: " + uri, e); - } - return android.media.ExifInterface.ORIENTATION_UNDEFINED; - } - - /** - * @param inputStream The stream to the image file. Closed on completion - * @return The exif orientation value for the image in the specified stream - */ - public static int getOrientation(final InputStream inputStream) { - int orientation = android.media.ExifInterface.ORIENTATION_UNDEFINED; - if (inputStream != null) { - try { - final ExifInterface exifInterface = new ExifInterface(); - exifInterface.readExif(inputStream); - final Integer orientationValue = - exifInterface.getTagIntValue(ExifInterface.TAG_ORIENTATION); - if (orientationValue != null) { - orientation = orientationValue.intValue(); - } - } catch (IOException e) { - // If the image if GIF, PNG, or missing exif header, just use the defaults - } finally { - try { - if (inputStream != null) { - inputStream.close(); - } - } catch (IOException e) { - LogUtil.e(TAG, "getOrientation error closing input stream", e); - } - } - } - return orientation; - } - - /** - * Returns whether the resource is a GIF image. - */ - public static boolean isGif(String contentType, Uri contentUri) { - if (TextUtils.equals(contentType, ContentType.IMAGE_GIF)) { - return true; - } - if (ContentType.isImageType(contentType)) { - try { - ContentResolver contentResolver = Factory.get().getApplicationContext() - .getContentResolver(); - InputStream inputStream = contentResolver.openInputStream(contentUri); - return ImageUtils.isGif(inputStream); - } catch (Exception e) { - LogUtil.w(TAG, "Could not open GIF input stream", e); - } - } - // Assume anything with a non-image content type is not a GIF - return false; - } - - /** - * @param inputStream The stream to the image file. Closed on completion - * @return Whether the image stream represents a GIF - */ - public static boolean isGif(InputStream inputStream) { - if (inputStream != null) { - try { - byte[] gifHeaderBytes = new byte[6]; - int value = inputStream.read(gifHeaderBytes, 0, 6); - if (value == 6) { - return Arrays.equals(gifHeaderBytes, GIF87_HEADER) - || Arrays.equals(gifHeaderBytes, GIF89_HEADER); - } - } catch (IOException e) { - return false; - } finally { - try { - inputStream.close(); - } catch (IOException e) { - // Ignore - } - } - } - return false; - } - - /** - * Read an image and compress it to particular max dimensions and size. - * Used to ensure images can fit in an MMS. - * TODO: This uses memory very inefficiently as it processes the whole image as a unit - * (rather than slice by slice) but system JPEG functions do not support slicing and dicing. - */ - public static class ImageResizer { - - /** - * The quality parameter which is used to compress JPEG images. - */ - private static final int IMAGE_COMPRESSION_QUALITY = 95; - /** - * The minimum quality parameter which is used to compress JPEG images. - */ - private static final int MINIMUM_IMAGE_COMPRESSION_QUALITY = 50; - - /** - * Minimum factor to reduce quality value - */ - private static final double QUALITY_SCALE_DOWN_RATIO = 0.85f; - - /** - * Maximum passes through the resize loop before failing permanently - */ - private static final int NUMBER_OF_RESIZE_ATTEMPTS = 6; - - /** - * Amount to scale down the picture when it doesn't fit - */ - private static final float MIN_SCALE_DOWN_RATIO = 0.75f; - - /** - * When computing sampleSize target scaling of no more than this ratio - */ - private static final float MAX_TARGET_SCALE_FACTOR = 1.5f; - - - // Current sample size for subsampling image during initial decode - private int mSampleSize; - // Current bitmap holding initial decoded source image - private Bitmap mDecoded; - // If scaling is needed this holds the scaled bitmap (else should equal mDecoded) - private Bitmap mScaled; - // Current JPEG compression quality to use when compressing image - private int mQuality; - // Current factor to scale down decoded image before compressing - private float mScaleFactor; - // Flag keeping track of whether cache memory has been reclaimed - private boolean mHasReclaimedMemory; - - // Initial size of the image (typically provided but can be UNSPECIFIED_SIZE) - private int mWidth; - private int mHeight; - // Orientation params of image as read from EXIF data - private final ExifInterface.OrientationParams mOrientationParams; - // Matrix to undo orientation and scale at the same time - private final Matrix mMatrix; - // Size limit as provided by MMS library - private final int mWidthLimit; - private final int mHeightLimit; - private final int mByteLimit; - // Uri from which to read source image - private final Uri mUri; - // Application context - private final Context mContext; - // Cached value of bitmap factory options - private final BitmapFactory.Options mOptions; - private final String mContentType; - - private final int mMemoryClass; - - /** - * Return resized (compressed) image (else null) - * - * @param width The width of the image (if known) - * @param height The height of the image (if known) - * @param orientation The orientation of the image as an ExifInterface constant - * @param widthLimit The width limit, in pixels - * @param heightLimit The height limit, in pixels - * @param byteLimit The binary size limit, in bytes - * @param uri Uri to the image data - * @param context Needed to open the image - * @param contentType of image - * @return encoded image meeting size requirements else null - */ - public static byte[] getResizedImageData(final int width, final int height, - final int orientation, final int widthLimit, final int heightLimit, - final int byteLimit, final Uri uri, final Context context, - final String contentType) { - final ImageResizer resizer = new ImageResizer(width, height, orientation, - widthLimit, heightLimit, byteLimit, uri, context, contentType); - return resizer.resize(); - } - - /** - * Create and initialize an image resizer - */ - private ImageResizer(final int width, final int height, final int orientation, - final int widthLimit, final int heightLimit, final int byteLimit, final Uri uri, - final Context context, final String contentType) { - mWidth = width; - mHeight = height; - mOrientationParams = ExifInterface.getOrientationParams(orientation); - mMatrix = new Matrix(); - mWidthLimit = widthLimit; - mHeightLimit = heightLimit; - mByteLimit = byteLimit; - mUri = uri; - mWidth = width; - mContext = context; - mQuality = IMAGE_COMPRESSION_QUALITY; - mScaleFactor = 1.0f; - mHasReclaimedMemory = false; - mOptions = new BitmapFactory.Options(); - mOptions.inScaled = false; - mOptions.inDensity = 0; - mOptions.inTargetDensity = 0; - mOptions.inSampleSize = 1; - mOptions.inJustDecodeBounds = false; - mOptions.inMutable = false; - final ActivityManager am = - (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - mMemoryClass = Math.max(16, am.getMemoryClass()); - mContentType = contentType; - } - - /** - * Try to compress the image - * - * @return encoded image meeting size requirements else null - */ - private byte[] resize() { - return ImageUtils.isGif(mContentType, mUri) ? resizeGifImage() : resizeStaticImage(); - } - - private byte[] resizeGifImage() { - byte[] bytesToReturn = null; - final String inputFilePath; - if (MediaScratchFileProvider.isMediaScratchSpaceUri(mUri)) { - inputFilePath = MediaScratchFileProvider.getFileFromUri(mUri).getAbsolutePath(); - } else { - if (!TextUtils.equals(mUri.getScheme(), ContentResolver.SCHEME_FILE)) { - Assert.fail("Expected a GIF file uri, but actual uri = " + mUri.toString()); - } - inputFilePath = mUri.getPath(); - } - - if (GifTranscoder.canBeTranscoded(mWidth, mHeight)) { - // Needed to perform the transcoding so that the gif can continue to play in the - // conversation while the sending is taking place - final Uri tmpUri = MediaScratchFileProvider.buildMediaScratchSpaceUri("gif"); - final File outputFile = MediaScratchFileProvider.getFileFromUri(tmpUri); - final String outputFilePath = outputFile.getAbsolutePath(); - - final boolean success = - GifTranscoder.transcode(mContext, inputFilePath, outputFilePath); - if (success) { - try { - bytesToReturn = Files.toByteArray(outputFile); - } catch (IOException e) { - LogUtil.e(TAG, "Could not create FileInputStream with path of " - + outputFilePath, e); - } - } - - // Need to clean up the new file created to compress the gif - mContext.getContentResolver().delete(tmpUri, null, null); - } else { - // We don't want to transcode the gif because its image dimensions would be too - // small so just return the bytes of the original gif - try { - bytesToReturn = Files.toByteArray(new File(inputFilePath)); - } catch (IOException e) { - LogUtil.e(TAG, - "Could not create FileInputStream with path of " + inputFilePath, e); - } - } - - return bytesToReturn; - } - - private byte[] resizeStaticImage() { - if (!ensureImageSizeSet()) { - // Cannot read image size - return null; - } - // Find incoming image size - if (!canBeCompressed()) { - return null; - } - - // Decode image - if out of memory - reclaim memory and retry - try { - for (int attempts = 0; attempts < NUMBER_OF_RESIZE_ATTEMPTS; attempts++) { - final byte[] encoded = recodeImage(attempts); - - // Only return data within the limit - if (encoded != null && encoded.length <= mByteLimit) { - return encoded; - } else { - final int currentSize = (encoded == null ? 0 : encoded.length); - updateRecodeParameters(currentSize); - } - } - } catch (final FileNotFoundException e) { - LogUtil.e(TAG, "File disappeared during resizing"); - } finally { - // Release all bitmaps - if (mScaled != null && mScaled != mDecoded) { - mScaled.recycle(); - } - if (mDecoded != null) { - mDecoded.recycle(); - } - } - return null; - } - - /** - * Ensure that the width and height of the source image are known - * @return flag indicating whether size is known - */ - private boolean ensureImageSizeSet() { - if (mWidth == MessagingContentProvider.UNSPECIFIED_SIZE || - mHeight == MessagingContentProvider.UNSPECIFIED_SIZE) { - // First get the image data (compressed) - final ContentResolver cr = mContext.getContentResolver(); - InputStream inputStream = null; - // Find incoming image size - try { - mOptions.inJustDecodeBounds = true; - inputStream = cr.openInputStream(mUri); - BitmapFactory.decodeStream(inputStream, null, mOptions); - - mWidth = mOptions.outWidth; - mHeight = mOptions.outHeight; - mOptions.inJustDecodeBounds = false; - - return true; - } catch (final FileNotFoundException e) { - LogUtil.e(TAG, "Could not open file corresponding to uri " + mUri, e); - } catch (final NullPointerException e) { - LogUtil.e(TAG, "NPE trying to open the uri " + mUri, e); - } finally { - if (inputStream != null) { - try { - inputStream.close(); - } catch (final IOException e) { - // Nothing to do - } - } - } - - return false; - } - return true; - } - - /** - * Choose an initial subsamplesize that ensures the decoded image is no more than - * MAX_TARGET_SCALE_FACTOR bigger than largest supported image and that it is likely to - * compress to smaller than the target size (assuming compression down to 1 bit per pixel). - * @return whether the image can be down subsampled - */ - private boolean canBeCompressed() { - final boolean logv = LogUtil.isLoggable(LogUtil.BUGLE_IMAGE_TAG, LogUtil.VERBOSE); - - int imageHeight = mHeight; - int imageWidth = mWidth; - - // Assume can use half working memory to decode the initial image (4 bytes per pixel) - final int workingMemoryPixelLimit = (mMemoryClass * 1024 * 1024 / 8); - // Target 1 bits per pixel in final compressed image - final int finalSizePixelLimit = mByteLimit * 8; - // When choosing to halve the resolution - only do so the image will still be too big - // after scaling by MAX_TARGET_SCALE_FACTOR - final int heightLimitWithSlop = (int) (mHeightLimit * MAX_TARGET_SCALE_FACTOR); - final int widthLimitWithSlop = (int) (mWidthLimit * MAX_TARGET_SCALE_FACTOR); - final int pixelLimitWithSlop = (int) (finalSizePixelLimit * - MAX_TARGET_SCALE_FACTOR * MAX_TARGET_SCALE_FACTOR); - final int pixelLimit = Math.min(pixelLimitWithSlop, workingMemoryPixelLimit); - - int sampleSize = 1; - boolean fits = (imageHeight < heightLimitWithSlop && - imageWidth < widthLimitWithSlop && - imageHeight * imageWidth < pixelLimit); - - // Compare sizes to compute sub-sampling needed - while (!fits) { - sampleSize = sampleSize * 2; - // Note that recodeImage may try using mSampleSize * 2. Hence we use the factor of 4 - if (sampleSize >= (Integer.MAX_VALUE / 4)) { - LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, String.format( - "Cannot resize image: widthLimit=%d heightLimit=%d byteLimit=%d " + - "imageWidth=%d imageHeight=%d", mWidthLimit, mHeightLimit, mByteLimit, - mWidth, mHeight)); - Assert.fail("Image cannot be resized"); // http://b/18926934 - return false; - } - if (logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, - "computeInitialSampleSize: Increasing sampleSize to " + sampleSize - + " as h=" + imageHeight + " vs " + heightLimitWithSlop - + " w=" + imageWidth + " vs " + widthLimitWithSlop - + " p=" + imageHeight * imageWidth + " vs " + pixelLimit); - } - imageHeight = mHeight / sampleSize; - imageWidth = mWidth / sampleSize; - fits = (imageHeight < heightLimitWithSlop && - imageWidth < widthLimitWithSlop && - imageHeight * imageWidth < pixelLimit); - } - - if (logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, - "computeInitialSampleSize: Initial sampleSize " + sampleSize - + " for h=" + imageHeight + " vs " + heightLimitWithSlop - + " w=" + imageWidth + " vs " + widthLimitWithSlop - + " p=" + imageHeight * imageWidth + " vs " + pixelLimit); - } - - mSampleSize = sampleSize; - return true; - } - - /** - * Recode the image from initial Uri to encoded JPEG - * @param attempt Attempt number - * @return encoded image - */ - private byte[] recodeImage(final int attempt) throws FileNotFoundException { - byte[] encoded = null; - try { - final ContentResolver cr = mContext.getContentResolver(); - final boolean logv = LogUtil.isLoggable(LogUtil.BUGLE_IMAGE_TAG, LogUtil.VERBOSE); - if (logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, "getResizedImageData: attempt=" + attempt - + " limit (w=" + mWidthLimit + " h=" + mHeightLimit + ") quality=" - + mQuality + " scale=" + mScaleFactor + " sampleSize=" + mSampleSize); - } - if (mScaled == null) { - if (mDecoded == null) { - mOptions.inSampleSize = mSampleSize; - final InputStream inputStream = cr.openInputStream(mUri); - mDecoded = BitmapFactory.decodeStream(inputStream, null, mOptions); - if (mDecoded == null) { - if (logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, - "getResizedImageData: got empty decoded bitmap"); - } - return null; - } - } - if (logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, "getResizedImageData: decoded w,h=" - + mDecoded.getWidth() + "," + mDecoded.getHeight()); - } - // Make sure to scale the decoded image if dimension is not within limit - final int decodedWidth = mDecoded.getWidth(); - final int decodedHeight = mDecoded.getHeight(); - if (decodedWidth > mWidthLimit || decodedHeight > mHeightLimit) { - final float minScaleFactor = Math.max( - mWidthLimit == 0 ? 1.0f : - (float) decodedWidth / (float) mWidthLimit, - mHeightLimit == 0 ? 1.0f : - (float) decodedHeight / (float) mHeightLimit); - if (mScaleFactor < minScaleFactor) { - mScaleFactor = minScaleFactor; - } - } - if (mScaleFactor > 1.0 || mOrientationParams.rotation != 0) { - mMatrix.reset(); - mMatrix.postRotate(mOrientationParams.rotation); - mMatrix.postScale(mOrientationParams.scaleX / mScaleFactor, - mOrientationParams.scaleY / mScaleFactor); - mScaled = Bitmap.createBitmap(mDecoded, 0, 0, decodedWidth, decodedHeight, - mMatrix, false /* filter */); - if (mScaled == null) { - if (logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, - "getResizedImageData: got empty scaled bitmap"); - } - return null; - } - if (logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, "getResizedImageData: scaled w,h=" - + mScaled.getWidth() + "," + mScaled.getHeight()); - } - } else { - mScaled = mDecoded; - } - } - // Now encode it at current quality - encoded = ImageUtils.bitmapToBytes(mScaled, mQuality); - if (encoded != null && logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, - "getResizedImageData: Encoded down to " + encoded.length + "@" - + mScaled.getWidth() + "/" + mScaled.getHeight() + "~" - + mQuality); - } - } catch (final OutOfMemoryError e) { - LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, - "getResizedImageData - image too big (OutOfMemoryError), will try " - + " with smaller scale factor"); - // fall through and keep trying with more compression - } - return encoded; - } - - /** - * When image recode fails this method updates compression parameters for the next attempt - * @param currentSize encoded image size (will be 0 if OOM) - */ - private void updateRecodeParameters(final int currentSize) { - final boolean logv = LogUtil.isLoggable(LogUtil.BUGLE_IMAGE_TAG, LogUtil.VERBOSE); - // Only return data within the limit - if (currentSize > 0 && - mQuality > MINIMUM_IMAGE_COMPRESSION_QUALITY) { - // First if everything succeeded but failed to hit target size - // Try quality proportioned to sqrt of size over size limit - mQuality = Math.max(MINIMUM_IMAGE_COMPRESSION_QUALITY, - Math.min((int) (mQuality * Math.sqrt((1.0 * mByteLimit) / currentSize)), - (int) (mQuality * QUALITY_SCALE_DOWN_RATIO))); - if (logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, - "getResizedImageData: Retrying at quality " + mQuality); - } - } else if (currentSize > 0 && - mScaleFactor < 2.0 * MIN_SCALE_DOWN_RATIO * MIN_SCALE_DOWN_RATIO) { - // JPEG compression failed to hit target size - need smaller image - // First try scaling by a little (< factor of 2) just so long resulting scale down - // ratio is still significantly bigger than next subsampling step - // i.e. mScaleFactor/MIN_SCALE_DOWN_RATIO (new scaling factor) < - // 2.0 / MIN_SCALE_DOWN_RATIO (arbitrary limit) - mQuality = IMAGE_COMPRESSION_QUALITY; - mScaleFactor = mScaleFactor / MIN_SCALE_DOWN_RATIO; - if (logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, - "getResizedImageData: Retrying at scale " + mScaleFactor); - } - // Release scaled bitmap to trigger rescaling - if (mScaled != null && mScaled != mDecoded) { - mScaled.recycle(); - } - mScaled = null; - } else if (currentSize <= 0 && !mHasReclaimedMemory) { - // Then before we subsample try cleaning up our cached memory - Factory.get().reclaimMemory(); - mHasReclaimedMemory = true; - if (logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, - "getResizedImageData: Retrying after reclaiming memory "); - } - } else { - // Last resort - subsample image by another factor of 2 and try again - mSampleSize = mSampleSize * 2; - mQuality = IMAGE_COMPRESSION_QUALITY; - mScaleFactor = 1.0f; - if (logv) { - LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, - "getResizedImageData: Retrying at sampleSize " + mSampleSize); - } - // Release all bitmaps to trigger subsampling - if (mScaled != null && mScaled != mDecoded) { - mScaled.recycle(); - } - mScaled = null; - if (mDecoded != null) { - mDecoded.recycle(); - mDecoded = null; - } - } - } - } - - /** - * Scales and center-crops a bitmap to the size passed in and returns the new bitmap. - * - * @param source Bitmap to scale and center-crop - * @param newWidth destination width - * @param newHeight destination height - * @return Bitmap scaled and center-cropped bitmap - */ - public static Bitmap scaleCenterCrop(final Bitmap source, final int newWidth, - final int newHeight) { - final int sourceWidth = source.getWidth(); - final int sourceHeight = source.getHeight(); - - // Compute the scaling factors to fit the new height and width, respectively. - // To cover the final image, the final scaling will be the bigger - // of these two. - final float xScale = (float) newWidth / sourceWidth; - final float yScale = (float) newHeight / sourceHeight; - final float scale = Math.max(xScale, yScale); - - // Now get the size of the source bitmap when scaled - final float scaledWidth = scale * sourceWidth; - final float scaledHeight = scale * sourceHeight; - - // Let's find out the upper left coordinates if the scaled bitmap - // should be centered in the new size give by the parameters - final float left = (newWidth - scaledWidth) / 2; - final float top = (newHeight - scaledHeight) / 2; - - // The target rectangle for the new, scaled version of the source bitmap will now - // be - final RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight); - - // Finally, we create a new bitmap of the specified size and draw our new, - // scaled bitmap onto it. - final Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, source.getConfig()); - final Canvas canvas = new Canvas(dest); - canvas.drawBitmap(source, null, targetRect, null); - - return dest; - } - - /** - * The drawable can be a Nine-Patch. If we directly use the same drawable instance for each - * drawable of different sizes, then the drawable sizes would interfere with each other. The - * solution here is to create a new drawable instance for every time with the SAME - * ConstantState (i.e. sharing the same common state such as the bitmap, so that we don't have - * to recreate the bitmap resource), and apply the different properties on top (nine-patch - * size and color tint). - * - * TODO: we are creating new drawable instances here, but there are optimizations that - * can be made. For example, message bubbles shouldn't need the mutate() call and the - * play/pause buttons shouldn't need to create new drawable from the constant state. - */ - public static Drawable getTintedDrawable(final Context context, final Drawable drawable, - final int color) { - // For some reason occassionally drawables on JB has a null constant state - final Drawable.ConstantState constantStateDrawable = drawable.getConstantState(); - final Drawable retDrawable = (constantStateDrawable != null) - ? constantStateDrawable.newDrawable(context.getResources()).mutate() - : drawable; - retDrawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP); - return retDrawable; - } - - /** - * Decodes image resource header and returns the image size. - */ - public static Rect decodeImageBounds(final Context context, final Uri imageUri) { - final ContentResolver cr = context.getContentResolver(); - try { - final InputStream inputStream = cr.openInputStream(imageUri); - if (inputStream != null) { - try { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeStream(inputStream, null, options); - return new Rect(0, 0, options.outWidth, options.outHeight); - } finally { - try { - inputStream.close(); - } catch (IOException e) { - // Do nothing. - } - } - } - } catch (FileNotFoundException e) { - LogUtil.e(TAG, "Couldn't open input stream for uri = " + imageUri); - } - return new Rect(0, 0, ImageRequest.UNSPECIFIED_SIZE, ImageRequest.UNSPECIFIED_SIZE); - } -} |