Dummy’s guide to drawing raw images in Java 2D

12

A couple months ago I signed up to do a presentation on concurrency for the St. Louis Java User Group, which happened last night. I wanted to explore a bunch of techniques and issues but stick to one domain, so I chose generation of the Mandelbrot set, just for fun.

It takes me way back to my middle school days, when I wrote my first Mandelbrot program in Turbo Pascal on my IBM PC (the original one). At the time, understanding how to program the algorithm was the challenging part – working through the recursive algorithm, splitting the complex and real parts of the calculation, understanding the escape tests, etc. Actually writing the picture to the screen was easy – for each pixel you just did something like draw(x,y,color). The original IBM PC was CGA so “color” meant a massive 2 bits: cyan, magenta, aqua, or black. I’d set up a particular coordinate region, kick it off, and go to bed. If I was lucky, it would be done painting the screen before I left for school the next morning. Then I could PrintScreen it to my dot matrix, stuff it in my TrapperKeeper, and strap on my Zips. Yeah, I’ve always been a nerd.

I roughed out the equivalent in Java the other night and found life has changed a lot. It took me only a few minutes to build up the algorithm and actually start printing out some really crummy Mandelbrots in text with the characters “.oO”. Then (not being a GUI guy) I started looking at how to actually draw some pixels on the screen in Java. Gadzooks! You gotta know about BufferedImages and ColorModels and Rasters and SampleModels. These fancy displays we’ve got have made life pretty complicated. But in the end, the key thing is that you want to paint some pixel on your screen a certain color.

I called this a dummy’s guide because I’m a total novice at this stuff. So, when I say really stupid stuff below, please have a good laugh and then drop me a comment or an email so I can fix it. This article is not exhaustive – if you want that, go buy a book. I’m trying to give you the skeleton as far as I understand it and some key insights and examples.

The Model

Java has had java.awt.Image forever as a way to read in and represent a chunk of image data. But there was no writable or editable equivalent until Java2D was added and we now have BufferedImage.

As always, remember that the goal is to paint pixels. The BufferedImage is comprised of a Raster, a description of per-pixel data values, and a ColorModel that describes how to take each set of pixel values and derive a color for that pixel. Conceptually, for each pixel in the image, the Raster is consulted for a set of values describing a pixel, then the ColorModel is used to determine the color corresponding to that set of values.

At this point in the detail we must acknowledge that there is a high amount of variability in the actual display system in use. For high performance, it is desirable to represent data close to how the display wants to receive it. The data types, the way they are packed, the order they are packed in, and so on all differ across the vast Java universe. Providing both a general programming abstraction and a high performance solution is difficult. I’ve seen this happen in design before and there is no good solution – typically you just pick a middle road and try to balance the forces. I feel that tension in these classes.

Inside the Raster, the pixel data is broken into two parts: the DataBuffer and the SampleModel. The DataBuffer stores the actual pixel data as one or more arrays of values, each called a bank. The values in each bank may be of a variety of types (byte, short, unsigned short, integer, float, double). To understand the data within the DataBuffer, we need the SampleModel, which describes how to extract a particular pixel’s data to pass to the ColorModel. The SampleModel is your map to unlock the mysteries of a particular DataBuffer.

For example, DataBuffers might store red, green, and blue pixels each in a separate bank. Or it might store them interleaved in a single bank. Or something else.

To summarize the general flow of information:

  • DataBuffer holds raw data values in some form
  • SampleModel understands how to read the DataBuffer and produce one or more values describing a single pixel
  • ColorModel understands how to take the data values describing a pixel and produce the appropriate color on the display

Colors

Before talking further about data models, let’s talk about colors first. ColorModels are nothing more than translators (in both directions) between a set of samples (data values) describing a pixel and colors. Of course, there are many ways to do this conversion and many ways to specify the samples.

The most direct way to specify a color model is with ComponentColorModel where each sample value maps directly to the components of a color (RGB for example). The ComponentColorModel expects to receive a set of values (an int[] for example) where each value of the array represents red, green, or blue.

There is also a PackedColorModel that expects to receive all of the samples for a pixel in one value. The PackedColorModel unpacks the single value using a set of specified bit masks to retrieve each channel from the single value. A subclass called DirectColorModel deals specifically with the most common case of RGB packed values.

The IndexColorModel is used for cases where you have a fixed palette of colors and a single value serves as an index into that palette (list) of colors. In this case, the pixel value does not represent components of color but the choice of a particular color by index.

Putting it all together

There are a zillion ways to put all these pieces together and much better references than this page on how to do so. But here I’ll show you an actual example for what I needed to do. I wanted to just calculate an iteration value for each pixel and then use that to index into a simple color palette with 256 colors.

