By Golrien <q@golrien.cjb.net>
Okay, most people already know about Windows BMPs. But I'm lazy, and they're easy to write about. And as a bonus, I'll explain RLE compression, which most BMP specifications leave out.
The BMP (short for Bitmap) image format is one of the simplest image formats in existance. It's also one of the least flexible, was devised by Microsoft, has several quirks and only the most basic of compression.
There are Windows BMPs and there are OS/2 BMPs. This stems from the time when M$ and IBM were working together on OS/2, but then M$ ran off with the source and made Windoze on their own, leaving IBM with the bills to pay, in true Bill Gates style. The OS/2 ones are possibly better (yay IBM) and I might cover them some other time. This tut only covers Windows-format BMPs.
Bitmap image files have a header. Most files have headers, and the best way to store the header info is in a TYPE definition. This is the BMP header, as a TYPE:
TYPE BMPheaderType ID AS STRING * 2 'Should be 'BM' for a windows BMP. FileSize AS LONG 'Size of the whole file. Reserved AS STRING * 4 ImageOffset AS LONG 'Offset of image data in file. InfoHeaderLength AS LONG 'The BitmapInfoHeader starts directly after ' this header. It could be: ' 12 bytes - OS/2 1.x format, or ' 40 bytes - Windows 3.x format, or ' 64 bytes - OS/2 2.x format. ImageWidth AS LONG 'Width and height of the image, in pixels. ImageHeight AS LONG ' - NumPlanes AS INTEGER 'Number of planes. BPP AS INTEGER 'Bits per pixel, the colour depth. Could be: ' 4 bit - 16 colours. ' 8 bit - 256 colours. ' 24 bit - A lot more colours. CompressionType AS LONG 'Type of compression. ' 0 - uncompressed, ' 1 - RLE 8-bit/pixel ' 2 - RLE 4-bit/pixel ImageSize AS LONG 'Size of image data in bytes. xRes AS LONG 'Horizontal and vertical resolution of the yRes AS LONG ' image. NumColsUsed AS LONG 'Number of used colours and number of NumColsImportant AS LONG ' important colours. END TYPE
Now all you have to do to get all the information is:
DIM SHARED BMPheader AS BMPheaderType OPEN BMPfile$ FOR BINARY AS #1 GET #1, , BMPheader
In a 24bpp BMP, the last two entries will be zero (they are in mine, anyway). In a 4- or 8-bit BMP, a palette follows the header. Each colour (there are 16 for a 4-bit and 256 for an 8-bit) has its own entry, with a byte for the red, green and blue.
Unfortunately, it's not that simple.
To start with, the colours are actually stored BGR, not RGB. Secondly, there's a byte of filler after every colour. This wastes 256 bytes, which is a quarter of a kilobyte. More attempts from M$ to waste disk space. Oh, and you'll have to divide the attributes by four to get them to how the VGA likes them. This, however, will load the palette.
'Reset the VGA palette ports. ' OUT &H3C8, 0 'Template strings, so QB knows how many bytes to get out of the file 'at a time. ' Red$ = " ": Green$ = " ": Blue$ = " " Byte$ = " " '2 ^ BMPheader.BPP is the number of colours used in the file. ' FOR i = 1 TO 2 ^ BMPheader.BPP 'Because the BMP stores the palette BGR, and the VGA takes it in 'as RGB, we need to get all the palette values for the colour and 'give them to the VGA one at a time, in reverse order (not 'forgetting the byte of filler): ' GET #1, , Blue$ GET #1, , Green$ GET #1, , Red$ GET #1, , Byte$ 'All the colour attributes must be divided by 4, as they go 0-255 'whereas the VGA card prefers 0-63. ' OUT &H3C9, ASC(Red$) \ 4 OUT &H3C9, ASC(Green$) \ 4 OUT &H3C9, ASC(Blue$) \ 4 NEXT i
That will set the palette up for viewing the BMP. Of course, if you're using SVGAQB or DirectQB or Future.Lib (?) or any other library that likes palettes in strings, you won't just be able to grab the whole string at once. Oh, no, thanks to Bill Gates' backwards nature, you have to do a whole load of screwing around first. This code will load the BMP palette into a string * 768 of RGB byte values, for use with DQBsetPal or whatever (only for 8-bit BMPs).
DIM SHARED Pal AS STRING * 768 'Reset the palette string. ' Pal = "" Red$ = " ": Green$ = " ": Blue$ = " ": Byte$ = " " FOR i = 1 TO 2 ^ BMPheader.BPP GET #1, , Blue$ GET #1, , Green$ GET #1, , Red$ GET #1, , Byte$ Pal = Pal + CHR$(ASC(Red$) \ 4) + CHR$(ASC(Green$) \ 4) + CHR$(ASC(Blue$) \ 4) NEXT i
However you've loaded the palette, the only thing remaining is to grab the image data. However, Micro$oft being Micro$oft, our troubles are not yet over. Whilst the conventional monitor prefers its scanlines to run from top to bottom, the BMP image is stored UPSIDE DOWN! The scanlines still run left to right, but the bottom line is first in the file and the top last. Fortunately, it is easier to get over than the palette difficulties, because it is possible to draw the scanlines in any order. So, for an 8-bit BMP:
Byte$ = " " FOR y = BMPheader.ImageHeight - 1 TO 0 STEP -1 FOR x = 0 TO BMPheader.ImageWidth - 1 GET #1, , Byte$ PSET (x, y), ASC(Byte$) NEXT NEXT
Four-bit BMPs are more difficult, as they have two pixels per byte...
Byte$ = " " FOR y = BMPheader.ImageHeight - 1 TO 0 STEP -1 FOR x = 0 TO BMPheader.ImageWidth - 1 STEP 2 GET #1, , Byte$ LowNibble = ASC(Byte$) \ 16: HighNibble = ASC(Byte$) AND 15 PSET (x, y), LowNibble: PSET (x + 1, y), HighNibble NEXT NEXT
However, the hardest to draw are 24-bit BMPs. Of course, in a hi-col mode it would be simple (simpler than an 8-bit BMP), but QB can't do that, Future.lib has its own BMP functions and this is a BMP tut, not a VESA tut. There are, however, a few ways to load 24-bit BMPs into SCREEN 13. For those that don't know, hi-col screenmodes have no palette, each pixel stores its own colour attributes, so to PSET you have to use an RGB value. 24-bit bmps are stored like that, with three bytes per pixel, one per colour.
The first trick we can play is to greyscale the image. This is easy, just set up a palette of 256 grey shades and add together the RGB values and average them to get a grey value. Then we put that as the colour. That was probably a crap explanation, so here's the code to do it.
'Grey out the palette. ' OUT &H3C8, 0 FOR i = 0 TO 255 OUT &H3C9, i \ 4 OUT &H3C9, i \ 4 OUT &H3C9, i \ 4 NEXT i Red$ = " ": Green$ = " ": Blue$ = " " FOR y = BMPheader.ImageHeight - 1 TO 0 STEP -1 FOR x = 0 TO BMPheader.ImageWidth - 1 GET #1, , Blue$ GET #1, , Green$ GET #1, , Red$ PSET (x, y), (ASC(Red$) + ASC(Green$) + ASC(Blue$)) \ 3 NEXT NEXT
That actually worked the first time I coded it and I never even misspelt a command which made me slightly happier than I usually am. However, I'm still not happy enough to teach you about reducing the colour depth from 24-bit to 8-bit.
It is possible to compress BMPs. Only some of them, the 24-bit ones cannot be compressed and the algorithm for 4- and 8-bit compression sucks pretty much. But it is useful to know how.
The BMP can be compressed in two modes, absolute mode and RLE mode. Both modes can occur anywhere in a single bitmap.
The RLE mode is a very simple, the first byte contains the count and the second the pixel to be replicated (if this makes no sense, don't worry, just cut and paste the code and pretend you wrote it yourself (no, don't do that)). If the count byte is zero the second byte is a special byte.
In absolute mode, the second byte contains the number of bytes to be copied exactly. Each absolute run is word-aligned, which means it may be padded with an extra byte to make the numbers round. After an absolute run, RLE compression continues.
The second bytes after a zero count can be: 0 - end of line. 1 - end of bitmap. 2 - delta - move to a new X and Y position. 3+ - switch to absolute mode.
'RLE-8 compression. Yay. RLE images also have the bottom line 'first, just to make things *really* wierd. ' xPos = 0 yPos = BMPheader.ImageHeight - 1 DO GET #1, , Byte$: ByteCount = ASC(Byte$) IF ByteCount = 0 THEN 'Special code. ' GET #1, , Byte$: Code = ASC(Byte$) IF Code = 0 THEN 'End of line. ' xPos = 0: yPos = yPos - 1 ELSEIF Code = 1 THEN 'End of image. ' EXIT DO ELSEIF Code = 2 THEN 'Delta. ' GET #1, , Byte$ xPos = xPos + ASC(Byte$) yPos = yPos - ASC(Byte$) ELSE 'Absolute mode. ' FOR i = 1 TO Code GET #1, , Byte$ PSET (xPos, yPos), ASC(Byte$) xPos = xPos + 1 NEXT i 'Remember that the bytes must be word-aligned. ' IF Code MOD 2 <> 0 THEN GET #1, , Byte$ END IF ELSE 'Just plain vanilla RLE encoding here. ' GET #1, , Byte$: PixelColour = ASC(Byte$) FOR i = 1 TO ByteCount PSET (xPos, yPos), PixelColour xPos = xPos + 1 NEXT i END IF LOOP
I don't promise *anything* about the delta code, seeing as I have no images that have one and Paint Shop Pro 7 seems to be allergic to them. However, this is the code used in the Allegro library, so I bet it's right.
Four-bit RLE is pretty much the same, except each byte contains two pixels:
'RLE-4 compression. Yay. ' xPos = 0 yPos = BMPheader.ImageHeight - 1 Byte$ = " " DO GET #1, , Byte$: ByteCount = ASC(Byte$) IF ByteCount = 0 THEN GET #1, , Byte$: Code = ASC(Byte$) IF Code = 0 THEN xPos = 0: yPos = yPos - 1 ELSEIF Code = 1 THEN EXIT DO ELSEIF Code = 2 THEN GET #1, , Byte$: xPos = xPos + ASC(Byte$) GET #1, , Byte$: yPos = yPos - ASC(Byte$) ELSE FOR i = 1 TO Code IF i MOD 2 <> 0 THEN GET #1, , Byte$ LowNibble = ASC(Byte$) AND 15 HighNibble = (ASC(Byte$) \ 16) AND 15 PSET (xPos, yPos), HighNibble ELSE PSET (xPos, yPos), LowNibble END IF xPos = xPos + 1 NEXT i IF Code MOD 4 <> 0 THEN GET #1, , Byte$ END IF ELSE GET #1, , Byte$ LowNibble = ASC(Byte$) AND 15 HighNibble = (ASC(Byte$) \ 16) AND 15 FOR i = 1 TO ByteCount IF i MOD 2 <> 0 THEN PSET (xPos, yPos), HighNibble ELSE PSET (xPos, yPos), LowNibble END IF xPos = xPos + 1 NEXT i END IF LOOP
This code took about four days before I got it working, and then I managed to fix it, at 12:20pm one night. That was about ten minutes after I'd finished condensing a 50-line fire effect into a 15-line fire effect for a competition (look out for Minifire, kids =).
Well, that's all there is to it. Hopefully you can do something with this stuff, and I bet this is the only *complete* tutorial with QB code samples. If you didn't understand any of it, have a look at the example program, BMPTUT.BAS, which should be included somewhere, along with some BMPs. Try adding some watches and stepping through the code or something, it might be vaguely interesting.
I might do another of these tutorials. If I do, it'll probably be on RIFF wave files (WAVs), how to play them and stuff.