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
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:
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>
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%
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%
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.
- Intead of reading one byte at a time, read BmpHeader.wid bytes and load a line at a time
- 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))
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 qbasic.qb45.com tutorials