Java Tip 43: How to read 8- and 24-bit Microsoft Windows bitmaps in Java applications

A step-by-step guide to loading bitmap files in a Java application

Reading Microsoft Windows bitmap files in Java applications is not officially supported in the current release of Java. But fear not, there is a way! This tip will show you how to accomplish this task -- starting with a description of the basic steps involved in reading the Microsoft Windows file format.

The Windows DIB (Device-Independent Bitmap) file format is relatively straightforward. The DIB format preserves explicit information unlike the plain bitmap format which is used for storage of the image in RAM. The problem is that there are so many image format varieties (1, 4, 8, and 16 bits, among others). The solution offered in this Java Tip will address only the 8- and 24-bit flavors. These two represent the most widely encountered varieties.

Regardless of the Windows DIB sub-type, the file format always consists of a 14-byte file header and a 40-byte information header. These two headers contain information about exactly what is stored in the file and in what order. Consult the Microsoft Software Development Kit (SDK) for the exact and precise meaning of each item in the headers. The remaining file contents vary depending on the data in the information header.

Let's look at the two sub-types we're addressing here. The 24-bit flavor is very simple: The RGB (Red-Green-Blue) color values (3 bytes, in BGR order) follow immediately after the information header. However, each scan line is padded out to an even 4-byte boundary. This "padding" is documented (see Microsoft SDK) to be an optimization for the Windows bitmap drawing APIs. Also, the bottom scan line is the first thing encountered in the file -- so the image has to be read backwards in relation to the usual graphics coordinate systems in which the positive vector orientation is down and to the right.

The 8-bit sub-type is complicated by the insertion of color palette information between the information header and the pixel data. Therefore, each pixel entry is simply an 8-bit index into the palette array of 24-bit RGB colors. Once again, the pixel information is padded out to an even 4-byte boundary for each scan line.

Note that the bitmap image-loading method presented here does not support decompression of compressed bitmap images. In fact, this routine does not even look for this possibility! This routine is sure to generate an exception if a compressed Windows DIB file is encountered. The compressed Windows DIB format is documented in the Windows SDK.

In terms of performance, this routine can read a 24-bit, 640 x 480 file (approximately 920 kilobytes) in less than 10 seconds on a 486-DX2-66MHz running Microsoft Windows 95. Performance can be signficantly enhanced by using BufferedInputStream instead of FileInputStream.

TEXTBOX: TEXTBOX_HEAD: Follow-Up Tip! from Don McQuinn

The loadbitmap method in Java Tip 43 may have a bug. For the 24-bit format, it computes the pad ("npad"), which can be 4. When npad is computed as 4, it should be set to 0, as is done in the writeBitmap method in Java Tip 60. :END_TEXTBOX

