How to Scale a Streaming Bitmap In-Place Without Reading The Whole Image First

How do I scale a streaming bitmap in-place without reading the whole image first?

This method will read the header information from the image to determine its size, then read the image and scale it to the desired size in place without allocating memory for the full original sized image.

It also uses BitmapFactory.Options.inPurgeable, which seems to be a sparsely documented but desirable option to prevent OoM exceptions when using lots of bitmaps. UPDATE: no longer uses inPurgeable, see this note from Romain

It works by using a BufferedInputStream to read the header information for the image before reading the entire image in via the InputStream.

/**
* Read the image from the stream and create a bitmap scaled to the desired
* size. Resulting bitmap will be at least as large as the
* desired minimum specified dimensions and will keep the image proportions
* correct during scaling.
*/
protected Bitmap createScaledBitmapFromStream( InputStream s, int minimumDesiredBitmapWith, int minimumDesiredBitmapHeight ) {

final BufferedInputStream is = new BufferedInputStream(s, 32*1024);
try {
final Options decodeBitmapOptions = new Options();
// For further memory savings, you may want to consider using this option
// decodeBitmapOptions.inPreferredConfig = Config.RGB_565; // Uses 2-bytes instead of default 4 per pixel

if( minimumDesiredBitmapWidth >0 && minimumDesiredBitmapHeight >0 ) {
final Options decodeBoundsOptions = new Options();
decodeBoundsOptions.inJustDecodeBounds = true;
is.mark(32*1024); // 32k is probably overkill, but 8k is insufficient for some jpgs
BitmapFactory.decodeStream(is,null,decodeBoundsOptions);
is.reset();

final int originalWidth = decodeBoundsOptions.outWidth;
final int originalHeight = decodeBoundsOptions.outHeight;

// inSampleSize prefers multiples of 2, but we prefer to prioritize memory savings
decodeBitmapOptions.inSampleSize= Math.max(1,Math.min(originalWidth / minimumDesiredBitmapWidth, originalHeight / minimumDesiredBitmapHeight));

}

return BitmapFactory.decodeStream(is,null,decodeBitmapOptions);

} catch( IOException e ) {
throw new RuntimeException(e); // this shouldn't happen
} finally {
try {
is.close();
} catch( IOException ignored ) {}
}

}

Memory efficient way to scale an image

I'm glad you asked, I was about to investigate this for my own project.

It looks like BitmapFactory.Options is your friend here, specifically BitmapFactory.Options.inSampleSize enter link description here. You can use BitmapFactory.decodeStream to get the image dimensions without creating a Bitmap.

Googling revealed the com.bristle.javalib.awt ImgUtil classes; these appear to be open source (don't know the license), and are AWT based, so may work on Android. The interesting method is ImgUtil.scalueImageToOutputStreamAsJPEG, which works with InputSteam/OutputStream, so may be memory efficient.

UPDATE

An alternative is to write a JNI wrapper around /system/lib/libjpeg.so, which appears to be standard on Android devices. This library works with scanlines, so can be memory friendly. Another plus is that using JNI should be faster than pure java.

Hope this helps,

Phil Lello

Resize a large bitmap file to scaled output file on Android

No. I'd love for someone to correct me, but I accepted the load/resize approach you tried as a compromise.

Here are the steps for anyone browsing:

  1. Calculate the maximum possible inSampleSize that still yields an image larger than your target.
  2. Load the image using BitmapFactory.decodeFile(file, options), passing inSampleSize as an option.
  3. Resize to the desired dimensions using Bitmap.createScaledBitmap().

Resizing bitmap from InputStream

Actually you have two different BufferedInputStream but they internally use the only one InputStream object because BufferedInputStream is only a wrapper for InputStream.

So you can't just call two times BitmapFactory.decodeStream method on the same stream, it will definitely fail because the second time it wouldn't start decoding from the beginning of the stream. You need to reset your stream if it is supported or reopen it.

Preserve the image quality when decode stream in android

The following code uses several concepts from Displaying Bitmaps Efficiently

First off the bitmap reading is done in a background thread, I'm using mark / reset on inputStream (wrapped with BufferedInputstream) to not read more than necessary from stream when we try to find out the size of the image to use when calculating scale factor. The example code below subsamples the image to match a size of 320x240 pixles. In a non example code one could have simple callback interface send the bitmap from onPostExecute to implementing class (callback interface implementer). Or provide the view as a member tot the AsyncTask directly and set the bitmap in onPostExecute.

Call the code with (example downloaded image on my device):

BitmapTask task = new BitmapTask(getContentResolver());
task.execute(Uri.parse("file:///storage/emulated/0/Download/download.jpg"));

The classes in question

private static class BitmapTask extends AsyncTask<Uri, Void, Bitmap> {

// prevent mem leaks
private WeakReference<ContentResolver> mWeakContentResolver;

public BitmapTask(ContentResolver resolver) {
mWeakContentResolver = new WeakReference<ContentResolver>(resolver);
}

@Override
protected Bitmap doInBackground(Uri... params) {
Bitmap bitmap = null;
ContentResolver resolver = mWeakContentResolver.get();
if (resolver != null) {
BufferedInputStream stream = null;
try {
stream = new BufferedInputStream(
resolver.openInputStream(params[0]));
stream.mark(1 * 1024);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
// Find out size of image
BitmapFactory.decodeStream(stream, null, options);
try {
stream.reset();
} catch (IOException e) {
Log.d(TAG, "reset failed");
}
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;
Log.d(TAG, "w, h, mime " + imageWidth + " , " + imageHeight
+ " , " + imageType);
options.inJustDecodeBounds = false;
// Calculate down scale factor
options.inSampleSize = calculateInSampleSize(options, 320,
240);
return BitmapFactory.decodeStream(stream, null, options);
} catch (FileNotFoundException e) {
bitmap = null;
} finally {
IOUtils.closeStreamSilently(stream);
}
}
return bitmap;
}

@Override
protected void onPostExecute(Bitmap result) {
Log.d(TAG,
"bitmap result: "
+ ((result != null) ? "" + result.getByteCount()
: "0"));
result.recycle();
}
}

public static int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;

if (height > reqHeight || 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 ((halfHeight / inSampleSize) > reqHeight
&& (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}

return inSampleSize;
}

Edit: For large inputstreams there might be a problem with the mark/reset technique, SkImageDecoder::Factory returned null can be seen in logs sometimes, resulting in null bitmap, se other SO question on the matter: SkImageDecoder::Factory returned null. It can be fixed by reiniting the stream variable again stream = new resolver.openInputStream(params[0])); before return in doInBackground

Edit 2: If you have to preserve the image size but wan't to limit memory usage you could use the options.inPreferredConfig = Bitmap.Config.RGB_565; that halves the memory per pixel, but bare in mind that the images might not have great quality anymore (experiment!).

Trying to scale an image and return a bitmap from android gallery

You need to seek back to the start of the InputStream after reading the bounds before calling decodeStream for a second time, otherwise you will end up with an invalid Bitmap. The easiest way is just to close the stream and open it again.

Try this code (note function is no longer static to allow calling getContentResolver() and you have pass in a Uri not an InputStream):

public Bitmap getBitmapFromReturnedImage(Uri selectedImage, int reqWidth, int reqHeight) throws IOException {

InputStream inputStream = getContentResolver().openInputStream(selectedImage);

// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStream, null, options);

// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

// close the input stream
inputStream.close();

// reopen the input stream
inputStream = getContentResolver().openInputStream(selectedImage);

// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeStream(inputStream, null, options);
}

call like this

Uri selectedImage = data.getData();
imageBitmap = getBitmapFromReturnedImage(selectedImage, 800, 800);


Related Topics



Leave a reply



Submit