Java Tip 60: Saving bitmap files in Java

A tutorial -- including all the code you need to write a bitmap file from an image object

This tip complements Java Tip 43, which demonstrated the process of loading bitmap files in Java applications. This month, I follow up with a tutorial on how to save images in 24-bit bitmap files and a code snip you can use to write a bitmap file from an image object.

The ability to create a bitmap file opens many doors if you're working in a Microsoft Windows environment. On my last project, for example, I had to interface Java with Microsoft Access. The Java program allowed the user to draw a map on the screen. The map was then printed in a Microsoft Access report. Because Java doesn't support OLE, my only solution was to create a bitmap file of the map and tell the Microsoft Access report where to pick it up. If you've ever had to write an application to send an image to the clipboard, this tip may be of use to you -- especially if this information is being passed to another Windows application.

The format of a bitmap file

The bitmap file format supports 4-bit RLE (run length encoding), as well as 8-bit and 24-bit encoding. Because we're only dealing with the 24-bit format, let's take a look at the structure of the file.

The bitmap file is divided into three sections. I've laid them out for you below.

Section 1: Bitmap file header

This header contains information about the type size and layout of the bitmap file. The structure is as follows (taken from a C language structure definition):

typedef struct tagBITMAPFILEHEADER {
   UINT bfType;
   DWORD bfSize;
   UINT bfReserved1;
   UINT bfReserved2;
   DWORD bfOffBits;
}BITMAPFILEHEADER;

Here's a description of the code elements from the above listing:

  • bfType: Indicates the type of the file and is always set to BM.
  • bfSize: Specifies the size of the whole file in bytes.
  • bfReserved1: Reserved -- must be set to 0.
  • bfReserved2: Reserved -- must be set to 0.
  • bfOffBits: Specifies the byte offset from the BitmapFileHeader to the start of the image.

Here you've seen that the purpose of the bitmap header is to identify the bitmap file. Every program that reads bitmap files uses the bitmap header for file validation.

Section 2: Bitmap information header

The next header, called the information header, contains all the properties of the image itself.

Here's how you specify information about the dimension and the color format of a Windows 3.0 (or higher) device independent bitmap (DIB):

typedef struct tagBITMAPINFOHEADER {
    DWORD biSize;
    LONG  biWidth;
    LONG  biHeight;
    WORD  biPlanes;
    WORD  biBitCount;
    DWORD biCompression;
    DWORD biSizeImage;
    LONG  biXPelsPerMeter;
    LONG  biYPelsPerMeter;
    DWORD biClrUsed;
    DWORD biClrImportant;
} BITMAPINFOHEADER;

Each element of the above code listing is described below:

  • biSize: Specifies the number of bytes required by the BITMAPINFOHEADER structure.
  • biWidth: Specifies the width of the bitmap in pixels.
  • biHeight: Specifies the height of the bitmap in pixels.
  • biPlanes: Specifies the number of planes for the target device. This member must be set to 1.
  • biBitCount: Specifies the number of bits per pixel. This value must be 1, 4, 8, or 24.
  • biCompression: Specifies the type of compression for a compressed bitmap. In a 24-bit format, the variable is set to 0.
  • biSizeImage: Specifies the size in bytes of the image. It is valid to set this member to 0 if the bitmap is in the BI_RGB format.
  • biXPelsPerMeter: Specifies the horizontal resolution, in pixels per meter, of the target device for the bitmap. An application can use this value to select a bitmap from a resource group that best matches the characteristics of the current device.
  • biYPelsPerMeter: Specifies the vertical resolution, in pixels per meter, of the target device for the bitmap.
  • biClrUsed: Specifies the number of color indexes in the color table actually used by the bitmap. If biBitCount is set to 24, biClrUsed specifies the size of the reference color table used to optimize performance of Windows color palettes.
  • biClrImportant: Specifies the number of color indexes considered important for displaying the bitmap. If this value is 0, all colors are important.

Now all the information needed to create the image has been defined.

Section 3: Image

