12 March 2007

Rendering PNGs in Java

This post details an optimization problem that took me on a long and circuitous route. I'm writing it as documentation of my trail of tears. If anyone else is having problems with Java rendering 8bit png's slowly, or if the png's are taking up too much memory, I would endorse the approach described below. Without a tRNS chunk and color palette I consistently see poorer performance out of the Java render pipeline.

I have an application that works in the GIS mode of operation. There is a base map and on top of that base map are layers of geographically positioned images. For various reasons, all of these images are in the PNG format. Each image, except for the base layer, consists of two colors: black, which is set to fully transparent, and some other color, set to fully opaque. To control the navigation of the geographic context I use the terrific Piccolo API, an API designed for developing Zoomable User Interfaces. The application works very well; layers are easy to add, piccolo provides all the proper hooks to make user coordinate to geographic coordinate conversions, and the pan/zoom interactions are easy to use. There was but one problem: performance.

Especially on windows, the application performance is actually pretty good, particularly in light of the fact that all image rendering is happening on the main CPU rather than the graphics card. I tried to get the opengl pipeline in Java working through various tactics, but none of them worked reliably enough to make me feel confident that I could manage the install on other peoples computers, much less my own. Further, when I tried to use Java's default opengl pipeline the render performance on my zippy windows box slowed to a crawl. I didn't realize this until I tested on Linux, where the opengl pipeline is enabled by default (disabled by default in windows). If you see really slow rendering of PNGs with java, try passing the command line argument -Dsun.java2d.opengl=false, it makes a world of difference in my application.

So, I'm not complaining about the speed, it's "good enough". However, because I am rendering in memory on the main CPU my application was quite a resource hog. I'm using at least 3 images that are 4000x5000 pixels and a fourth that can be that big as well. While each of those images weighs in at a svelte 100k on disk, when you render, the full raster info needs to sit in an array. This means that at best, the smallest in-memory size for each of these images will be 2.5MB (20Mpx * 1bit / 8). But to make matters worse, because I hadn't adjusted the defaults on my image producing process, I was generating images with 64 bits per pixel. Using IrfanView I see that a 5000x4000, downconverted to 24 Bits per pixel, uses 60MB of memory. Additionally, IrfanView says this image takes 1.6 seconds to load, and that's using native code. No wonder my application wasn't performing as well as it could!

Okay, this is easy, I just need to convert to a lower Bit Per Pixel (BPP) level and I'm set. Not so easy, of course. From all of my testing, it looks to me like Java's png rendering is optimized for certain paths. For instance, when I converted my images to 1BPP (remember, my images only have two colors so I can do this), not only did the Java memory consumption not go down at all, I actually saw a significant slow down in the rendering speed in my application.

Through much trial and error, I found that a combination of 8bit pixels and a color palette was the friendliest format for Java to render. Now, I just tell ImageMagick that this is what I want and it's as easy as that, right? Wrong... of course. ImageMagick can generate 8bit images, but it only does it the GIF way, with binary transparency (i.e. some color is declared the transparent color and all instances of that color in the image are drawn transparent). Sadly, the png renderer in Java just flat out refused to honor this format (I didn't fiddle with this much). What Java really likes, as far as I can tell, is what png calls RGBA-Palette. This is a format that is unique to PNG, so I'm not surprised that ImageMagick does not support it. However, it is a cool format. What it does is allow for an additional block of metadata called tRNS. tRNS is an array of tranparency values from 0-255 that are applied to the color palette's array, so any color in the color palette can have a transparency value added to it. So here's what I did to get tRNS into my image.

  • Create my colored image with 8BPP using ImageMagic
    • C:\test_8bit>convert test.png -transparent black -fill red -opaque white -map nestscape: test2.png
      • Note, I use the nestcape color map to force 8bit color. When I don't use it I get a 4 color image back and for some reason the next step of inserting the tRNS chunk does not work with that image.
      • test.png is a grayscale image that consists of black and white pixels. The imagemagick command will convert black pixels to transparent and white to red.

  • Insert the tRNS chunk.

    • C:\test_8bit>pngcrush -trns_array 1 0 test2.png test3.png
      • Because the netscape color map starts with black, the trns_array needs to only contain one entry. The first number after -trns_array is the array length, which is then followed by array elements. 0 sets the first color in the color palette (black) to transparent.

And that's it. I now have transparent 8-bit images that render quickly, take up as little memory as possible and use less than a fifth of the disk space than they did originally. Woohoo!

Useful PNG Tools
Through all this fooling around, I came across a couple useful tools for working with PNGs. The two most useful for my purposes were PNGCRUSH and TweakPng. TweakPng is the tool that I used to examine PNG files that I knew worked in my system. Through that, i was able to manually manipulate the images I was getting back from ImageMagick and keep tweaking those images until they rendered as I wanted in Java. PNGCRUSH was then the followup to TweakPng that let me insert the tRNS programatically. Of course, if you really need to hack at PNGs there's always libpng as well...

Labels: , ,