The following routine reads either of the two file formats and generates an Image object. Comprehensive error and exception handling is not included below to keep from muddying the routine further. An unsupported Windows DIB sub-type can always be converted using the Windows Paint program.

    /**
    loadbitmap() method converted from Windows C code.
    Reads only uncompressed 24- and 8-bit images.  Tested with
    images saved using Microsoft Paint in Windows 95.  If the image
    is not a 24- or 8-bit image, the program refuses to even try.
    I guess one could include 4-bit images by masking the byte
    by first 1100 and then 0011.  I am not really 
    interested in such images.  If a compressed image is attempted,
    the routine will probably fail by generating an IOException.
    Look for variable ncompression to be different from 0 to indicate
    compression is present.
    Arguments:
        sdir and sfile are the result of the FileDialog()
        getDirectory() and getFile() methods.
    Returns:
        Image Object, be sure to check for (Image)null !!!!
    */
    public Image loadbitmap (String sdir, String sfile)
    {
    Image image;
    System.out.println("loading:"+sdir+sfile);
    try
        {
        FileInputStream fs=new FileInputStream(sdir+sfile);
        int bflen=14;  // 14 byte BITMAPFILEHEADER
        byte bf[]=new byte[bflen];
        fs.read(bf,0,bflen);
        int bilen=40; // 40-byte BITMAPINFOHEADER
        byte bi[]=new byte[bilen];
        fs.read(bi,0,bilen);
        // Interperet data.
        int nsize = (((int)bf[5]&0xff)<<24) 
        | (((int)bf[4]&0xff)<<16)
        | (((int)bf[3]&0xff)<<8)
        | (int)bf[2]&0xff;
        System.out.println("File type is :"+(char)bf[0]+(char)bf[1]);
        System.out.println("Size of file is :"+nsize);
        int nbisize = (((int)bi[3]&0xff)<<24)
        | (((int)bi[2]&0xff)<<16)
        | (((int)bi[1]&0xff)<<8)
        | (int)bi[0]&0xff;
        System.out.println("Size of bitmapinfoheader is :"+nbisize);
        int nwidth = (((int)bi[7]&0xff)<<24)
        | (((int)bi[6]&0xff)<<16)
        | (((int)bi[5]&0xff)<<8)
        | (int)bi[4]&0xff;
        System.out.println("Width is :"+nwidth);
        int nheight = (((int)bi[11]&0xff)<<24)
        | (((int)bi[10]&0xff)<<16)
        | (((int)bi[9]&0xff)<<8)
        | (int)bi[8]&0xff;
        System.out.println("Height is :"+nheight);
        int nplanes = (((int)bi[13]&0xff)<<8) | (int)bi[12]&0xff;
        System.out.println("Planes is :"+nplanes);
        int nbitcount = (((int)bi[15]&0xff)<<8) | (int)bi[14]&0xff;
        System.out.println("BitCount is :"+nbitcount);
        // Look for non-zero values to indicate compression
        int ncompression = (((int)bi[19])<<24)
        | (((int)bi[18])<<16)
        | (((int)bi[17])<<8)
        | (int)bi[16];
        System.out.println("Compression is :"+ncompression);
        int nsizeimage = (((int)bi[23]&0xff)<<24)
        | (((int)bi[22]&0xff)<<16)
        | (((int)bi[21]&0xff)<<8)
        | (int)bi[20]&0xff;
        System.out.println("SizeImage is :"+nsizeimage);
        int nxpm = (((int)bi[27]&0xff)<<24)
        | (((int)bi[26]&0xff)<<16)
        | (((int)bi[25]&0xff)<<8)
        | (int)bi[24]&0xff;
        System.out.println("X-Pixels per meter is :"+nxpm);
        int nypm = (((int)bi[31]&0xff)<<24)
        | (((int)bi[30]&0xff)<<16)
        | (((int)bi[29]&0xff)<<8)
        | (int)bi[28]&0xff;
        System.out.println("Y-Pixels per meter is :"+nypm);
        int nclrused = (((int)bi[35]&0xff)<<24)
        | (((int)bi[34]&0xff)<<16)
        | (((int)bi[33]&0xff)<<8)
        | (int)bi[32]&0xff;
        System.out.println("Colors used are :"+nclrused);
        int nclrimp = (((int)bi[39]&0xff)<<24)
        | (((int)bi[38]&0xff)<<16)
        | (((int)bi[37]&0xff)<<8)
        | (int)bi[36]&0xff;
        System.out.println("Colors important are :"+nclrimp);
        if (nbitcount==24)
        {
        // No Palatte data for 24-bit format but scan lines are
        // padded out to even 4-byte boundaries.
        int npad = (nsizeimage / nheight) - nwidth * 3;
        int ndata[] = new int [nheight * nwidth];
        byte brgb[] = new byte [( nwidth + npad) * 3 * nheight];
        fs.read (brgb, 0, (nwidth + npad) * 3 * nheight);
        int nindex = 0;
        for (int j = 0; j < nheight; j++)
            {
            for (int i = 0; i < nwidth; i++)
            {
            ndata [nwidth * (nheight - j - 1) + i] =
                (255&0xff)<<24
                | (((int)brgb[nindex+2]&0xff)<<16)
                | (((int)brgb[nindex+1]&0xff)<<8)
                | (int)brgb[nindex]&0xff;
            // System.out.println("Encoded Color at ("
                +i+","+j+")is:"+nrgb+" (R,G,B)= ("
                +((int)(brgb[2]) & 0xff)+","
                +((int)brgb[1]&0xff)+","
                +((int)brgb[0]&0xff)+")");
            nindex += 3;
            }
            nindex += npad;
            }
        image = createImage
            ( new MemoryImageSource (nwidth, nheight,
                         ndata, 0, nwidth));
        }
        else if (nbitcount == 8)
        {
        // Have to determine the number of colors, the clrsused
        // parameter is dominant if it is greater than zero.  If
        // zero, calculate colors based on bitsperpixel.
        int nNumColors = 0;
        if (nclrused > 0)
            {
            nNumColors = nclrused;
            }
        else
            {
            nNumColors = (1&0xff)<<nbitcount;
            }
        System.out.println("The number of Colors is"+nNumColors);
        // Some bitmaps do not have the sizeimage field calculated
        // Ferret out these cases and fix 'em.
        if (nsizeimage == 0)
            {
            nsizeimage = ((((nwidth*nbitcount)+31) & ~31 ) >> 3);
            nsizeimage *= nheight;
            System.out.println("nsizeimage (backup) is"+nsizeimage);
            }
        // Read the palatte colors.
        int  npalette[] = new int [nNumColors];
        byte bpalette[] = new byte [nNumColors*4];
        fs.read (bpalette, 0, nNumColors*4);
        int nindex8 = 0;
        for (int n = 0; n < nNumColors; n++)
            {
            npalette[n] = (255&0xff)<<24
            | (((int)bpalette[nindex8+2]&0xff)<<16)
            | (((int)bpalette[nindex8+1]&0xff)<<8)
            | (int)bpalette[nindex8]&0xff;
            // System.out.println ("Palette Color "+n
            +" is:"+npalette[n]+" (res,R,G,B)= ("
            +((int)(bpalette[nindex8+3]) & 0xff)+","
            +((int)(bpalette[nindex8+2]) & 0xff)+","
            +((int)bpalette[nindex8+1]&0xff)+","
            +((int)bpalette[nindex8]&0xff)+")");
            nindex8 += 4;
            }
        // Read the image data (actually indices into the palette)
        // Scan lines are still padded out to even 4-byte
        // boundaries.
        int npad8 = (nsizeimage / nheight) - nwidth;
        System.out.println("nPad is:"+npad8);
        int  ndata8[] = new int [nwidth*nheight];
        byte bdata[] = new byte [(nwidth+npad8)*nheight];
        fs.read (bdata, 0, (nwidth+npad8)*nheight);
        nindex8 = 0;
        for (int j8 = 0; j8 < nheight; j8++)
            {
            for (int i8 = 0; i8 < nwidth; i8++)
            {
            ndata8 [nwidth*(nheight-j8-1)+i8] =
                npalette [((int)bdata[nindex8]&0xff)];
            nindex8++;
            }
            nindex8 += npad8;
            }
        image = createImage
            ( new MemoryImageSource (nwidth, nheight,
                         ndata8, 0, nwidth));
        }
        else
        {
        System.out.println ("Not a 24-bit or 8-bit Windows Bitmap, aborting...");
        image = (Image)null;
        }
        fs.close();
        return image;
        }
    catch (Exception e)
        {
        System.out.println("Caught exception in loadbitmap!");
        }
    return (Image) null;
    }

And there you have it. This method can be extended easily to read the monochrome and 16-color (4-bit) formats.

Jeff West is an engineering graduate student in San Diego, CA. In between doing research on combustion and flame spread, he gets his Java fix.
Join the discussion
Be the first to comment on this article. Our Commenting Policies