Handling Bitmaps

Level:
Level3

Once you know how bitmap files are formatted you'll be able to do a whole host of things you could never do before. Compress your bitmaps using your own routines, modify bitmap data/colouring on the fly, create your own bitmap files from scratch (screencaptures!), and many more! Excited? I know I am! :)

If you wish to follow along you can download the Visual Basic source for this tutorial. 

The first thing you'll find stored in a bitmap file is what's called the File Header structure:

Private Type BITMAPFILEHEADER
   bfType As Integer
   bfSize As Long
   bfReserved1 As Integer
   bfReserved2 As Integer
   bfOffBits As Long
End Type

This UDT (User-Defined Type) will be used to extract the first 14 bytes of information from the BMP file (an Integer takes up two bytes, a Long takes up four). bfType will always return 19778 which corresponds to the two character string, "BM" for bitmap. All bitmaps start with these two characters. bfSize will describe the entire file's size in bytes, bfReserved1 and bfReserved2 are reserved spaces and should simply be set to zero. bfOffBits tells us the byte offset from the beginning of the file at which the bitmap data starts (yes, I know it's called bfOffBITS, but it actually describes the number of bytes). So if bfOffBits = 1078 (as it should for most 8bit bitmaps) then we know that the header and colour table will be complete by the 1078th byte, and the picture data has begun.

Now for the Info Header structure:

Private Type BITMAPINFOHEADER
   biSize As Long
   biWidth As Long
   biHeight As Long
   biPlanes As Integer
   biBitCount As Integer
   biCompression As Long
   biSizeImage As Long
   biXPelsPerMeter As Long
   biYPelsPerMeter As Long
   biClrUsed As Long
   biClrImportant As Long
End Type 

biSize is the size of the BITMAPINFOHEADER structure given in bytes (usually equals 40). biWidth and biHeight describe the width and height of the bitmap in pixels, as you would expect. biPlanes describes the number of planes contained in the bitmap, this is not normally used, and is set to one

Now, biBitCount is an important one. It describes the "bit-depth" of this bitmap. It can have any of four values: 1, 4, 8, and 24. A bit depth of one indicates that the bitmap will have only two colours (monochrome), a bit depth of 4 will allow 16 colours, 8bit equals 256 colours, and 24bit is 16.8 million colours. Now the bit depth dictates whether or not a bitmap will use a colour table (discussed later). 24bit bitmaps DO NOT use a colour table, while the other bit formats do.

biCompression indicates whether or not RLE (Run Length Encoding) is used to compress the bitmap data; I won't get into the algorithm here. Simply set this to zero for no compression. biSizeImage contains the length of the bitmap image data (the actual pixels) in bytes. You might expect this to simply be equal to the width multiplied by the height, but it isn't always... more on that later.

biXPelsPerMeter and biYPelsPerMeter describe the resolution of the bitmap in pixels per meter. To go with standard resolution, just set these to zero. biClrUsed indicates how many of the colour table colours are actually used in the bitmap, set to zero to use all colours. biClrImportant tells the program which colours are most important, this can increase the display speed under some circumstances. Simply set this to zero for standard operation.

