By CGI Joe Dec 9th 2000

Loading BMP files in QBasic

Here I will give you the general format of a .bmp image file and outline the steps needed to load and display one on the screen. Although there are many libraries that will do this for you, it's nice to write your own code and one day, you might be writing your own library =).

BMP images are actualy one of the simplest image formats as there is no compression involved.
The header is 1078 bytes long and contains vital elements such as image dimensions, colour depth and the actual palette. Following is a QBasic TYPE structure that we can use to load the image in one go: TYPE BMPHeaderType id AS STRING * 2 'Should be "BM" size AS LONG 'Size of the data rr1 AS INTEGER ' rr2 AS INTEGER ' offset AS LONG 'Position of start of pixel data horz AS LONG ' wid AS LONG 'Image width hei AS LONG 'Image height planes AS INTEGER ' bpp AS INTEGER 'Should read 8 for a 256 colour image pakbyte AS LONG ' imagebytes AS LONG 'Width*Height xres AS LONG ' yres AS LONG ' colch AS LONG ' ic AS LONG ' pal AS STRING * 1024 'Stored as <Blue, Green, Red, 0> END TYPE
I've commented the important fields. We can use these to verify the contents of the file. Now to grab the header, all we need to do is DIM an instance of this structure, open the file and read the data. We open the file in BINARY mode so we can access the file data directly. We'll use a made-up BMP file called "demo.bmp". Here's the code: DIM BmpHeader AS BMPHeaderType OPEN "demo.bmp" FOR BINARY AS #1 GET #1, , BmpHeader ' Don't close the file just yet - we're not finished!
Now we have all the data we need to load the image. From here, the rest of the file just contains the pixel data. After the GET, the file pointer is moved to the first pixel. But before we start displaying anything, we should change our current palette to the one given in our image file. The palette is stored in our header as 256 DWords (4 bytes). For each colour, 3 bytes contain the RGB palette values, while the 4th is just used for padding and is set to zero. Here's how to set the palette: SCREEN 13 ' Set graphics mode a$ = BmpHeader.pal ' Pal is stored in a 1024 character string OUT &H3C8, 0 ' Start writing from Colour 0 FOR I% = 1 TO 1024 STEP 4 b% = ASC(MID$(a$, I%, 1)) \ 4 'blue g% = ASC(MID$(a$, I% + 1, 1)) \ 4 'green r% = ASC(MID$(a$, I% + 2, 1)) \ 4 'red ' I% + 3 is set to zero. OUT &H3C9, r% ' Set the colour. OUT &H3C9, g% OUT &H3C9, b% NEXT
Now we're ready to show something! What we do is, create two FOR .. NEXT (x and y) which extend to the dimensions of our image. Now the very weird thing here is, the image is actually stored in the file upside down! I don't know why they chose to do this, but we have to modify our Y loop to count backwards instead. Each pixel consumes exactly one byte, so this is how we read it. Unfotunately, QBasic does not have a Byte-sized (excuse the pun ;) variable. So we improvise and DIM a string of size 1, this also holds one byte. Here's the code .. DIM Pixel AS STRING * 1 ' Our pixel "byte". iHeight% = BmpHeader.hei - 1 ' Subtract 1 for actual screen position iWidth% = BmpHeader.wid - 1 ' FOR y% = iHeight% TO 0 STEP -1 ' Countdown for upsidedown image FOR x% = 0 TO iWidth% GET #1, , Pixel ' read pixel ' Read one pixel (byte) PSET (x%, y%), ASC(Pixel) ' Pixel is actually a string so we get the pixel ' number by requesting the "ASC" value NEXT x%, y% CLOSE #1
That's it! Unless your image is bigger than 320x200, you will have your BMP image on-screen.

Making it faster

Unless you have a really fast computer (greater than 400Mhz) you will be unsatisfied by the speed in which the image loaded. There are a couple of optimisations we can apply here.
  1. Intead of reading one byte at a time, read BmpHeader.wid bytes and load a line at a time
  2. We can do a memcopy from our pixel buffer to display memory instead of PSETting.

The first one is easy. We simply create a string of size BmpHeader.wid bytes. We can't do the familiar DIM statement this time, as QBasic only allows constants in that declaration. Still, we have a workaround. All we do is set a string to a specified number of SPACE$() Here's a direct replacement of the previous code: Pixel$ = SPACE$(bmpheader.wid) iHeight% = bmpheader.hei - 1 iWidth% = bmpheader.wid - 1 FOR y% = iHeight% TO 0 STEP -1 GET #1, , Pixel$ ' Reads an entire line at once FOR x% = 0 TO iWidth% PSET (x%, y%), ASC(MID$(Pixel$, x% + 1, 1)) NEXT x% NEXT y% CLOSE #1
I timed this new function against the old one and it was nearly 8 times the speed. Better eh? This should tell you about the speed of data transfer from disk =).

Now the second optimisation isn't as easy. What we do is use an Interrupt. If you don't know what this is, then search elsewhere on this site for my tutorial on it (plug, plug ;). The interrupt we'll use is the DOS interrupt (&H21) and Sub-Function 3Fh (Read from file or device). What we do is pass the interrupt a memory address (in this case, some location on the screen) and the number of bytes to read. I wont cover the code here, as it means a complete re-write. Check my tutorial on interrupts, I give an example of loading a BMP image there.

Questions, comments, requests? Mail me

Written for tutorials