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