blob: 4143d2d42a3f6d8e196b268ad7d388b8a8de53fa [file] [log] [blame]
/*
* Copyright (C) 2011 Google Inc.
* Licensed to 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.mail.photo.util;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore.Images;
import android.provider.MediaStore.Images.ImageColumns;
import android.provider.MediaStore.Images.Thumbnails;
import android.provider.MediaStore.MediaColumns;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import com.android.mail.R;
import com.android.mail.photo.PhotoViewActivity;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Image utilities
*/
public class ImageUtils {
/** Specifies no background colour should be added during image resizing */
public static int NO_COLOR = 0;
public static final int INSERT_PHOTO_DIALOG_ID = R.id.dialog_insert_photo;
// added from EsService
public static final int CROP_NONE = 0;
public static final int CROP_SQUARE = 1;
public static final int CROP_WIDE = 2;
private static int MICRO_KIND_MAX_DIMENSION = 0;
private static int MINI_KIND_MAX_DIMENSION = 0;
private static int DEFAULT_JPEG_QUALITY = 90;
// Logging
private static final String TAG = "ImageUtils";
// Paints and modes
private static final Paint sResizePaint = new Paint(Paint.FILTER_BITMAP_FLAG);
/** The paint used for cropped photos */
private static final Paint sCropPaint;
static {
sCropPaint = new Paint();
sCropPaint.setAntiAlias(true);
sCropPaint.setFilterBitmap(true);
sCropPaint.setDither(true);
}
private static final Paint sOutStrokePaint = new Paint();
static {
sOutStrokePaint.setStrokeWidth(1);
sOutStrokePaint.setStyle(Paint.Style.STROKE);
sOutStrokePaint.setColor(0xff999999);
}
private static final Paint sInStrokePaint = new Paint();
static {
sInStrokePaint.setStrokeWidth(1);
sInStrokePaint.setStyle(Paint.Style.STROKE);
sInStrokePaint.setColor(0xfff0f0f0);
}
/** Minimum class memory class to use full-res photos */
private final static long MIN_NORMAL_CLASS = 32;
/** Minimum class memory class to use small photos */
private final static long MIN_SMALL_CLASS = 24;
public static final boolean sUseLowResImages;
static {
if (Build.VERSION.SDK_INT >= 11) {
// On HC and beyond, assume devices are more capable
sUseLowResImages = false;
} else {
if (PhotoViewActivity.sMemoryClass >= MIN_SMALL_CLASS) {
sUseLowResImages = false;
} else {
// If we're not in the small class, use low-res [i.e. RGB_565] photos
sUseLowResImages = true;
}
}
}
public static enum ImageSize {
EXTRA_SMALL,
SMALL,
NORMAL,
}
public static final ImageSize sUseImageSize;
static {
// On HC and beyond, assume devices are more capable
if (Build.VERSION.SDK_INT >= 11) {
sUseImageSize = ImageSize.NORMAL;
} else {
if (PhotoViewActivity.sMemoryClass >= MIN_NORMAL_CLASS) {
// We have plenty of memory; use full sized photos
sUseImageSize = ImageSize.NORMAL;
} else if (PhotoViewActivity.sMemoryClass >= MIN_SMALL_CLASS) {
// We have slight less memory; use smaller sized photos
sUseImageSize = ImageSize.SMALL;
} else {
// We have little memory; use very small sized photos
sUseImageSize = ImageSize.EXTRA_SMALL;
}
}
}
/**
* Interface for when a dialog informing about a camera photo insertion
* should be shown or hidden.
*/
public interface InsertCameraPhotoDialogDisplayer {
public void showInsertCameraPhotoDialog();
public void hideInsertCameraPhotoDialog();
}
/**
* This class cannot be instantiated
*/
private ImageUtils() {
}
/**
* Parses an image from a byte array. May return either a Bitmap or
* a {@link Drawable}.
*
* @param data byte array of compressed image data
* @return The decoded bitmap or {@link Drawable}, or null if the image could not be decoded.
*/
public static Object decodeMedia(byte[] data) {
try {
if (GifDrawable.isGif(data)) {
return new GifDrawable(data);
} else {
return BitmapFactory.decodeByteArray(data, 0, data.length);
}
} catch (OutOfMemoryError oome) {
Log.e(TAG, "ImageUtils#decodeMedia(byte[]) threw an OOME", oome);
return null;
}
}
/**
* Wrapper around {@link BitmapFactory#decodeByteArray(byte[], int, int)}
* that returns {@code null} on {@link OutOfMemoryError}.
*
* @param data byte array of compressed image data
* @param offset offset into imageData for where the decoder should begin
* parsing.
* @param length the number of bytes, beginning at offset, to parse
* @return The decoded bitmap, or null if the image could not be decode.
*/
public static Bitmap decodeByteArray(byte[] data, int offset, int length) {
try {
return BitmapFactory.decodeByteArray(data, offset, length);
} catch (OutOfMemoryError oome) {
Log.e(TAG, "ImageUtils#decodeByteArray(byte[], int, int) threw an OOME", oome);
return null;
}
}
/**
* Wrapper around {@link BitmapFactory#decodeByteArray(byte[], int, int,
* BitmapFactory.Options)} that returns {@code null} on {@link
* OutOfMemoryError}.
*
* @param data byte array of compressed image data
* @param offset offset into imageData for where the decoder should begin
* parsing.
* @param length the number of bytes, beginning at offset, to parse
* @param opts null-ok; Options that control downsampling and whether the
* image should be completely decoded, or just is size returned.
* @return The decoded bitmap, or null if the image could not be decode.
*/
public static Bitmap decodeByteArray(byte[] data, int offset, int length,
BitmapFactory.Options opts) {
try {
return BitmapFactory.decodeByteArray(data, offset, length, opts);
} catch (OutOfMemoryError oome) {
Log.e(TAG, "ImageUtils#decodeByteArray(byte[], int, int, Options) threw an OOME", oome);
return null;
}
}
/**
* Wrapper around {@link BitmapFactory#decodeResource(Resources, int)}
* that returns {@code null} on {@link OutOfMemoryError}.
*
* @param res The resources object containing the image data
* @param id The resource id of the image data
* @return The decoded bitmap, or null if the image could not be decode.
*/
public static Bitmap decodeResource(Resources res, int id) {
try {
return BitmapFactory.decodeResource(res, id);
} catch (OutOfMemoryError oome) {
Log.e(TAG, "ImageUtils#decodeResource(Resources, int) threw an OOME", oome);
return null;
}
}
/**
* Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect,
* BitmapFactory.Options)} that returns {@code null} on {@link
* OutOfMemoryError}.
*
* @param is The input stream that holds the raw data to be decoded into a
* bitmap.
* @param outPadding If not null, return the padding rect for the bitmap if
* it exists, otherwise set padding to [-1,-1,-1,-1]. If
* no bitmap is returned (null) then padding is
* unchanged.
* @param opts null-ok; Options that control downsampling and whether the
* image should be completely decoded, or just is size returned.
* @return The decoded bitmap, or null if the image data could not be
* decoded, or, if opts is non-null, if opts requested only the
* size be returned (in opts.outWidth and opts.outHeight)
*/
public static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) {
try {
return BitmapFactory.decodeStream(is, outPadding, opts);
} catch (OutOfMemoryError oome) {
Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome);
return null;
}
}
/**
* Create a bitmap from a local URI
*
* @param resolver The ContentResolver
* @param uri The local URI
* @param maxSize The maximum size (either width or height)
*
* @return The new bitmap
*/
public static Bitmap createLocalBitmap(ContentResolver resolver, Uri uri, int maxSize) {
InputStream inputStream = null;
try {
final BitmapFactory.Options opts = new BitmapFactory.Options();
final Point bounds = getImageBounds(resolver, uri);
inputStream = resolver.openInputStream(uri);
opts.inSampleSize = Math.max(bounds.x / maxSize, bounds.y / maxSize);
final Bitmap decodedBitmap = decodeStream(inputStream, null, opts);
// Correct thumbnail orientation as necessary
// TODO: Fix rotation if it's actually a problem
//return rotateBitmap(resolver, uri, decodedBitmap);
return decodedBitmap;
} catch (FileNotFoundException exception) {
// Do nothing - the photo will appear to be missing
} catch (IOException exception) {
// Do nothing - the photo will appear to be missing
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException ignore) {
}
}
return null;
}
/**
* Creates a bitmap from the given bytes at the specified dimension and with the
* specified crop. Sub-sample as necessary.
*
* TODO(toddke) Currently, we only perform the wide crop in this method. The square
* crop is already handled via the FIFE / Image Proxy URLs. When the photo cache and
* image cache are merged, we'll need to support square crop as well.
*/
public static Bitmap createBitmap(byte[] imageBytes, int width, int height, int cropType) {
if (imageBytes == null || imageBytes.length == 0) {
return null;
}
final ByteArrayInputStream inputStream = new ByteArrayInputStream(imageBytes);
final boolean useLowResImages = ImageUtils.sUseLowResImages;
try {
final BitmapFactory.Options opts = new BitmapFactory.Options();
final Point bounds = getImageBounds(imageBytes);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "PhotoCache#createBitmap; w: " +
bounds.x + ", h: " + bounds.y + ", max: " + width);
}
opts.inSampleSize = Math.max(bounds.x / width, bounds.y / height);
if (useLowResImages) {
opts.inPreferredConfig = Config.RGB_565;
}
final Bitmap decodedBitmap = decodeStream(inputStream, null, opts);
if (decodedBitmap == null) {
return null;
}
final Bitmap croppedBitmap;
if (cropType == CROP_WIDE) { // changed from EsService.CROP_WIDE
croppedBitmap = cropWideBitmap(decodedBitmap, width, height);
decodedBitmap.recycle();
if (croppedBitmap == null) {
return null;
}
} else {
croppedBitmap = decodedBitmap;
}
if (useLowResImages) {
final Bitmap lowResBitmap = ImageUtils.getLowResBitmap(croppedBitmap);
if (lowResBitmap != croppedBitmap) {
croppedBitmap.recycle();
}
return lowResBitmap;
} else {
return croppedBitmap;
}
} catch (OutOfMemoryError e) {
// Do nothing - the photo will appear to be missing
} finally {
try {
inputStream.close();
} catch (IOException ignore) {
}
}
return null;
}
/**
* Crops the given bitmap according to the {@link EsService#CROP_WIDE} style. The
* center of the bitmap is used to create a new bitmap of exactly width x height
* pixels, maintaining the original aspect ratio. The original bitmap will be
* cropped and/or enlarged as necessary.
*/
private static Bitmap cropWideBitmap(Bitmap inputBitmap, int width, int height) {
final Rect srcRect;
final int srcWidth = inputBitmap.getWidth();
final int srcHeight = inputBitmap.getHeight();
final int dstWidth = width;
final int dstHeight = height;
if (srcWidth == dstWidth && srcHeight == dstHeight) {
// Photo is exactly the same size as the on-screen image
srcRect = new Rect(0, 0, srcWidth, srcHeight);
} else {
// create a source rectangle of the same aspect ratio as the requested size.
int cropWidth = srcWidth;
int cropHeight = srcHeight;
if (srcWidth * dstHeight > srcHeight * dstWidth) {
// the input bitmap is a wider aspect ratio. Crop the sides.
cropWidth = srcHeight * dstWidth / dstHeight;
} else {
// The input bitmap is a taller aspect ratio. Crop the top and bottom.
cropHeight = srcWidth * dstHeight / dstWidth;
}
final int left = (srcWidth - cropWidth) / 2;
final int top = (srcHeight - cropHeight) / 2;
srcRect = new Rect(left, top, left + cropWidth, top + cropHeight);
}
// Create the new bitmap
final Bitmap.Config bitmapConfig =
ImageUtils.sUseLowResImages ? Bitmap.Config.RGB_565 : Bitmap.Config.ARGB_8888;
final Bitmap bitmap = Bitmap.createBitmap(width, height, bitmapConfig);
if (bitmap == null) {
return null;
}
final Canvas canvas = new Canvas(bitmap);
final Rect dstRect = new Rect(0, 0, width, height);
synchronized (sCropPaint) {
canvas.drawBitmap(inputBitmap, srcRect, dstRect, sCropPaint);
}
return bitmap;
}
/**
* Gets the image bounds
*/
private static Point getImageBounds(byte[] imageBytes) {
final BitmapFactory.Options opts = new BitmapFactory.Options();
final ByteArrayInputStream inputStream = new ByteArrayInputStream(imageBytes);
try {
opts.inJustDecodeBounds = true;
decodeStream(inputStream, null, opts);
return new Point(opts.outWidth, opts.outHeight);
} finally {
try {
inputStream.close();
} catch (IOException ignore) {
}
}
}
/**
* Create a center-cropped bitmap from a uri.
*
* @param resolver The ContentResolver
* @param uri The uri
* @param width The width of the output bitmap
* @param height The height of the output bitmap
*
* @return the new bitmap
*/
public static Bitmap createCroppedBitmap(ContentResolver resolver, Uri uri,
int width, int height) {
try {
InputStream inputStream = resolver.openInputStream(uri);
final BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
decodeStream(inputStream, null, opts);
inputStream.close();
// use Math.min() here to ensure that each of the image dimensions are
// >= the target size
inputStream = resolver.openInputStream(uri);
opts.inJustDecodeBounds = false;
opts.inSampleSize = Math.min(opts.outWidth / width, opts.outHeight / height);
Bitmap srcBitmap = decodeStream(inputStream, null, opts);
inputStream.close();
if (srcBitmap == null) {
return null;
}
final int srcWidth = srcBitmap.getWidth();
final int srcHeight = srcBitmap.getHeight();
if (srcWidth == width && srcHeight == height) {
return srcBitmap;
}
Bitmap destBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
if (destBitmap == null) {
srcBitmap.recycle();
return null;
}
final Canvas canvas = new Canvas(destBitmap);
int croppedWidth = srcWidth;
int croppedHeight = srcHeight;
// We want to take the center part of the image with the same aspect
// ratio as the target, and crop the rest. The same behavior as CENTER_CROP.
if (srcWidth * height > srcHeight * width) {
// The input bitmap is a wider aspect ratio. Crop the sides.
croppedWidth = srcHeight * width / height;
} else {
// The input bitmap is a taller aspect ratio. Crop the top and bottom.
croppedHeight = srcWidth * height / width;
}
final int left = (srcWidth - croppedWidth) / 2;
final int top = (srcHeight - croppedHeight) / 2;
final Rect src = new Rect(left, top, left + croppedWidth, top + croppedHeight);
synchronized (sResizePaint) {
canvas.drawBitmap(srcBitmap, src, new Rect(0, 0, width, height), sResizePaint);
}
srcBitmap.recycle();
// correct orientation, as necessary
return rotateBitmap(resolver, uri, destBitmap);
} catch (FileNotFoundException exception) {
return null;
} catch (IOException exception) {
return null;
}
}
/**
* Returns the maximum dimension in pixels for a given MediaStore.Images.Thumbnails kind.
*
* @param context The context
* @param kind MICRO_KIND or MINI_KIND
*
* @return maxDimension in pixels
*/
public static int getMaxThumbnailDimension(Context context, int kind) {
// determine max dimension based on kind
final int maxDimension;
switch (kind) {
case Thumbnails.MICRO_KIND:
maxDimension = getThumbnailSize(context, Thumbnails.MICRO_KIND);
break;
case Thumbnails.MINI_KIND:
maxDimension = getThumbnailSize(context, Thumbnails.MINI_KIND);
break;
default:
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "illegal kind=" + kind + " specified; using MINI_KIND");
}
maxDimension = getThumbnailSize(context, Thumbnails.MINI_KIND);
break;
}
return maxDimension;
}
/**
* Convert thumbnail dimensions to pixels
*
* @param context The context
* @param kind The kind
*
* @return The size of the thumbnail in pixels
*/
public static int getThumbnailSize(Context context, int kind) {
switch (kind) {
case Thumbnails.MICRO_KIND: {
if (MICRO_KIND_MAX_DIMENSION == 0) {
MICRO_KIND_MAX_DIMENSION = context.getResources().getDimensionPixelSize(
R.dimen.micro_kind_max_dimension);
}
return MICRO_KIND_MAX_DIMENSION;
}
case Thumbnails.MINI_KIND:
default: {
if (MINI_KIND_MAX_DIMENSION == 0) {
MINI_KIND_MAX_DIMENSION = context.getResources().getDimensionPixelSize(
R.dimen.mini_kind_max_dimension);
}
return MINI_KIND_MAX_DIMENSION;
}
}
}
/**
* Scale a bitmap to a square bitmap
*
* @param imageBytes The input bitmap
* @param size The width and height
*
* @return The new bitmap
*/
public static byte[] resizeToSquareBitmap(byte[] imageBytes, int size) {
return resizeToSquareBitmap(imageBytes, size, NO_COLOR);
}
/**
* Scale a bitmap to a square bitmap
*
* @param imageBytes The input bitmap
* @param size The width and height
* @param backgroundColor The background color that should be used for translucent avatars.
*
* @return The new bitmap
*/
public static byte[] resizeToSquareBitmap(byte[] imageBytes, int size, int backgroundColor) {
if (imageBytes == null) {
return imageBytes;
}
final BitmapFactory.Options dbo = new BitmapFactory.Options();
dbo.inJustDecodeBounds = true;
decodeByteArray(imageBytes, 0, imageBytes.length, dbo);
int nativeWidth = dbo.outWidth;
int nativeHeight = dbo.outHeight;
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "resizeToSquareBitmap: Input: " + nativeWidth + "x" + nativeHeight
+ ", resize to: " + size);
}
Bitmap bitmap;
int sampleSize = Math.min(nativeWidth / size, nativeHeight / size);
if (sampleSize > 1) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = sampleSize;
bitmap = decodeByteArray(imageBytes, 0, imageBytes.length, options);
} else {
bitmap = decodeByteArray(imageBytes, 0, imageBytes.length);
}
if (bitmap == null) {
return null;
}
Bitmap scaledBitmap = resizeToSquareBitmap(bitmap, size, backgroundColor);
bitmap.recycle();
if (scaledBitmap == null) {
return null;
}
ByteArrayOutputStream stream = new ByteArrayOutputStream();
scaledBitmap.compress(CompressFormat.JPEG, 80, stream);
scaledBitmap.recycle();
scaledBitmap = null;
return stream.toByteArray();
}
/**
* Scale a bitmap to a square bitmap
*
* @param inputBitmap The input bitmap
* @param size The width and height
*
* @return The new bitmap
*/
public static Bitmap resizeToSquareBitmap(Bitmap inputBitmap, int size) {
return resizeToSquareBitmap(inputBitmap, size, NO_COLOR);
}
/**
* Scale a bitmap to a square bitmap
*
* @param inputBitmap The input bitmap
* @param size The width and height
* @param backgroundColor The solid color used to paint the image background. If
* {@link #NO_COLOR}, no background will be painted.
*
* @return The new bitmap
*/
public static Bitmap resizeToSquareBitmap(Bitmap inputBitmap, int size,
int backgroundColor) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "resizeToSquareBitmap: Input: " + inputBitmap.getWidth()
+ "x" + inputBitmap.getHeight() + ", output:" + size + "x" + size);
}
final Bitmap bitmap;
try {
// Create the new bitmap
bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
} catch (OutOfMemoryError e) {
Log.w(TAG, "resizeToSquareBitmap OutOfMemoryError for image size: " + size, e);
return null;
}
if (bitmap == null) {
return null;
}
final Canvas canvas = new Canvas(bitmap);
if (backgroundColor != NO_COLOR) {
canvas.drawColor(backgroundColor);
}
if (inputBitmap.getWidth() != size || inputBitmap.getHeight() != size) {
final Rect src = new Rect(0, 0, inputBitmap.getWidth(), inputBitmap.getHeight());
final Rect dest = new Rect(0, 0, size, size);
synchronized(sResizePaint) {
canvas.drawBitmap(inputBitmap, src, dest, sResizePaint);
}
} else {
canvas.drawBitmap(inputBitmap, 0, 0, null);
}
return bitmap;
}
/**
* Resize and crop a bitmap.
*
* @param inputBitmap The input bitmap
* @param height The height
* @param width The width
*
* @return The new bitmap
*/
public static Bitmap resizeAndCropBitmap(Bitmap inputBitmap, int width, int height) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "resizeAndCropBitmap: Input: " + inputBitmap.getWidth()
+ "x" + inputBitmap.getHeight() + ", output:"
+ width + "x" + height);
}
// Create the new bitmap
final Bitmap bitmap = Bitmap.createBitmap(
width, height, Bitmap.Config.ARGB_8888);
if (bitmap == null) {
return null;
}
final Canvas canvas = new Canvas(bitmap);
if (inputBitmap.getWidth() != width || inputBitmap.getHeight() != height) {
// create a source rectangle of the same aspect ratio as the requested size.
int croppedWidth = inputBitmap.getWidth();
int croppedHeight = inputBitmap.getHeight();
if (inputBitmap.getWidth() * height > inputBitmap.getHeight() * width) {
// the input bitmap is a wider aspect ratio. Crop the sides.
croppedWidth = inputBitmap.getHeight() * width / height;
} else {
// The input bitmap is a taller aspect ratio. Crop the top and bottom.
croppedHeight = inputBitmap.getWidth() * height / width;
}
int left = (inputBitmap.getWidth() - croppedWidth) / 2;
int top = (inputBitmap.getHeight() - croppedHeight) / 2;
final Rect src = new Rect(left, top,
left + croppedWidth, top + croppedHeight);
final Rect dest = new Rect(0, 0, width, height);
synchronized(sResizePaint) {
canvas.drawBitmap(inputBitmap, src, dest, sResizePaint);
}
} else {
canvas.drawBitmap(inputBitmap, 0, 0, null);
}
return bitmap;
}
/**
* Resize a bitmap
*
* @param imageBytes The image bytes
* @param width The width of the resized image
* @param height The width of the resized image
*
* @return The resized bitmap
*/
public static Bitmap resizeBitmap(byte[] imageBytes, int width, int height) {
final BitmapFactory.Options dbo = new BitmapFactory.Options();
dbo.inJustDecodeBounds = true;
decodeByteArray(imageBytes, 0, imageBytes.length, dbo);
final int nativeWidth = dbo.outWidth;
final int nativeHeight = dbo.outHeight;
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "resizeBitmap: Input: " + nativeWidth + "x" + nativeHeight
+ ", resize to: " + width + "x" + height);
}
final Bitmap srcBitmap;
if (nativeWidth > width || nativeHeight > height) {
final float bitmapWidth = (nativeWidth * width) / nativeHeight;
final float bitmapHeight = (nativeHeight * height) / nativeWidth;
if (nativeWidth / bitmapWidth > 1 || nativeHeight / bitmapHeight > 1) {
// Create a scaled bitmap
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = Math.max(nativeWidth / (int)bitmapWidth,
nativeHeight / (int)bitmapHeight);
srcBitmap = decodeByteArray(imageBytes, 0, imageBytes.length, options);
} else {
srcBitmap = decodeByteArray(imageBytes, 0, imageBytes.length);
}
} else {
srcBitmap = decodeByteArray(imageBytes, 0, imageBytes.length);
}
if (srcBitmap == null) {
return null;
}
// Crop the bitmap
final Bitmap croppedBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
if (croppedBitmap == null) {
srcBitmap.recycle();
return null;
}
final int srcWidth = srcBitmap.getWidth();
final int srcHeight = srcBitmap.getHeight();
int croppedWidth = srcWidth;
int croppedHeight = srcHeight;
if (nativeWidth * height > width * nativeHeight) {
// the input bitmap is a wider aspect ratio. Crop the sides.
croppedWidth = srcBitmap.getHeight() * width / height;
} else {
// the input bitmap is a taller aspect ratio. Crop the top and bottom.
croppedHeight = srcBitmap.getWidth() * height / width;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "resizeBitmap: cropped: " + croppedWidth + "x" + croppedHeight);
}
final int srcLeft = (srcWidth - croppedWidth) / 2;
final int srcTop = (srcHeight - croppedHeight) / 2;
final Rect src = new Rect(srcLeft, srcTop, srcLeft + croppedWidth, srcTop + croppedHeight);
final Rect dest = new Rect(0, 0, width, height);
final Canvas croppedCanvas = new Canvas(croppedBitmap);
croppedCanvas.drawColor(0xffe0e0e0);
synchronized (sResizePaint) {
croppedCanvas.drawBitmap(srcBitmap, src, dest, sResizePaint);
}
srcBitmap.recycle();
return croppedBitmap;
}
/**
* Resize the bitmap so that its height does not exceed the supplied value.
*
* @param imageBytes The image bytes
* @param height The maximum height of the scaled image
*
* @return The resized bitmap as bytes
*/
public static byte[] resizeBitmapToHeight(byte[] imageBytes, int height) {
if (imageBytes == null) {
return imageBytes;
}
final BitmapFactory.Options dbo = new BitmapFactory.Options();
dbo.inJustDecodeBounds = true;
decodeByteArray(imageBytes, 0, imageBytes.length, dbo);
int nativeWidth = dbo.outWidth;
int nativeHeight = dbo.outHeight;
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "scaleBitmap: Input: " + nativeWidth + "x" + nativeHeight
+ ", resize to: " + height);
}
if (nativeHeight <= height) {
return imageBytes;
}
int width = (int) ((float) nativeWidth / nativeHeight * height);
Bitmap bitmap;
if (nativeWidth / width > 1 || nativeHeight / height > 1) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = Math.max(nativeWidth / width, nativeHeight / height);
bitmap = decodeByteArray(imageBytes, 0, imageBytes.length, options);
if (bitmap == null) {
return null;
}
nativeWidth = bitmap.getWidth();
nativeHeight = bitmap.getHeight();
} else {
bitmap = decodeByteArray(imageBytes, 0, imageBytes.length);
if (bitmap == null) {
return null;
}
}
Bitmap scaledBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
if (scaledBitmap == null) {
bitmap.recycle();
return null;
}
final Canvas canvas = new Canvas(scaledBitmap);
synchronized (sResizePaint) {
canvas.drawBitmap(bitmap, new Rect(0, 0, nativeWidth, nativeHeight),
new Rect(0, 0, width, height), sResizePaint);
}
bitmap.recycle();
bitmap = null;
ByteArrayOutputStream stream = new ByteArrayOutputStream();
scaledBitmap.compress(CompressFormat.PNG, 100, stream);
scaledBitmap.recycle();
scaledBitmap = null;
return stream.toByteArray();
}
/**
* @param context The context
* @return A {@link ProgressDialog} informing the user a photo is being
* inserted
*/
public static Dialog createInsertCameraPhotoDialog(Context context) {
final ProgressDialog dialog = new ProgressDialog(context);
dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
dialog.setCancelable(false);
dialog.setMessage(context.getString(R.string.dialog_inserting_camera_photo));
return dialog;
}
/**
* Inserts a newly taken photo into the media store. We cannot directly use
* {@code Images.Media#insertImage(ContentResolver, String, String, String)}
* as this method will not properly set the photo's timestamp. Additionally,
* any EXIF information in the original image is lost and there's a much higher
* chance for an OOME as insertImage() actually decodes the JPEG just to
* immediately re-encode it back to a JPEG.
* <p>
* NOTE: This code was shamelessly copied and merged from the Camera app
* [see method addImage() in Storage.java] and Images.Media#insertImage().
*
* NOTE: This method should not be called from the UI thread. It performs
* file IO and generates a thumbnail.
*
* @param context The context
* @param filename The name of the photo
* @return The media URL of the photo
* @throws FileNotFoundException If the file is not found
*/
public static String insertCameraPhoto(Context context, String filename)
throws FileNotFoundException {
final File f = new File(Environment.getExternalStorageDirectory(), filename);
final long dateTaken = System.currentTimeMillis();
final String photoName = createPhotoName(context, dateTaken);
final ContentResolver resolver = context.getContentResolver();
// Insert into MediaStore
final ContentValues values = new ContentValues(5);
final int orientation = ImageUtils.getExifRotation(resolver, f.getAbsolutePath());
values.put(ImageColumns.TITLE, photoName);
values.put(ImageColumns.DISPLAY_NAME, photoName + ".jpg");
values.put(ImageColumns.DATE_TAKEN, dateTaken);
values.put(ImageColumns.MIME_TYPE, "image/jpeg");
values.put(ImageColumns.ORIENTATION, orientation);
// TODO(kkiyohara): be smarter about figuring out what storage is available, or
// maybe preventing the photo from being taken if the SD card (external storage)
// is missing.
Uri mediaUri;
try {
mediaUri = resolver.insert(Images.Media.EXTERNAL_CONTENT_URI, values);
} catch (Exception e1) {
// here when saving to external failed, try internal
try {
mediaUri = resolver.insert(Images.Media.INTERNAL_CONTENT_URI, values);
} catch (Exception e2) {
try {
// last chance, try save to HTC-specific PhoneStorage
mediaUri = resolver.insert(MediaStoreUtils.PHONE_STORAGE_IMAGES_URI, values);
} catch (Exception e3) {
Log.e(TAG, "Failed to save image", e3);
return null;
}
}
}
try {
// On some platforms this method may throw a NullPointerException
final OutputStream imageOut = resolver.openOutputStream(mediaUri);
final FileInputStream imageIn = new FileInputStream(f);
try {
final int downloadBufferSize = 10240;
final byte[] array = new byte[downloadBufferSize];
int bytesRead;
do {
bytesRead = imageIn.read(array);
if (bytesRead == -1) {
break;
}
imageOut.write(array, 0, bytesRead);
} while (true);
} finally {
imageOut.close();
}
// Wait until MINI_KIND thumbnail is generated.
//
// If Images.Media.EXTERNAL_CONTENT_URI is not writable, then
// it is not possible to generate the thumbnail using public APIs.
if (MediaStoreUtils.isExternalMediaStoreUri(mediaUri)) {
Bitmap bmp = MediaStoreUtils.getThumbnail(
context, mediaUri, Images.Thumbnails.MINI_KIND);
bmp.recycle();
bmp = null;
}
} catch (FileNotFoundException fe) {
Log.e(TAG, "File not found", fe);
throw fe;
} catch (Exception e) {
Log.e(TAG, "Failed to insert image", e);
if (mediaUri != null) {
resolver.delete(mediaUri, null, null);
mediaUri = null;
}
} finally {
f.delete();
}
return (mediaUri == null ? null : mediaUri.toString());
}
/**
* Returns a a name that is consistent with the Android camera application.
*/
private static String createPhotoName(Context context, long dateTaken) {
final Date date = new Date(dateTaken);
final SimpleDateFormat dateFormat =
new SimpleDateFormat(context.getString(R.string.image_file_name_format));
return dateFormat.format(date);
}
/**
* Gets a URL that can be used to download an image at the given size. The size specifies
* the maximum width or height of the image. If the given URL is either a FIFE URL or an
* Image Proxy URL, it will be modified to contain the proper sizing parameters. Otherwise,
* the URL will be converted to an Image Proxy URL.
*
* @return A URL that can be used to retrieve an image of the given size.
*/
public static String getResizedUrl(int size, String url) {
if (FIFEUtil.isFifeHostedUrl(url)) {
return FIFEUtil.setImageUrlSize(size, url, false);
} else {
return ImageProxyUtil.setImageUrlSize(size, url);
}
}
/**
* Gets a URL that can be used to download an image at the given size. The size specifies
* the maximum width or height of the image. If the given URL is either a FIFE URL or an
* Image Proxy URL, it will be modified to contain the proper sizing parameters. Otherwise,
* the URL will be converted to an Image Proxy URL.
*
* @return A URL that can be used to retrieve an image of the given size.
*/
public static String getResizedUrl(int width, int height, String url) {
if (FIFEUtil.isFifeHostedUrl(url)) {
return FIFEUtil.setImageUrlSize(width, height, url, false, false);
} else {
return ImageProxyUtil.setImageUrlSize(width, height, url);
}
}
/**
* See {@link #getCroppedAndResizedUrl(int, String)} for more information. This method
* differs from getCroppedAndResizedUrl because it attempts to get a center cropped
* version of the requested image. This is only possible for FIFE hosted URLs; Image
* Proxy URLs will work as they do in getCroppedAndResizedUrl.
*
* @return A URL that can be used to retrieve an image of the given size.
*/
public static String getCenterCroppedAndResizedUrl(int width, int height, String url) {
if (url == null) {
return null;
}
if (FIFEUtil.isFifeHostedUrl(url)) {
final StringBuilder options = new StringBuilder();
options.append("w").append(width);
options.append("-h").append(height);
options.append("-d");
options.append("-n");
return FIFEUtil.setImageUrlOptions(options.toString(), url).toString();
} else {
return ImageProxyUtil.setImageUrlSize(width, height, url);
}
}
/**
* See {@link #getResizedUrl(int, String)} for more information. This method differs
* from getResizedUrl because it attempts to get a cropped version of the requested
* image, meaning that for a given size, the returned image will be of dimension size
* in both x and y. This is only possible for FIFE hosted URLs; Image Proxy URLs will
* work as they do in getResizedUrl.
*
* @param size The size
* @param url The URL
* @return A URL that can be used to retrieve an image of the given size,
* cropped if possible.
*/
public static String getCroppedAndResizedUrl(int size, String url) {
if (FIFEUtil.isFifeHostedUrl(url)) {
return FIFEUtil.setImageUrlSize(size, url, true);
} else {
// The image proxy has no facility to crop images
return ImageProxyUtil.setImageUrlSize(size, url);
}
}
/**
* For some images, namely PNG images, the decode ignores the preferred config option and
* always decodes them as 32bpp. On devices that will see the most benefit, we re-encode
* the image as 16bpp. Otherwise, prefer to have greater fidelity in a PNG. The specified
* bitmap will be recycled automatically as necessary.
*/
public static Bitmap getLowResBitmap(Bitmap bitmap) {
if (bitmap == null) {
return null;
}
if (bitmap.getConfig() == Config.ARGB_8888) {
final int width = bitmap.getWidth();
final int height = bitmap.getHeight();
final Bitmap lowResBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
final Canvas canvas = new Canvas(lowResBitmap);
final Rect src = new Rect(0, 0, width, height);
final Rect dest = new Rect(0, 0, width, height);
synchronized(sResizePaint) {
canvas.drawBitmap(bitmap, src, dest, sResizePaint);
}
bitmap.recycle();
return lowResBitmap;
}
return bitmap;
}
/**
* Gets the image bounds
*
* @param resolver The ContentResolver
* @param uri The uri
*
* @return The image bounds
*/
private static Point getImageBounds(ContentResolver resolver, Uri uri)
throws IOException {
final BitmapFactory.Options opts = new BitmapFactory.Options();
InputStream inputStream = null;
try {
opts.inJustDecodeBounds = true;
inputStream = resolver.openInputStream(uri);
decodeStream(inputStream, null, opts);
return new Point(opts.outWidth, opts.outHeight);
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException ignore) {
}
}
}
/**
* Get the file path of a media item
*
* @return the filepath for a given MediaStore uri, or null if there was a
* problem
*/
private static String getFilePath(ContentResolver resolver, Uri uri) {
// Ask MediaStore for the actual file path
final Cursor cursor = resolver.query(uri,
new String[] {MediaColumns._ID, MediaColumns.DATA}, null, null, null);
if (cursor == null) {
Log.w(TAG, "getFilePath: query returned null cursor for uri=" + uri);
return null;
}
String path = null;
try {
if (!cursor.moveToFirst()) {
Log.w(TAG, "getFilePath: query returned empty cursor for uri=" + uri);
return null;
}
// Get the file path
path = cursor.getString(cursor.getColumnIndexOrThrow(MediaColumns.DATA));
if (TextUtils.isEmpty(path)) {
Log.w(TAG, "getFilePath: MediaColumns.DATA was empty for uri=" + uri);
return null;
}
} finally {
cursor.close();
}
return path;
}
/**
* Encode the given image as a Base64 string (recycle the bitmap)
*
* @param imageBytes The image bytes
*
* @return A base64 encoded string
*/
public static String encodeImageBytes(byte[] imageBytes) {
String base64 = Base64.encodeToString(imageBytes, Base64.NO_WRAP);
return "data:image/jpeg;base64," + base64;
}
/**
* Decode an image from a Base64 string
*
* @param string A base64 encoded string
*
* @return The image bytes
*/
public static byte[] decodeImageBytes(String string) {
int start = string.indexOf("base64,");
if (start == -1) {
return null;
}
return Base64.decode(string.substring(start+7), Base64.DEFAULT);
}
/**
* Compress the bitmap to JPEG and return the compressed image bytes. The given bitmap will
* be recycled.
*
* @param bitmap The bitmap
* @param quality the quality level for JPEG coding (90 is default).
*
* @return The compressed image bytes
*/
public static byte[] compressBitmap(Bitmap bitmap, int quality) {
final ByteArrayOutputStream stream = new ByteArrayOutputStream();
try {
bitmap.compress(CompressFormat.JPEG, quality, stream); // Copy #1
stream.flush();
} catch (IOException ignore) {
} finally {
try {
stream.close();
} catch (IOException ignore) {
}
}
bitmap.recycle();
bitmap = null;
final byte[] imageBytes = stream.toByteArray(); // Copy #2
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "compressBitmap: Image size: " + imageBytes.length);
}
return imageBytes;
}
/**
* Compress the bitmap to JPEG and return the compressed image bytes. The given bitmap will
* be recycled. A default quality level of 90 is used.
*
* @param bitmap The bitmap
*
* @return The compressed image bytes
*/
public static byte[] compressBitmap(Bitmap bitmap) {
return compressBitmap(bitmap, DEFAULT_JPEG_QUALITY);
}
/**
* Retrieve the EXIF rotation of an image
*
* @param cr the content resolver, only used when the path given is an
* actual content uri.
* @param path an absolute file path to the photo for which we want to get
* the rotation angle. Can also be a content uri, in which case
* the content resolver is used.
*
* @return the number of degrees an image needs to be rotated to face the
* "correct" way. Does this by reading the actual file's EXIF
* metadata.
*/
private static int getExifRotation(ContentResolver cr, String path) {
// create the Exif interface
ExifInterface exif = null;
try {
exif = new ExifInterface(path);
} catch (IOException e) {
Log.w(TAG, "failed to create ExifInterface for " + path);
}
if (exif == null) {
return 0;
}
// get and translate the orientation
int orientation = exif.getAttributeInt(
ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
int degrees = 0;
switch (orientation) {
case ExifInterface.ORIENTATION_NORMAL:
degrees = 0;
break;
case ExifInterface.ORIENTATION_ROTATE_90:
degrees = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
degrees = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
degrees = 270;
break;
}
return degrees;
}
/**
* Rotate a bitmap based on the MediaStore uri's EXIF information.
*
* @param cr standard content resolver
* @param uri MediaStore uri
* @param bmp bitmap to rotated
* @return bitmap with proper orientation
*/
public static Bitmap rotateBitmap(ContentResolver cr, Uri uri, Bitmap bmp) {
if (bmp != null) {
final String path = getFilePath(cr, uri);
final int degrees = getExifRotation(cr, path);
if (degrees != 0) {
bmp = rotateBitmap(bmp, degrees);
}
}
return bmp;
}
/**
* Bitmap rotation method
*
* @param bitmap The input bitmap
* @param degrees The rotation angle
*/
private static Bitmap rotateBitmap(Bitmap bitmap, int degrees) {
if (degrees != 0 && bitmap != null) {
final Matrix m = new Matrix();
final int w = bitmap.getWidth();
final int h = bitmap.getHeight();
m.setRotate(degrees, (float) w / 2, (float) h / 2);
try {
final Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, w, h, m, true);
if (bitmap != rotatedBitmap) {
bitmap.recycle();
bitmap = rotatedBitmap;
}
} catch (OutOfMemoryError ex) {
// We have no memory to rotate. Return the original bitmap.
}
}
return bitmap;
}
}