Now, we've described the bitmap, all that's left is to list the colours to be used (if it's not 24bit) and then store the actual per-pixel data. First, lets look at the structure used for the colour table (or palette):

Private Type RGBQUAD
   rgbBlue As Byte
   rgbGreen As Byte
   rgbRed As Byte
   rgbReserved As Byte
End Type 

Each of these rgb values are a number from 0-255 indicating the intensity of that particular channel. The rgbReserved byte must be set to zero. The colour table is comprised of a number of these RGBQUAD structures, the exact number of which depends on the bit depth of the bitmap. A 1bit bitmap will have two RGBQUADs describing its colour table (since a 1bit bitmap can have only two colours). A 4bit bitmap will have 16 RGBQUADs, and an 8bit bitmap will have 256. These values will be referred to by the per-pixel data stored later in the BMP file. For example, to display the colour white in a pixel, we would have to set up an RGBQUAD structure where each of the values (rgbBlue, rgbGreen, and rgbWhite) were equal to 255 and then refer to this structure by its order in the colour table. So if we set this as the first colour (that's zero in an array) in the table then any pixel referring to the zeroth colour will show up white. This will become more clear in a moment (I hope!).

24bit bitmaps, on the other hand, have no colour table because each pixel is made up of 3bytes, each describing the intensity of a specific colour channel (red, green, or blue). There is no need to look up a colour in the table since 16.8million can be described by each 3byte triplet. An 8bit bitmap, on the other hand, would only be able to display 256 colours without a colour table, since you can only store 256 discrete values within an 8bit span. Similarly, for 4bit bitmaps, only 16 combinations can be stored in the 4bit span. Therefore, having a colour look-up table at the start of the bitmap enables these lower bit formats to display a variety of colours, not simply a fixed set of 256 or 16.

Finally the bitmap data itself can be stored in a simple array of bytes:

Dim BMPData() As Byte 

You'll have to redim this array to the size of the biSizeImage member of the BITMAPINFOHEADER structure. As noted before, this value is not always simply the multiplication of the bitmap's height and width. This is due to 32bit boundary padding. You see, computers these days like things to be presented to them in 32bit chunks, so in order to ensure optimal performance, bitmaps are always encoded so that their "scan line boundaries" end on 32bit edges. That is to say, if your 8bit bitmap is supposed to be 3 pixels wide (3 pixels at 8bits per pixel = 24bits) then you'd be 8bits shy of the 32bit boundary. Eight zero padded bits would then have to be added to make up the difference. So, if your 8bit bitmap is 3 pixels wide and 3 pixels tall, you'd normally expect the pixel data to be stored in 72bytes (3pixels * 3pixels * 8bits per pixel) but as a result of the padding, each 3 pixel row (scan line) will end up being represented by 32bits rather than 24bits. That's 8 extra bits per row, for 3 rows. Our final total would then come to 96bits (3 rows * 32 bits per row). Understand?

This works just the same for the scan lines at any bit depth, they all have to end on 32bit boundaries. If a 1bit bitmap was 30 pixels wide, then there'd have to be 2bits of padding (30 pixels * 1bit per pixel = 30bits, 2 bits short). I'm sure you get the picture now.

The only other thing you need to know about bitmaps is that the pixel data is stored from the bottom left hand corner and progressing upward with scan lines from left to right. So the whole bottom row is stored first, then the second from the bottom, etc, until the final pixel from the top right-corner of the bitmap is stored at last. Bear this in mind when you store your data or read data from a bitmap, otherwise your picture will come out upside-down.

To open a bitmap and modify or extract the data, simply access it in binary mode:

Open "SAMPLE.BMP" For Binary Access Read Write Lock Write As #1 

You can then use Get and Put statements to extract or place the data. For example, to extract the BITMAPFILEHEADER and BITMAPINFOHEADER you can do this:

Dim BMPFileHeader As BITMAPFILEHEADER
Dim BMPInfoHeader As BITMAPINFOHEADER
    
Get #1, 1, BMPFileHeader
Get #1, , BMPInfoHeader

The BMPFileHeader variable should extract the data from the very start of the file, so we pass 1 as the second argument for the Get statement. The BMPInfoHeader variable data commences immediately following the BITMAPFILEHEADER data, and so we omit the second argument, indicating that we'd like to continue extracting data where we left off. You can then Get the RGBQUAD data, according to the number of entries in the colour table, and finally extract the bitmap pixel data itself, by Getting an appropriately sized array of bytes.

Click here for sample source code demonstrating how to put these principles to use. Hopefully you found this Visual Basic 6 tutorial usefull. Play around with the sample source and expand upon this to manipulate different bitmaps. If you have any questions or comments please use the Add Comments link below.

This tutorail is released under the GNU Free Documentation License 1.2. The original can be found here.

If you enjoyed this post, subscribe for updates (it's free)

Sample source code

Thanks for your tutorial. I was great to get me started on manipulating .BMP files.
Without it I would still know nothing about .BMP files .. Thanks again

Once I got in to it I began to find a few problems with your source code.
Firstly I found that the image size in ".biSizeImage" is in some files set to "0". Though, this I only found in some 24b BMP (that fit the 32b boundery) produced in MS Paint.
Second problem I found is that the alogrythm you use to calculate the number of padding bytes does not work for 24b BMP's.
The call the "GetPixelColour" does not work with 24b BMP that have padding bytes.
The order of the 3 byte 24b pixel data is BGR not RGB as expected.
Also the bit order for 1b BMP's is bit7->bit0.
I've added my code (it's a bit messy still, I'll clean it ups later when I get more time:))

Public Function ReadFile(fName As String) As Boolean
Dim Image_Pixels As Long
Dim p As Long
Dim cTable As Long
Dim nFileNum As Integer
Dim DataPadding As Integer
'On Error GoTo errRead

nFileNum = FreeFile
Open fName For Binary Access Read As #nFileNum
Get #nFileNum, 1, BMPFileHeader
Get #nFileNum, , BMPInfoHeader
ImageWidth = BMPInfoHeader.biWidth
ImageHeight = BMPInfoHeader.biHeight
'If the bit count is 8 or less then get the colour table
If BMPInfoHeader.biBitCount < 24 Then
cTable = (2 ^ BMPInfoHeader.biBitCount) - 1
ReDim BMPRGB(cTable)
Get #nFileNum, , BMPRGB()
End If
'Find out how much padding there will be at the end of each data row (if any)
'DataPadding = 32 - ((BMPInfoHeader.biWidth * BMPInfoHeader.biBitCount) Mod 32)
'If DataPadding = 32 Then DataPadding = 0
'DataPadding = DataPadding \ BMPInfoHeader.biBitCount

'Get the image size
If BMPInfoHeader.biSizeImage = 0 Then BMPInfoHeader.biSizeImage = BMPFileHeader.bfSize - BMPFileHeader.bfOffBits
'ImageSize = BMPFileHeader.bfSize - BMPFileHeader.bfOffBits
ReDim BMPData(BMPInfoHeader.biSizeImage - 1)
'Fill the BMPData array
Get #nFileNum, , BMPData
Close #nFileNum
Image_Pixels = ImageWidth * ImageHeight
ReDim ImageData(Image_Pixels - 1)
Select Case BMPInfoHeader.biBitCount
Case 24
DataPadding = (BMPInfoHeader.biSizeImage - (BMPInfoHeader.biWidth * BMPInfoHeader.biHeight * 3)) \ BMPInfoHeader.biHeight
Do
ImageData(p) = GetPixelColour(BMPInfoHeader.biBitCount, (p * 3) + ((p * 3) \ (BMPInfoHeader.biWidth * 3)) * DataPadding)
p = p + 1
Loop Until p = Image_Pixels
Case 8
DataPadding = (BMPInfoHeader.biSizeImage - (BMPInfoHeader.biWidth * BMPInfoHeader.biHeight)) \ BMPInfoHeader.biHeight
Do
ImageData(p) = GetPixelColour(BMPInfoHeader.biBitCount, p + (p \ BMPInfoHeader.biWidth) * DataPadding)
p = p + 1
Loop Until p = Image_Pixels
Case 4
DataPadding = (BMPInfoHeader.biSizeImage * 2 - (BMPInfoHeader.biWidth * BMPInfoHeader.biHeight)) \ BMPInfoHeader.biHeight
Do
ImageData(p) = GetPixelColour(BMPInfoHeader.biBitCount, p + (p \ BMPInfoHeader.biWidth) * DataPadding)
p = p + 1
Loop Until p = Image_Pixels
Case 1
DataPadding = (BMPInfoHeader.biSizeImage * 8 - (BMPInfoHeader.biWidth * BMPInfoHeader.biHeight)) / BMPInfoHeader.biHeight

Do
ImageData(p) = GetPixelColour(BMPInfoHeader.biBitCount, p + (p \ BMPInfoHeader.biWidth) * DataPadding)
p = p + 1
Loop Until p = Image_Pixels
End Select

'On Error GoTo 0
ReadFile = True
Exit Function
errRead:
Close #nFileNum
ReadFile = False
End Function

Private Function GetPixelColour(intBPP As Integer, lngPixelnum As Long) As Long
Dim blnFirstHalf As Boolean
Dim bytBitNum As Byte
Dim bytBitVal As Byte

'Find the colour of a given pixel within an array
'of data of given bit depth (Bits Per Pixel)

'If it's a 24bit bitmap produce the colour from the 3 bits
If intBPP = 24 Then
GetPixelColour = RGB(BMPData((lngPixelnum) + 2), BMPData((lngPixelnum) + 1), BMPData(lngPixelnum))
'If it's 8bit, look up the colour in the table
ElseIf intBPP = 8 Then
GetPixelColour = RGB(BMPRGB(BMPData(lngPixelnum)).rgbRed, BMPRGB(BMPData(lngPixelnum)).rgbGreen, BMPRGB(BMPData(lngPixelnum)).rgbBlue)
'If it's 4bit, split the byte and look up the colour in the table
ElseIf intBPP = 4 Then
'Find out which half of the byte we're using

blnFirstHalf = False
If lngPixelnum Mod 2 = 0 Then blnFirstHalf = True
'Extract the number from that half of the byte
Dim bytNum As Byte
If blnFirstHalf = True Then bytNum = Val("&H" & Left(Hex(BMPData(lngPixelnum \ 2)), 1))
If blnFirstHalf = False Then bytNum = Val("&H" & Right(Hex(BMPData(lngPixelnum \ 2)), 1))
'Return the colour from the colour table
GetPixelColour = RGB(BMPRGB(bytNum).rgbRed, BMPRGB(bytNum).rgbGreen, BMPRGB(bytNum).rgbBlue)
'If it's 1bit, split the byte into bits and look up the colour table
ElseIf intBPP = 1 Then
'Find which bit to use
'Start at the Hi bit
bytBitNum = 7 - (lngPixelnum Mod 8)
'Determine if the bit is set or not
bytBitVal = 0
If CByte(2 ^ bytBitNum) And BMPData(lngPixelnum \ 8) Then bytBitVal = 1
'Return the colour from the colour table
GetPixelColour = RGB(BMPRGB(bytBitVal).rgbRed, BMPRGB(bytBitVal).rgbGreen, BMPRGB(bytBitVal).rgbBlue)
End If

End Function