In my last article I presented an architecture that included a memory-efficient model for scaling images. This article will use that architecture to kick some scaling butt.
Surely (?) developers have come before me that tackled this issue, right? This sounds like something ImageIO
might have addressed already, but when I googled "ImageIO to create BMP thumbnail" I didn't find any promising leads. (I did find this page which mentions the BMP reader doesn't support thumbnails.)
Reading BMPs
The trickiest part of this subject is: we need to be able to read BMPs. Yes, there is existing code that can do this. Mostly I'm thinking of
ImageIO
classes, but I'm sure there are other options too. The problem is: these interface with BufferedImages
, and my goal here is interface with my new PixelIterator
class.So I dusted off the file specifications and wrote my own BMP decoder.
As of this writing: this decoder is not fully featured. It supports 1, 4, 8, 24 and 32-bit uncompressed BMPs, but nothing else. It is my experience that the vast majority of BMPs in the world are uncompressed, though.
This functionality is nicely wrapped up in a few static classes in the BmpDecoder
.
Creating Thumbnails
With a
BytePixelIterator
to work with, and the previously established ScalingIterator: it's incredibly easy to scale BMP data on-the-fly.public static BufferedImage createThumbnail(InputStream bmp,
Dimension maxSize) throws IOException {
PixelIterator i = BmpDecoderIterator.get(bmp);
int srcW = i.getWidth();
int srcH = i.getHeight();
float widthRatio = ((float)maxSize.width)/((float)srcW);
float heightRatio = ((float)maxSize.height)/((float)srcH);
float ratio = Math.min(widthRatio, heightRatio);
if(ratio<1) {
i = ScalingIterator.get(i, ratio);
}
return BufferedImageIterator.create( i, null );
}
Writing BMPs
Also for good measure: I included a BmpEncoder
. The motivation for this class was actually for a separate project I may discuss later, but since it's related I'll include it here.
Results
So far you might be underwhelmed by this article: there's nothing especially clever or innovative here.
But consider this example. I have a folder on my computer called "bigpix". (A QA guy at work put this folder together.) I don't know where these files came from -- so I'm probably not legally allowed to distribute any of them -- but there's a file here called "ElPerroDelMar-Portratt.bmp". It's 2,469 pixels by 3,234 pixels.
If I wanted to create a thumbnail of this file that fit inside a 128x128 image, my first reaction is simply to use ImageIO
to read the file and then scale that image to the appropriate size. This requires creating an image that is 2469*3234*(3 colors) bytes (22 MB). I just allocated 22 MB to create an eventual 48 KB image.
This is ridiculous. And if the user is trying to navigate a folder of two dozen similar images (and you're creating thumbnails on-the-fly for all of them): you just used over 512 MB of RAM.
The Bmp.jar (source included) compares the cost of creating a thumbnail using the ImageIO
approach and the BmpDecoder
class. Also (without scaling) it pits the encoding ability of ImageIO
against BmpDecoder
. Here is the output when I ask the Bmp.jar
to work with the ElPerroDelMar image:
OS = Mac OS X (10.5.8), i386
Java Version = 1.5.0_24
apple.awt.graphics.UseQuartz = false
Running Thumbnail Tests...
BmpThumbnail: 72 ms, 332944 bytes RAM
ImageIO: 1065 ms, 23317200 bytes RAM
Running Encode Tests...
BmpEncoder: 585 ms, 332952 bytes RAM, 22987298 bytes file
ImageIO: 649 ms, 14932160 bytes RAM, 22987326 bytes file
As expected: the thumbnail generation is a huge improvement. The BmpDecoder
takes less than 1.5% of the memory ImageIO
uses, and less than 7% of the time.
I was surprised (but pleased) to see such a substantial advantage in the encoding tests, too. I'm not entirely sure what causes this, but the huge memory allocation suggests the ImageIO
classes are extracting all the image data before writing anything (instead of efficiently piping the image data).
No comments:
Post a Comment