QB CULT MAGAZINE
Vol. 2 Iss. 3 - August 2001

BMP's explained

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.

golrien.cjb.net