In the 24-bit format, each pixel in the image is represented by a series of three bytes of RGB stored as BRG. Each scan line is padded to an even 4-byte boundary. To complicate the process a little bit more, the image is stored from bottom to top, meaning that the first scan line is the last scan line in the image. The following figure shows both headers (BITMAPHEADER) and (BITMAPINFOHEADER) and part of the image. Each section is delimited by a vertical bar:

                 
0000000000    4D42   B536   0002   0000   0000   0036   0000 | 0028
0000000020    0000   0107   0000   00E0   0000   0001   0018   0000
0000000040    0000   B500   0002   0EC4   0000   0EC4   0000   0000
0000000060    0000   0000   0000 | FFFF   FFFF   FFFF   FFFF   FFFF
0000000100    FFFF   FFFF   FFFF   FFFF   FFFF   FFFF   FFFF   FFFF
*

Now, on to the code

Now that we know all about the structure of a 24-bit bitmap file, here's what you've been waiting for: the code to write a bitmap file from an image object.

import java.awt.*;
import java.io.*;
import java.awt.image.*;
public class BMPFile extends Component {
  //--- Private constants
  private final static int BITMAPFILEHEADER_SIZE = 14;
  private final static int BITMAPINFOHEADER_SIZE = 40;
  //--- Private variable declaration
  //--- Bitmap file header
  private byte bitmapFileHeader [] = new byte [14];
  private byte bfType [] = {'B', 'M'};
  private int bfSize = 0;
  private int bfReserved1 = 0;
  private int bfReserved2 = 0;
  private int bfOffBits = BITMAPFILEHEADER_SIZE + BITMAPINFOHEADER_SIZE;
  //--- Bitmap info header
  private byte bitmapInfoHeader [] = new byte [40];
  private int biSize = BITMAPINFOHEADER_SIZE;
  private int biWidth = 0;
  private int biHeight = 0;
  private int biPlanes = 1;
  private int biBitCount = 24;
  private int biCompression = 0;
  private int biSizeImage = 0x030000;
  private int biXPelsPerMeter = 0x0;
  private int biYPelsPerMeter = 0x0;
  private int biClrUsed = 0;
  private int biClrImportant = 0;
  //--- Bitmap raw data
  private int bitmap [];
  //--- File section
  private FileOutputStream fo;
  //--- Default constructor
  public BMPFile() {
  }
  public void saveBitmap (String parFilename, Image parImage, int
parWidth, int parHeight) {
     try {
        fo = new FileOutputStream (parFilename);
        save (parImage, parWidth, parHeight);
        fo.close ();        
     }
     catch (Exception saveEx) {
        saveEx.printStackTrace ();
     }
  }
  /*
   *  The saveMethod is the main method of the process. This method
   *  will call the convertImage method to convert the memory image to
   *  a byte array; method writeBitmapFileHeader creates and writes
   *  the bitmap file header; writeBitmapInfoHeader creates the 
   *  information header; and writeBitmap writes the image.
   *
   */
  private void save (Image parImage, int parWidth, int parHeight) {
     try {
        convertImage (parImage, parWidth, parHeight);
        writeBitmapFileHeader ();
        writeBitmapInfoHeader ();
        writeBitmap ();
     }
     catch (Exception saveEx) {
        saveEx.printStackTrace ();
     }
  }
  /*
   * convertImage converts the memory image to the bitmap format (BRG).
   * It also computes some information for the bitmap info header.
   *
   */
  private boolean convertImage (Image parImage, int parWidth, int parHeight) {
     int pad;
     bitmap = new int [parWidth * parHeight];
     PixelGrabber pg = new PixelGrabber (parImage, 0, 0, parWidth, parHeight,
                                         bitmap, 0, parWidth);
     try {
        pg.grabPixels ();
     }
     catch (InterruptedException e) {
        e.printStackTrace ();
        return (false);
     }
     pad = (4 - ((parWidth * 3) % 4)) * parHeight;
     biSizeImage = ((parWidth * parHeight) * 3) + pad;
     bfSize = biSizeImage + BITMAPFILEHEADER_SIZE +
BITMAPINFOHEADER_SIZE;
     biWidth = parWidth;
     biHeight = parHeight;
     return (true);
  }
  /*
   * writeBitmap converts the image returned from the pixel grabber to
   * the format required. Remember: scan lines are inverted in
   * a bitmap file!
   *
   * Each scan line must be padded to an even 4-byte boundary.
   */
  private void writeBitmap () {
      int size;
      int value;
      int j;
      int i;
      int rowCount;
      int rowIndex;
      int lastRowIndex;
      int pad;
      int padCount;
      byte rgb [] = new byte [3];
      size = (biWidth * biHeight) - 1;
      pad = 4 - ((biWidth * 3) % 4);
      if (pad == 4)   // <==== Bug correction
         pad = 0;     // <==== Bug correction
      rowCount = 1;
      padCount = 0;
      rowIndex = size - biWidth;
      lastRowIndex = rowIndex;
      try {
         for (j = 0; j < size; j++) {
            value = bitmap [rowIndex];
            rgb [0] = (byte) (value & 0xFF);
            rgb [1] = (byte) ((value >> 8) & 0xFF);
            rgb [2] = (byte) ((value >>  16) & 0xFF);
            fo.write (rgb);
            if (rowCount == biWidth) {
               padCount += pad;
               for (i = 1; i <= pad; i++) {
                  fo.write (0x00);
               }
               rowCount = 1;
               rowIndex = lastRowIndex - biWidth;
               lastRowIndex = rowIndex;
            }
            else
               rowCount++;
            rowIndex++;
         }
         //--- Update the size of the file
         bfSize += padCount - pad;
         biSizeImage += padCount - pad;
      }
      catch (Exception wb) {
         wb.printStackTrace ();
      }
   }  
  /*
   * writeBitmapFileHeader writes the bitmap file header to the file.
   *
   */
  private void writeBitmapFileHeader () {
     try {
        fo.write (bfType);
        fo.write (intToDWord (bfSize));
        fo.write (intToWord (bfReserved1));
        fo.write (intToWord (bfReserved2));
        fo.write (intToDWord (bfOffBits));
     }
     catch (Exception wbfh) {
        wbfh.printStackTrace ();
     }
  }
  /*
   *
   * writeBitmapInfoHeader writes the bitmap information header
   * to the file.
   *
   */
  private void writeBitmapInfoHeader () {
     try {
        fo.write (intToDWord (biSize));
        fo.write (intToDWord (biWidth));
        fo.write (intToDWord (biHeight));
        fo.write (intToWord (biPlanes));
        fo.write (intToWord (biBitCount));
        fo.write (intToDWord (biCompression));
        fo.write (intToDWord (biSizeImage));
        fo.write (intToDWord (biXPelsPerMeter));
        fo.write (intToDWord (biYPelsPerMeter));
        fo.write (intToDWord (biClrUsed));
        fo.write (intToDWord (biClrImportant));
     }
     catch (Exception wbih) {
        wbih.printStackTrace ();
     }
  }
  /*
   *
   * intToWord converts an int to a word, where the return
   * value is stored in a 2-byte array.
   *
   */
  private byte [] intToWord (int parValue) {
     byte retValue [] = new byte [2];
     retValue [0] = (byte) (parValue & 0x00FF);
     retValue [1] = (byte) ((parValue >>  8) & 0x00FF);
     return (retValue);
  }
  /*
   *
   * intToDWord converts an int to a double word, where the return
   * value is stored in a 4-byte array.
   *
   */
  private byte [] intToDWord (int parValue) {
     byte retValue [] = new byte [4];
     retValue [0] = (byte) (parValue & 0x00FF);
     retValue [1] = (byte) ((parValue >>  8) & 0x000000FF);
     retValue [2] = (byte) ((parValue >>  16) & 0x000000FF);
     retValue [3] = (byte) ((parValue >>  24) & 0x000000FF);
     return (retValue);
  }
}

Conclusion

That's all there is to it. I'm sure you'll find this class very useful, since, as of JDK 1.1.6, Java doesn't support saving images in any of the popular formats. JDK 1.2 will offer support for creating JPEG images, but not support for bitmaps. So this class will still fill a gap in JDK 1.2.

If you play around with this class and find ways to improve it, let me know! My e-mail appears below, along with my bio.

Jean-Pierre Dubé is an independent Java consultant. He founded Infocom, registered in 1988. Since then, Infocom has developed several custom applications ranging from manufacturing, document management, and large-scale electrical power-line management. He has extensive programming experience in C, Visual Basic, and most recently Java, which is now the primary language used by his company. One of Infocom's recent projects is a diagram API that should become available as a beta release soon.
Related: