Monday 24 October 2011

Android: Programmatically loading large image/bitmap files

Hello!

The use of imagery in our Apps is practically mandatory. After all, the Man said "make it look great".
Embedding icons and images as resources in an App is trivial and the Resources class makes loading them even easier.

But have you tried lately loading a large image file, let's say like an 8Mp picture? Well if you haven't, try it and you'll see that there's not enough memory for that. And don't ever embed a picture that large as a resource in your app.

So how can we load something that we can't load because there's not enough memory? Very carefully and in steps.

1) Loading bitmaps
The Android Bitmap class possesses several factory methods to create bitmaps but unfortunately it does not solve our problems - and I understand is also not recommended. What is left to us is the super awesome BitmapFactory class.
To create a bitmap object using BitmapFactory just call one of its several decode* factory methods, for instance:

...
Bitmap testBitmap=BitmapFactoryClass.decodeResource(resources, R.drawable.myimage);
// do stuff with testBitmap
// recycle testBitmap
...

this creates a Bitmap from the resource  myimage and stores in testBitmap. We can also load from a file on, let's say, the SD card

...
Bitmap testBitmap=BitmapFactoryClass.decodeFile("/mnt/sdcard/picture.jpg";
// do stuff with testBitmap
// recycle testBitmap
...


if we do that to load a 3000x4000 pixels large picture, it will blowup in our faces. It will probably run out of memory loading an image half this size.

To get around this problem, we need to sample the image during the decoding phase.

2) Sampling a large bitmap
the code snippet below does all the hard work of sampling an image and returning it as a Bitmap object.
A few notes before you use it:

BitmapFactoriy.Options is the secret weapon, more specifically the field inJustDecodeBounds. Setting it to true will tell the framework to not try loading the image, instead just return its size (returned in options.outHeight and options.outWidth). Once we have the dimensions of the image, we can read it scaled down by a factor defined in the field inSampleSize.

The SDK recomends using inSampleSize in powers of 2 so the line

int sampleSize=(int) Math.pow(2, Math.floor(Math.sqrt(factor)));
 
tries to find the best power of 2 close to the sample rate you requested. It's simplistic but works  fine for almost all cases. My suggestion is you find what's best for your app. I recommend sampling the image to a bigger size than what you really want and then use Bitmap.createScaledBitmap to set the image to the size you want.
  
BitmapFactory.Options options=new BitmapFactory.Options();
options.inJustDecodeBounds=true;
BitmapFactory.decodeFile(fileName, options);
     
long totalImagePixes=options.outHeight*options.outWidth;
totalScreenPixels=__some_maximum_number_of_pixels_supported;
   
if(totalImagePixes>totalScreenPixels){    
 double factor=(float)totalImagePixes/(float)(totalScreenPixels);
 int sampleSize=(int) Math.pow(2, Math.floor(Math.sqrt(factor)));
 options.inJustDecodeBounds=false;
 options.inSampleSize=sampleSize;
 return BitmapFactory.decodeFile(fileName, options);
}
      
return BitmapFactory.decodeFile(fileName); 
 
Also, not this the variable sets the maximum number of pixels (i.e, total bytes used) that we want the sampled image to have.
You can use an approach like that or just find a sample size based on your needs. Again, it's simplistic but works majority of cases.

To recap, all  you need is to set inJustDecodeBounds to true, call decodeFile, set inSampleSize to the desired value (power of 2 preferably) and call decodeFile again.

Have fun!