BMP FILES
This article will discuss how to read and display BMP files. BMP files
can be 2, 16, 256 or 16.7M colors. We are going to deal with the
256 color versions only. Because they are the easiest to show on a 256
color VGA card.
BMP FILE LAYOUT
As with almost all graphics files the first part of the BMP file is
called the header. The header contain all the information about the
image. After the header is the palette (if there is one). Finally comes
the image information itself.
HEADER LAYOUT
Here is the layout of the BMP header as seen by Turbo Pascal:
Type
TBMPHeader = Record
ID: Array[0..1] of Char; { Must be 'BM' }
BMPFileSize: LongInt; { Size of this file }
Reserved: LongInt; { ??? }
HeaderSize: LongInt; { Size of header }
InfoSize: LongInt; { Size of info that follows header }
Width, Height: LongInt; { Width and Height of image }
biPlanes, Bits: Integer; { Bits can be 1, 4, 8, or 24 }
biCompression, biSizeImage, { We don't care about anything else }
biXPelsPerMeter, biYPelsPerMeter,
biClrUsed, biClrImportant: LongInt;
End;
Looks we have quite a bit of information about the image. Want we really
care about is the width and height and the bits.
Here is the code to read the header:
----------------- SNIP HERE --------------------
Program BMPHead;
Type
TBMPHeader = Record
ID: Array[0..1] of Char; { Must be 'BM' }
BMPFileSize: LongInt; { Size of this file }
Reserved: LongInt; { ??? }
HeaderSize: LongInt; { Size of header }
InfoSize: LongInt; { Size of info that follows header }
Width, Height: LongInt; { Width and Height of image }
biPlanes, Bits: Integer; { Bits can be 1, 4, 8, or 24 }
biCompression, biSizeImage, { We don't care about anything else }
biXPelsPerMeter, biYPelsPerMeter,
biClrUsed, biClrImportant: LongInt;
End;
Var
BMPFile: File;
BMPHeader: TBMPHeader;
Begin
Assign(BMPFile, 'BABY.BMP'); { NOTE: Change 'TEST.BMP' to another name }
Reset(BMPFile, 1); { We want to read the file 1 byte at a time }
BlockRead(BMPFile, BMPHeader, SizeOf(BMPHeader)); { Read the header }
Close(BMPFile);
WriteLn('Image Width= ', BMPHeader.Width);
WriteLn('Image Height=', BMPHeader.Height);
WriteLn('Image Bits= ', BMPHeader.Bits);
End.
----------------- SNIP HERE --------------------
IMAGES WITH PALETTES
If the bits value in the header is 8 then that means that the image is
a 256 color image that uses a palette. In this case we must get the
palette from the BMP file. Luckily the palette is right after the header.
For some reason microsoft made each palette entry 4 bytes long, even
though we only need 3 bytes (RGB) for the VGA card. The palette of course
will have 256 entries. Not only is the BMP palette entry 4 bytes long, but
the first three are in the reverse order. Also the color values range from
0 to 255, but the VGA card excepts only 0 to 63, so we have to divide each
of them by 4. Here is how to read the palette from the BMP file:
----------------- SNIP HERE --------------------
Program BMPPal;
Type
TBMPHeader = Record
ID: Array[0..1] of Char; { Must be 'BM' }
BMPFileSize: LongInt; { Size of this file }
Reserved: LongInt; { ??? }
HeaderSize: LongInt; { Size of header }
InfoSize: LongInt; { Size of info that follows header }
Width, Height: LongInt; { Width and Height of image }
biPlanes, Bits: Integer; { Bits can be 1, 4, 8, or 24 }
biCompression, biSizeImage, { We don't care about anything else }
biXPelsPerMeter, biYPelsPerMeter,
biClrUsed, biClrImportant: LongInt;
End;
Var
BMPFile: File;
BMPHeader: TBMPHeader;
BMPPaletteEntry: Array[0..3] of Byte; { BMP file 4 byte palette entry }
VGAPalette: Array[0..255, 0..2] of Byte; { Complete VGA palette }
I: Integer;
Begin
Assign(BMPFile, 'BABY.BMP'); { NOTE: Change 'TEST.BMP' to another name }
Reset(BMPFile, 1); { We want to read the file 1 byte at a time }
BlockRead(BMPFile, BMPHeader, SizeOf(BMPHeader)); { Read the header }
If BMPHeader.Bits = 8 Then
Begin
For I:=0 to 255 Do
Begin
BlockRead(BMPFile, BMPPaletteEntry, SizeOf(BMPPaletteEntry));
VGAPalette[I, 0]:=BMPPaletteEntry[2] Div 4; { Note reverse order 2,1,0}
VGAPalette[I, 1]:=BMPPaletteEntry[1] Div 4;
VGAPalette[I, 2]:=BMPPaletteEntry[0] Div 4;
End;
End;
Close(BMPFile);
WriteLn('Image Width= ', BMPHeader.Width);
WriteLn('Image Height=', BMPHeader.Height);
WriteLn('Image Bits= ', BMPHeader.Bits);
End.
----------------- SNIP HERE --------------------
SETTING THE VGA PALETTE
Now we have the image's palette now we have to set the VGA palette to the
image palette. Since the palette has 256 entries first we must get the
VGA card into a mode that supports 256 colors. The standard mode is
320x200 with 256 colors. To set this mode is very easy. However, we must
call the video bios directly since TP doesn't support this mode.
Here is what we do to set mode 13:
Asm
MOV AX,$0013
INT $10
End;
Now that we are in a 256 color mode let's set the palette. If you don't
set the palette the image will show using the default palette which is
terrible. We will use the direct hardware access method to set the palette:
For I:=0 to 255 Do
Begin
Port[$3C8]:=I;
Port[$03C9]:=VGAPalette[I, 0];
Port[$03C9]:=VGAPalette[I, 1];
Port[$03C9]:=VGAPalette[I, 2];
End;
To use this method just set port $03C8 to the color you want to set.
Then set port $03C9 with the Red, Green and Blue values.
PLOTTING 256 COLOR PIXELS
Ok, so now we are in 256 color mode and we have the palette set, now how
do we set pixels ? Well this video mode is very popular because it is so
easy to set pixels. First we must know that the video memory for this
mode starts at $A000:0000 in memory. Also each pixel is 1 byte. The pixel
in the upper left hand corner is at the start of video memory. Then next
pixel to the right is next and so on across the top row. The next byte
in video memory after the last pixel in the top row is the first pixel
in the next row down.
0 .... 319
320 .... 639
640 .... 959
And so on for 200 vertical lines. To make is easy on ourselves lets just
declare and array like this:
Var
VGAMemory: Array[0..199, 0..319] of Byte Absolute $A000:$0000;
Notice that the y range is the first dimension and the x is the second.
If you have never used the Absolute keyword basicly it just mean to place
the variable as a specific place in memory instead of at the next free
place.
Now setting pixel is a piece of cake. Just do this:
VGAMemory[Y, X]:=Color;
Here is a program that does the following:
Reads the BMP header.
Reads the BMP palette. (needs to be a 256 color image)
Set the VGA card for mode 13.
Set the VGA palette to the image palette.
Draws 256 vertical lines, one in each palette color.
----------------- SNIP HERE --------------------
Program ShowPal;
Uses Crt{TextMode};
Type
TBMPHeader = Record
ID: Array[0..1] of Char; { Must be 'BM' }
BMPFileSize: LongInt; { Size of this file }
Reserved: LongInt; { ??? }
HeaderSize: LongInt; { Size of header }
InfoSize: LongInt; { Size of info that follows header }
Width, Height: LongInt; { Width and Height of image }
biPlanes, Bits: Integer; { Bits can be 1, 4, 8, or 24 }
biCompression, biSizeImage, { We don't care about anything else }
biXPelsPerMeter, biYPelsPerMeter,
biClrUsed, biClrImportant: LongInt;
End;
Var
VGAMemory: Array[0..199, 0..319] of Byte Absolute $A000:$0000;
BMPFile: File;
BMPHeader: TBMPHeader;
BMPPaletteEntry: Array[0..3] of Byte; { BMP file 4 byte palette entry }
VGAPalette: Array[0..255, 0..2] of Byte; { Complete VGA palette }
I, X, Y: Integer;
Begin
Assign(BMPFile, 'BABY.BMP'); { NOTE: Change 'TEST.BMP' to another name }
Reset(BMPFile, 1); { We want to read the file 1 byte at a time }
BlockRead(BMPFile, BMPHeader, SizeOf(BMPHeader)); { Read the header }
If BMPHeader.Bits = 8 Then
Begin
For I:=0 to 255 Do
Begin
BlockRead(BMPFile, BMPPaletteEntry, SizeOf(BMPPaletteEntry));
VGAPalette[I, 0]:=BMPPaletteEntry[0];
VGAPalette[I, 1]:=BMPPaletteEntry[1];
VGAPalette[I, 2]:=BMPPaletteEntry[2];
End;
End
Else
Begin
WriteLn('Need to use a 256 color BMP file for this program.');
Close(BMPFile);
Halt;
End;
Close(BMPFile);
{ Put VGA card in mode 13 }
Asm
MOV AX,$0013
INT $10
End;
{ Set all palette entries }
For I:=0 to 255 Do
Begin
Port[$03C8]:=I;
Port[$03C9]:=VGAPalette[I, 0];
Port[$03C9]:=VGAPalette[I, 1];
Port[$03C9]:=VGAPalette[I, 2];
End;
{ Draw vertical lines }
For X:=0 to 255 Do
Begin
For Y:=0 to 199 Do VGAMemory[Y, X]:=X;
End;
{ Wait for user to press enter }
ReadLn;
TextMode(CO80);
End.
----------------- SNIP HERE --------------------
Well, that's everything except showing the image. The image data follows
the header and palette. Microsoft throws another monkey wrench in here too.
Each line of the image is padded to be an even multiple of 4. For example
if the image is 398 bytes wide. Each line in the file will have 400 bytes.
So we have to calculate filewidth like this:
FileWidth:=((BMPHeader.Width + 3) Div 4) * 4);
And just for kicks, microsoft make the bottom line in the image the
first line in the BMP file. (Someone had a little too much Jolt(R) cola
when they were writting the layout). To get around this problem I just
calculate where the lines start and seek to the first line, read it, then
seek to the second line. It's a slow method but it works.
IMAGES WITHOUT A PALETTE (TRUECOLOR)
Can you imagine 16.7 Million colors! Well that's how many colors are
possible with a 24-bit truecolor image. Of course if you made an image
that had all 16.7 million colors is would need over 50 megs! Another
problem, how do you display an image that can have 16.7 million colors
on a vga screen that can only show 256 colors? Well, we have to cheat.
Look at it this way a 24-bit uses 8-bits for each of the Red, Green and
Blue values. We have only 8-bits. So to split them up we give Red 3 bits,
we give green 3 bits, and blue 2 bits. This is because our eyes are less
sensitive to blue light. We lose 2/3 of the color information but the
images are not too bad, you can at least tell what it is. There are much
better ways to handle this problem, but this is just some code to show
you how to read true color images. Here again microsoft is up to it's
tricks, the colors are stored in reverse order for EACH pixel of the image.
This mean the color are stored as Blue, Green, Red. If anyone knows why
this was done please let me know as I can see absolutly no reason for it.
HERE'S THE CODE
----------------- SNIP HERE --------------------
Program ShowBMP;
Uses Crt{TextMode};
Type
TBMPHeader = Record
ID: Array[0..1] of Char; { Must be 'BM' }
BMPFileSize: LongInt; { Size of this file }
Reserved: LongInt; { ??? }
HeaderSize: LongInt; { Size of header }
InfoSize: LongInt; { Size of info that follows header }
Width, Height: LongInt; { Width and Height of image }
biPlanes, Bits: Integer; { Bits can be 1, 4, 8, or 24 }
biCompression, biSizeImage, { We don't care about anything else }
biXPelsPerMeter, biYPelsPerMeter,
biClrUsed, biClrImportant: LongInt;
End;
{ This is the structure of the BMP truecolor data }
TPixelBGR = Record Blue, Green, Red: Byte; End;
Var
{ Map an array to the VGA screen memory }
VGAMemory: Array[0..199, 0..319] of Byte Absolute $A000:$0000;
BMPFile: File;
BMPHeader: TBMPHeader;
BMPPaletteEntry: Array[0..3] of Byte; { BMP file 4 byte palette entry }
VGAPalette: Array[0..255, 0..2] of Byte; { Complete VGA palette }
I, X, Y: Integer;
FileWidth: Word;
ShowHeight: Integer;
ShowWidth: Integer;
Pixel: Byte;
PixelBGR: TPixelBGR;
Begin
Assign(BMPFile, 'C:\BABYTC.BMP'); { NOTE: Change 'TEST.BMP' to another name }
Reset(BMPFile, 1); { We want to read the file 1 byte at a time }
BlockRead(BMPFile, BMPHeader, SizeOf(BMPHeader)); { Read the header }
{ Check if image is 8 or 24 bits }
If (BMPHeader.Bits <> 8) and (BMPHeader.Bits <> 24) Then
Begin
WriteLn('Need to use a 256 or 16.7M color BMP file for this program.');
Close(BMPFile);
Halt;
End;
{ If a 256 color image then read palette }
If BMPHeader.Bits = 8 Then
Begin
{ Read the palette }
For I:=0 to 255 Do
Begin
BlockRead(BMPFile, BMPPaletteEntry, SizeOf(BMPPaletteEntry));
VGAPalette[I, 0]:=BMPPaletteEntry[2] SHR 2;
VGAPalette[I, 1]:=BMPPaletteEntry[1] SHR 2;
VGAPalette[I, 2]:=BMPPaletteEntry[0] SHR 2;
End;
End
Else { Must be a 24 bit image to setup palette }
Begin
{ Setup mapping palette }
For I:=0 to 255 Do
Begin
VGAPalette[I, 0]:=(I AND 7) * 8 + 7;
VGAPalette[I, 1]:=((I SHR 3) AND 7) * 8 + 7;
VGAPalette[I, 2]:=(I SHR 6) * 16 + 15;
End;
End;
{ Put VGA card in mode 13 }
Asm
MOV AX,$0013
INT $10
End;
{ Set all palette entries }
For I:=0 to 255 Do
Begin
Port[$03C8]:=I;
Port[$03C9]:=VGAPalette[I, 0];
Port[$03C9]:=VGAPalette[I, 1];
Port[$03C9]:=VGAPalette[I, 2];
End;
If BMPHeader.Bits = 8 Then
FileWidth:=((BMPHeader.Width +3) Div 4) * 4
Else
FileWidth:=((BMPHeader.Width * 3 + 3) Div 4) * 4;
ShowHeight:=BMPHeader.Height;
If ShowHeight > 200 Then ShowHeight:=200; { Can only show 200 lines }
ShowWidth:=BMPHeader.Width;
If ShowWidth > 320 Then ShowWidth:=320; { Can only show 320 pixels }
{ Y loops through each vertical line of the image }
For Y:=0 to ShowHeight -1 Do
Begin
{ Position file to line Y }
Seek(BMPFile, BMPHeader.HeaderSize + FileWidth * (BMPHeader.Height - Y - 1));
For X:=0 to ShowWidth -1 Do
Begin
If BMPHeader.Bits = 8 Then { Handle 8-bit pixel }
BlockRead(BMPFile, Pixel, SizeOf(Pixel))
Else
Begin { Handle 24-bit pixel }
BlockRead(BMPFile, PixelBGR, SizeOf(PixelBGR));
{ Map RGB to our much smaller palette index }
Pixel:=PixelBGR.Red SHR 5 + ((PixelBGR.Green SHR 5) SHL 3) +
(PixelBGR.Blue AND 192);
End;
{ Set the pixel on the screen }
VGAMemory[Y, X]:=Pixel;
End;
End;
{ Wait for user to press enter }
ReadLn;
{ Return to text mode }
TextMode(CO80);
End.
----------------- SNIP HERE --------------------
There that wasn't too hard. There is quite a bit of data moving around in
this program. It tends to be rather slow. But, again this is just for
demonstration. Next article will be how to print BMP images in gray
scale on a PCL printer (that's Laserjet/Deskjet printers).
Bean, thitt@igateway.com
               (
geocities.com/SiliconValley/2926)                   (
geocities.com/SiliconValley)