Skip to content

QBufferedImage

jeremy edited this page May 7, 2023 · 3 revisions

This page discusses the QBufferedImage class.

Context

The BufferedImage is a vital part of any Java app with a UI. Over the years I've added a few enhancements. These may be subtle, but I think they're worth sharing.

Serialization

The QBufferedImage is serializable.

I have only tested this with int and byte backed images, but in theory it should work with all BufferedImages.

Specifically: The QBufferedImage class implements Externalizable. When serialized it calls getRaster().getDataElements(..) to produce a large array of its data. This array gets serialized.

On deserialization: we create a new QBufferedImage of the same width/height/type, read the large array back, and call getRaster().setDataElements(..).

The deserialized image will render exactly the same as the serialized image. However there may be some minor internal differences. For example: if several BufferedImages are actually subimages of a master BufferedImage: that information is lost. When deserialized: all the subimages will be independent copies and their rasters will not share a parent raster like they used to.

(Marginally related: see the ConverterUtils class for static helper methods that help serialize/deserialize other AWT-ish objects that normally aren't serializable. Like AlphaComposites, BasicStrokes, Shapes, etc.)

Adding Properties

The QBufferedImage class includes a setProperty(String, Object) method.

This is analogous to Image.getProperty(String, ImageObserver).

Now I can add small pieces of metadata to an image. For example: I can attach the filename it originated from. This is mentioned in the toString() method, so if it's mentioned in an exception I can immediately identify what image is associated with the error.

hashCode and equals Support

The hashing function looks at the RGB values of 5 points (the corners + the center).

And the equals method checks to make sure every pixel between two images is identical. Also for two QBufferedImages to be identical: they must use the same pixel format and have the same properties.

(Note: the ImagePixelIterator.equalPixels(Image, Image) also helps check image equivalency. It supports different pixel formats. For example: you can compare INT_ARGB with BYTE_BGR. As long as the images would render identically it will return true. It's also capable of comparing BufferedImages with generic Images.)

Improved ImageProducer

This may be a specific edge case, but if we ever call myBufferedImage.getSource(): it returns a sun.awt.image.OffScreenImageSource.

This ImageProducer doesn't call consumer.setHints(int). In my opinion this is a big deal, because that method signals to ImageConsumers whether you plan on delivering the pixels in top-down-left-right order (among other things). When I write a consumer I really need to know that.

Also this resolves this ticket about silently printing a NullPointerException to System.err. I submitted a PR for this, but even if it's accepted I want the Pumpernickel codebase to stay backwards compatible for several years at a time.

To be fair, though: it is very rare that I ever need to call getSource() on a BufferedImage. Especially since the Pumpernickel project includes ImagePixelIterators.

Scaling

I override getScaledInstance to use the Scaling classes. (There's a whole separate wiki on the Scaling class.) In my opinion this makes the getScaledInstance method usable again.

Construction / Conversion

You can easily convert an existing BufferedImage into a QBufferedImage by calling:

BufferedImage bi = [..];
QBufferedImage qbi = new QBufferedImage(bi);

This is technically a unique object, but it points to the same underlying raster and color model so it's fast.

Clone this wiki locally