/**
* BitmapTools.java
* Copyright (C)2010 Nicholas Killewald
*
* This file is distributed under the terms of the BSD license.
* The source package should have a LICENSE file at the toplevel.
*/
package net.exclaimindustries.tools;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
/**
* BitmapTools are, as you probably guessed, tools for Bitmap manipulation.
* Static tools, too.
*
* @author Nicholas Killewald
*/
public class BitmapTools {
private static final String DEBUG_TAG = "BitmapTools";
/**
* Creates a new Bitmap that's a scaled version of the given Bitmap, but
* with the aspect ratio preserved. Note that this will only scale down; if
* the image is already smaller than the given dimensions, this will return
* the same bitmap that was given to it.
*
* @param bitmap Bitmap to scale
* @param maxWidth max width of new Bitmap, in pixels
* @param maxHeight max height of new Bitmap, in pixels
* @param reversible whether or not the ratio should be treated as
* reversible; that is, if the maxWidth and maxHeight are
* given as 800x600, but the image is 600x800, it will
* leave the image as 600x800 instead of reduce it to
* 450x600
* @return a new, scaled Bitmap, or the old bitmap if no scaling took place, or null if it failed entirely
*/
public static Bitmap createRatioPreservedDownscaledBitmap(Bitmap bitmap, int maxWidth, int maxHeight, boolean reversible) {
if(bitmap == null) return null;
// Make sure the width and height are properly reversed, if needed.
if(reversible && shouldBeReversed(maxWidth, maxHeight, bitmap.getWidth(), bitmap.getHeight())) {
int t = maxWidth;
//noinspection SuspiciousNameCombination
maxWidth = maxHeight;
maxHeight = t;
}
if(bitmap.getHeight() > maxHeight || bitmap.getWidth() > maxWidth) {
// So, we determine how we're going to scale this, mostly
// because there's no method in Bitmap to maintain aspect
// ratio for us.
double scaledByWidthRatio = ((double)maxWidth) / (double)bitmap.getWidth();
double scaledByHeightRatio = ((double)maxHeight) / (double)bitmap.getHeight();
int newWidth;
int newHeight;
if (bitmap.getHeight() * scaledByWidthRatio <= maxHeight) {
// Scale it by making the width the max, as scaling the
// height by the same amount makes it less than or equal
// to the max height.
newWidth = maxWidth;
newHeight = (int)Math.round(bitmap.getHeight() * scaledByWidthRatio);
} else {
// Otherwise, go by making the height its own max.
newWidth = (int)Math.round(bitmap.getWidth() * scaledByHeightRatio);
newHeight = maxHeight;
}
// Now, do the scaling! The caller must take care of GCing the
// original Bitmap.
Log.d(DEBUG_TAG, "Scaling file down to " + newWidth + "x" + newHeight + "...");
return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true);
} else {
// If it's too small already, just return what came in.
Log.d(DEBUG_TAG, "File is already small enough (" + bitmap.getWidth() + "x" + bitmap.getHeight() + ")");
if(bitmap.isMutable())
return bitmap;
else
return bitmap.copy(bitmap.getConfig(), true);
}
}
/**
* Creates a new Bitmap that's a downscaled, ratio-preserved version of
* a file on disk. I'll admit there's probably a shorter name I could have
* used, but none came to mind. The major difference between this and the
* Bitmap-oriented one is that it will attempt a rough downsampling before
* it loads the original into memory, which should save tons of RAM and
* avoid unsightly OutOfMemoryErrors.
*
* @param filename location of bitmap to open
* @param maxWidth max width of new Bitmap, in pixels
* @param maxHeight max height of new Bitmap, in pixels
* @param reversible whether or not the ratio should be treated as
* reversible; that is, if the maxWidth and maxHeight are
* given as 800x600, but the image is 600x800, it will
* leave the image as 600x800 instead of reduce it to
* 450x600
* @return a new, appropriately scaled Bitmap, or null if it failed entirely
*/
public static Bitmap createRatioPreservedDownscaledBitmapFromFile(String filename, int maxWidth, int maxHeight, boolean reversible) {
// First up, open the Bitmap ONLY for its size, if we can.
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
// This will always return null thanks to inJustDecodeBounds.
BitmapFactory.decodeFile(filename, opts);
// If the height or width are -1 in opts, we failed.
if(opts.outHeight < 0 || opts.outWidth < 0) {
Log.e(DEBUG_TAG, "Error opening file " + filename);
return null;
}
// Make sure the width and height are properly reversed, if needed.
if(reversible && shouldBeReversed(maxWidth, maxHeight, opts.outWidth, opts.outHeight)) {
int t = maxWidth;
//noinspection SuspiciousNameCombination
maxWidth = maxHeight;
maxHeight = t;
}
// Now, determine the best power-of-two to downsample by. We
// intentionally want it one level LOWER than the target; subsampling
// doesn't do any sort of filtering or interpolation at all, meaning if
// we wind up where it's a clean power-of-two to reduce it, the result
// will be grainy and blocky. This way, we wind up scaling it later
// WITH filtering but with far less memory being used, which is a fair
// tradeoff.
int tempWidth = opts.outWidth;
int tempHeight = opts.outHeight;
int sampleFactor = 1;
while(true) {
if(tempWidth / 2 < maxWidth || tempHeight / 2 < maxHeight)
break;
tempWidth /= 2;
tempHeight /= 2;
sampleFactor *= 2;
}
Log.d(DEBUG_TAG, "Downsampling file to " + tempWidth + "x" + tempHeight + "...");
// Good! Now, let's pop it open and scale it the rest of the way.
opts.inJustDecodeBounds = false;
opts.inSampleSize = sampleFactor;
// The reversible flag is always false here, as we've already applied
// it beforehand.
return createRatioPreservedDownscaledBitmap(BitmapFactory.decodeFile(filename, opts), maxWidth, maxHeight, false);
}
/**
* Creates a new Bitmap that's a downscaled, ratio-preserved version of the
* content at a URI. This will need to be something that ContentResolver
* can figure out, AND in a way that it can decode the image bounds properly
* before a full load. If it can't do that, it'll return a null.
*
* @param context Context from which a ContentResolver can be retrieved
* @param uri URI of content to load
* @param maxWidth max width of new Bitmap, in pixels
* @param maxHeight max height of new Bitmap, in pixels
* @param reversible whether or not the ratio should be treated as
* reversible; that is, if the maxWidth and maxHeight are
* given as 800x600, but the image is 600x800, it will
* leave the image as 600x800 instead of reduce it to
* 450x600
* @return a new, appropriately scaled Bitmap, or null if it failed entirely
*/
public static Bitmap createRatioPreservedDownscaledBitmapFromUri(Context context, Uri uri, int maxWidth, int maxHeight, boolean reversible) {
// Since any exception is grounds for an error, let's just try-catch
// everything and return null if anything goes wrong.
try {
// Alright, ContentResolver. You'd better do your job.
InputStream input = context.getContentResolver().openInputStream(uri);
if(input == null) return null;
// Otherwise, it's the same as before. Grab the bounds only.
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeStream(input, null, opts);
input.close();
// I'm reasonably certain that there's no oddities possible with the
// Uri in this case. If the dimensions wind up -1, I'm just saying
// it can't open it, and if there's some obscure case where a
// legitimate Bitmap can be loaded whose dimensions are less than
// zero, I don't care.
if(opts.outHeight < 0 || opts.outWidth < 0) {
Log.e(DEBUG_TAG, "Error opening URI " + uri.toString());
return null;
}
// Do the same calculations as in the filename version...
if(reversible && shouldBeReversed(maxWidth, maxHeight, opts.outWidth, opts.outHeight)) {
int t = maxWidth;
//noinspection SuspiciousNameCombination
maxWidth = maxHeight;
maxHeight = t;
}
int tempWidth = opts.outWidth;
int tempHeight = opts.outHeight;
int sampleFactor = 1;
while(true) {
if(tempWidth / 2 < maxWidth || tempHeight / 2 < maxHeight)
break;
tempWidth /= 2;
tempHeight /= 2;
sampleFactor *= 2;
}
Log.d(DEBUG_TAG, "Downsampling image to " + tempWidth + "x" + tempHeight + "...");
opts.inJustDecodeBounds = false;
opts.inSampleSize = sampleFactor;
// Re-open the stream, as we closed it already.
input = context.getContentResolver().openInputStream(uri);
if(input == null) return null;
// Read it into a Bitmap with the new options in hand.
Bitmap bitmap = BitmapFactory.decodeStream(input, null, opts);
// Close 'er up.
input.close();
// Let 'er rip.
return createRatioPreservedDownscaledBitmap(bitmap, maxWidth, maxHeight, false);
} catch (IOException ioe) {
// Aaaaaand something went wrong, so we return null.
return null;
}
}
private static boolean shouldBeReversed(int inWidth, int inHeight, int outWidth, int outHeight) {
// If this ratio is 1.0, we never need to reverse it.
if(inWidth == inHeight) return false;
// If the original is more wide than tall but the second isn't, we can
// reverse it. Same with the other way around.
return (inWidth < inHeight && outWidth > outHeight) || (inWidth > inHeight && outWidth < outHeight);
}
}