When I was first debugging my image watermark code, I found it strange it kept running out of memory. Why!?
You're doing it wrong!
Firstly, I was trying to edit an immutable (non-editable) Bitmap. You need to generate a mutable copy of the bitmap before you can start modifying it.
Secondly, my method was very heavy on memory usage. It involved loading the original immutable +4mega-pixel image into memory, then creating a new mutable Bitmap of equal size and colour depth, copying it over using a Canvas.
Keep in mind, the Android VM only allows 16M of memory per app. Each pixel in your image using ARGB_8888 can use up to 4 bytes of memory.
To help you get a perspective of things, a 32bit image with 724x724 has 524,176 pixels uses up 15.9M of memory. If you squish the sizes a bit, that's not very much on a mobile device, especially when you factor in that you need memory for other things in your app such as GUI elements and references to your Activity.
What does that mean? It means you can't load that 8mb photo on that SD card in full resolution no matter how hard you try. Your app has to make some compromises and trim it down a bit.
Not to mention my third step was to imprint a logo over the mutable new bitmap, which means loading yet another image into memory.
This is what the current situation felt like...
Looking for an answer
The internet and StackOverflow are littered with questions on this issue. Most responses tell you to either reduce the colour depth, partially process the bitmap or call Bitmap.recycle() on the source image before creating a new Bitmap.
Now that doesn't make much sense if I'm trying to COPY an image from Bitmap A to Bitmap B because I need both of them in order to do that.
Option A - Bump up the API level
BitmapFactory.Options has a new flag inMutable that allows loading of mutable bitmaps in Honeycomb (API level 11), but I try to maintain as much compatibility as possible.
If you choose to go this way, you will lose some of your market share. See the Android documentation for more information about which API level corresponds to the Android platform versions.
Since I don't actually have any Honeycomb compatible devices, this simply wasn't an option.
Option B - Load a smaller image
My preferred solution is to "pre-scale" the source bitmap so it isn't so damn big. That means loading it in a smaller immutable resolution so you have enough memory to hold both bitmaps at the same time.
The ideal size is 630px, since that's what Facebook uses for the photo album pictures and most people seem to be happy with it.
The downside is that scales works best in the power of 2's, so you can load an image that is 1/2 the size of the original, 1/4th the size, 1/8th the size, etc.
So the method here is:
- Open image and fetch it's dimensions
- Scale the image accordingly so it'll meet our "maximum pixels" criteria.
- Load a pre-scaled image.
- Create a new mutable bitmap with the same dimensions
- Copy it via a Canvas
- Recycle the source bitmap
private Bitmap loadPrescaledBitmap(String filename) throws IOException {
// Facebook image size
final int IMAGE_MAX_SIZE = 630;
File file = null;
FileInputStream fis;
BitmapFactory.Options opts;
int resizeScale;
Bitmap bmp;
file = new File(filename);
// This bit determines only the width/height of the bitmap without loading the contents
opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
fis = new FileInputStream(file);
BitmapFactory.decodeStream(fis, null, opts);
fis.close();
// Find the correct scale value. It should be a power of 2
resizeScale = 1;
if (opts.outHeight > IMAGE_MAX_SIZE || opts.outWidth > IMAGE_MAX_SIZE) {
resizeScale = (int)Math.pow(2, (int) Math.round(Math.log(IMAGE_MAX_SIZE / (double) Math.max(opts.outHeight, opts.outWidth)) / Math.log(0.5)));
}
// Load pre-scaled bitmap
opts = new BitmapFactory.Options();
opts.inSampleSize = resizeScale;
fis = new FileInputStream(file);
bmp = BitmapFactory.decodeStream(fis, null, opts);
fis.close();
return bmp;
}
The rest of the code can be found here.
Option C - Stash the bitmap away in a file
Well, a very interesting and creative method from Sudar Nimalan that will allow you to copy the full resolution image into a mutable form. It is however much slower.
His method involves:
- Loading the original bitmap
- Stashing the bitmap to a MappedByteBuffer
- Recycling the original bitmap
- Creating a new mutable bitmap
- Copying the bitmap from the MappedByteBuffer
It's not as hard as it sounds and the code sample is quite short. Worth a look at if you're keen on keeping the image resolution intact.