public class PointPanel extends JPanel {
// lots of code to set up the config for generating the image
private ColorModel colorModel = calculateColorModel();

public void paintComponent(Graphics g) {
if(needsRepaint()) {
int width = getWidth(); // Get actual current dimensions of panel
int heigh = getHeight();

byte[] data = generateData(width, height, …);
drawImage(g, data);
}
}

private void drawImage(Graphics g, byte[] data) {
DataBuffer buffer = new DataBufferByte(data, data.length);
SampleModel sm = colorModel.createCompatibleSampleModel(getWidth(), getHeight());
WritableRaster raster = Raster.createWritableRaster(sm, buffer, null);
BufferedImage img = new BufferedImage(colorModel, raster, false, null);
g.drawImage(img, 0, 0, null);
}

private ColorModel createColorModel() {
byte[] reds = new byte[colors];
byte[] greens = new byte[colors];
byte[] blues = new byte[colors];

for(int i=0; i<256; i++) {
reds[i] = (byte) i;
greens[i] = (byte) i;
blues[i] = (byte) i;
}
return new IndexColorModel(8, 256, reds, greens, blues);
}

private boolean needsRepaint() {
// determine whether I’ve already painted these bounds and image to avoid repaint
}

private byte[] generateData(int width, int height, …) {
// actually calculate the pixel values
}

So, you’ll notice here that the data is in a single array and not a 2-dimensional array. This is done to the data buffer and sample model that I’m using that expects all of the data to be spanned into a single long array. The index color model I’m creating above is really dumb. I actually defined a gradient in my model but the code is too long to reproduce here.

All the various pieces get stuck together to create a BufferedImage and that is simply draw onto the Graphics object.

This is not the most exciting example but hopefully it’s good enough that you know where to look to make modifications. Good luck!

Comments

12 Responses to “Dummy’s guide to drawing raw images in Java 2D”
  1. kiu says:

    BufferedImage bi = new BufferedImagte(width, heigth, BufferedImage.TYPE_INT_RGB );

    JPanel jp = new JPanel();
    jp.add(new ImageIcon(bi));

    Graphics g = bi.getGraphics();

    //paint you stuff
    g.setColor(myColor);
    g.drawLine(x,y,x,y);

  2. Alex says:

    Yeah, you can find this example all over the place. This is not about drawing lines though, it’s about painting individual pixels.

  3. Hari Jayaram says:

    I wonder how you would do this in processing?

  4. Martin says:

    Example above is slightly incorrect, try this:

    public static void main(String[] args)
    {
    JFrame f = new JFrame(“Drawing test”);
    f.getContentPane().add(new JScrollPane(new JComponent()
    {
    public void paint(Graphics g)
    {
    g.setColor(Color.RED);
    g.drawLine(20, 30, 140, 250);
    g.fillArc(20, 20, 200, 200, 200, 200);
    }
    }));
    f.pack();
    f.addWindowListener(new WindowAdapter()
    {
    public void windowClosing(WindowEvent evt)
    {
    System.exit(0);
    }
    });
    f.setVisible(true);
    }

  5. Alex says:

    That’s great, but this post is NOT about line drawing. It’s about painting individual pixels.

  6. Quintesse says:

    @Alex: I guess what the other are trying to tell you is: it’s all the same!

    A pixel is just a line with a length of one afterall.
    It’s very common for modern hardware to not have any support for drawing sperate pixels and trying to do so will result in much slower code. Using Java2D’s drawLine() might use some sort of hardware acceleration for example.

  7. Matt Nathan says:

    I think it’s actually better to draw a 1×1 rectangle instead of a line of length 1 as the Java2D guys have to jump through less hoops to discover it’s a single pixel.

  8. waqas says:

    You probably don’t know about this, but you can paint individual pixels or pixel ranges on a BufferedImage using the setRGB(…) methods.

  9. Alex says:

    Actually, I wrote my first version of this using those setRGB() methods. They work fine, but you are essentially limited to some (possibly good enough) constraints about the color model and data representation as these methods assume standard RGB and 8-bit color. You are also giving up a lot of control over how the data is stored and passed.

  10. Dmitri Trembovetski says:

    One comment about the implementation you do not have to
    create an image every time. And hopefully
    you are not creating the array either and just reusing it
    (assuming it doesn’t change size).

    Typically you’d get a pixel array from the already created
    DataBuffer (BufferedImage.getRaster().getDataBuffer().getData(),
    although you’ll need to cast to the particular DataBuffer subclass
    for the getData() call, like DataBufferInt) and just modify it.

    In general while there may be some cases where direct pixel access is
    warranted (like if your pixels are generated by some
    hardware – a video stream, or you are doing some per-pixel
    effects) in the majority of cases you should just use
    the Graphics (Graphics2D) context for rendering.

    While currently rendering to a BufferedImage is not
    hardware accelerated in Sun implementation we do have
    a highly optimized software rendering pipeline..

    Other rendering destinations (such is VolatileImage and
    BufferStrategy) may have hardware accelerated rendering
    operations so you might consider using them as the
    back-buffer for your application (BufferStrategy is the
    preferred way of doing it, btw).

    Thanks,
    Dmitri
    Java2D Team

  11. If you want to play with Java 2D you can try out my prototype tool Teppefall FX. You have to use Javascript instead of Java, but that gives you no compile cycle and the ability to reload via ctrl-r.

    FX also supports rendering over RMI and server side image generators (license required). The CAPTCHA images on my website are powered by the same framework.

    http://app.teppefall.com/download

    /jp
    Teppefall

  12. lokesh says:

    hi,
    I am working on raw data of images using which i have to draw image i am able to draw different image format data like .jpg,.bmp, .png. but i am facing a problem to draw a bmp image which is of index colormodel type and has 8 bits per pixel can you please assit me regarding this.