Andrew Ellis
Andrew Ellis

Reputation: 139

Finding the location of colour table in 8 bit BMP

I appreciate that this is probably really simple, but after spending a lot of time researching I'm struggling to come up with a definitive answer.

I'm writing some code to open and display a BMP on an embedded system with a TFT display. I have no problems displaying 16, 24, and 32 bit BMPs, but I'm struggling with 8 bit BMPs. The problem I have is that I'm not sure exactly where the colour table begins in a bmp file. Looking at the Wikipedia article

the colour table should be located 50 bytes into the file (as mine has a 40 byte dib header) + 12 bytes ie a total of 62 bytes in. When I try and index a table read from the area, I see an image which resembles the bmp, but the colours are completely wrong.

How exactly do I determin the correct position for the colour table?

When I look at location 0x1C in the file, the value is 8 indicating 8bpp, but when I look at 0x2E the value is 0, shouldn't that be 256?

My code for reading and displaying the bitmap is shown below:

typedef struct _BGRA {
unsigned char Blue;
unsigned char Green;
unsigned char Red;
unsigned char Alpha;
} BGRA;

int CBitmapViewerDlg::display_bmp(unsigned char *bmp_data, unsigned short left, 

unsigned short top, CDC *dc)
{
    unsigned short temp = 0;
    unsigned short width = 480, height = 272;
    unsigned short x = 0,  y = 0, idx = 0;
    unsigned char byte_width = 0;
    unsigned short data_offset = 0;
    unsigned short u16bpp = 8;


    width = bmp_data[0x12];
    width |= (bmp_data[0x13] << 8);
    width |= (bmp_data[0x14] << 16);
    width |= (bmp_data[0x15] << 24);

    height = bmp_data[0x16];
    height |= bmp_data[0x17] << 8;
    height |= bmp_data[0x18] << 16;
    height |= bmp_data[0x19] << 24;

    data_offset = bmp_data[0x0A];
    data_offset |= bmp_data[0x0B] << 8;
    data_offset |= bmp_data[0x0C] << 16;
    data_offset |= bmp_data[0x0D] << 24;

    BGRA *colour_table_offset = (BGRA *)bmp_data + 0x3E;// [0x3E];

    u16bpp = bmp_data[0x1C];
    u16bpp |= bmp_data[0x1D];

    bmp_data += data_offset;

    if (u16bpp == 8)
    {
        unsigned char r, g, b;
        for (y=height; y>0; y--)
        {           
            for(x=0;x<width;x++)
            {
                b = colour_table_offset[*bmp_data].Blue;
                g = colour_table_offset[*bmp_data].Green;
                r = colour_table_offset[*bmp_data].Red;             
                dc->SetPixel(x, y, RGB(r, g, b));                               
                bmp_data++;
            }           

        }
    }
}

The modified function for displaying bitmaps below:

int CBitmapViewerDlg::display_bmp(char *bmp_data, unsigned short left, unsigned short top, CDC *dc)

{ unsigned short x = 0, y = 0;

mBitmapHeader *bmHdr;
bmHdr = (mBitmapHeader*)bmp_data;

BGRA *colour_table_offset;
colour_table_offset = (BGRA*)bmp_data + (sizeof(mBitmapFileHdr_t) + sizeof(mBitmapInfoHdr_t));

int bytesPerRow = bmHdr->info.biWidth + (bmHdr->info.biWidth%2);    // rows must be a multiple of 2 bytes

if (bmHdr->info.biBitCount == 8)
{
    for (y=0; y<bmHdr->info.biHeight; y++)
    //for (y=bmHdr->info.biHeight; y>0; y--)
    {           
        uint8_t *curRow;
        curRow = (uint8_t*) (bmHdr->hdr.bfOffsetBits + (y*bytesPerRow) + bmp_data);
        for(x=0;x<bmHdr->info.biWidth;x++)
        {
            uint32_t palIndex;
            palIndex = curRow[x];
            BGRA curCol = colour_table_offset[palIndex];

            uint8_t r, g, b;
            b = (curCol>>0) & 0xFF;
            g = (curCol>>8) & 0xFF;
            r = (curCol>>16) & 0xFF;
            dc->SetPixel(x, y, RGB(r,g,b) );
        }                       
    }
}

return 1;

}

Andrew

Upvotes: 2

Views: 4365

Answers (2)

enhzflep
enhzflep

Reputation: 13109

To be fair, the documentation of the BMP format can be more than a little confusing. The various additions/customizations over the years certainly haven't helped the situation any, imho.

The short answer is that the offset of the colour table or palette is 54. Assuming an image saved with Gimp under Windows - this has held true for each of the 4, 8 and 24 bit images I'e played with today.

A much easier way to access the various fields of the file header is to make use of structs. Just be aware that the compiler may try to pad the structs so that the hardware has an easier time trying to load any given field. You can prevent this with compiler directives. In my case I'm using g++, and the smallest element is 2 bytes, so I use the compiler directive pair: (before structs)#pragma pack(push,2) and (after structs)#pragma pop. Other compilers may differ, I cant remember. Just about anyone can write a better explanation of structure padding than me, I encourage you to seek one out.

"When I look at location 0x1C in the file, the value is 8 indicating 8bpp, but when I look at 0x2E (46d) the value is 0, shouldn't that be 256?"

Well, if you take a look at a format spec for BMPs e.g http://en.wikipedia.org/wiki/BMP_file_format (See the table for Windows BITMAPINFOHEADER) and look at the description for this field, you can see that for images with a colour table, it is either (a) the number of colours or (b) 0, in which case one assumes 2^bitsPerPixel. Thus, for an 8bit image, if this value is 0 then you know you have 2^8 = 256 colours - the value you were expecting. No idea why that decision was made, you just need to add the extra little bit of logic to get/compute the number of palette values to read/process.

Consider the following code:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

/*
 2 x 2 bitmap (2x2.bmp)
 4bit indexed color - leftmost pixel in hi-nibble
 red, green
 blue, white
 78 bytes

42 4D - 4E 00 00 00 - 00 00 - 00 00 - 46 00 00 00 - 28 00
00 00 - 02 00 00 00 - 02 00 00 00 - 01 00 - 04 00 - 00 00
00 00 - 08 00 00 00 - 13 0B 00 00 - 13 0B 00 00 - 04 00
00 00 - 04 00 00 00 - FF 00 00 00 - 00 00 FF 00 - 00 FF
00 00 - FF FF FF 00 - 03 00 00 00 - 12 00 00 00
*/

/*
//mBitmapFileHdr_t
42 4D           'BM'
4E 00 00 00     78
00 00            0
00 00            0
46 00 00 00     70
//mBitmapInfoHdr_t
28 00 00 00     40
02 00 00 00      2
02 00 00 00      2
01 00            1
04 00            4
00 00 00 00      0
08 00 00 00      8
13 0B 00 00   2835
13 0B 00 00   2835
04 00 00 00 - cols in table
04 00 00 00 - important cols in table
// Colour-map data
FF 00 00 00     - 0 blue
00 00 FF 00     - 1 red
00 FF 00 00     - 2 green
FF FF FF 00     - 3 white
// Pixel data - bottom -> top in this image
03 00 00 00 - blue, white
12 00 00 00 - red, green
*/
#pragma pack(push,2)
typedef struct mBitmapFileHdr_t
{
    uint16_t bfType;
    uint32_t bfSize;
    uint16_t bfReserved1;
    uint16_t bfReserved2;
    uint32_t bfOffsetBits;
} ;

typedef struct mBitmapInfoHdr_t
{
    uint32_t biSize;
    uint32_t biWidth;
    uint32_t biHeight;
    uint16_t biPlanes;
    uint16_t biBitCount;
    uint32_t biCompression;
    uint32_t biSizeImage;
    uint32_t biXPelsPerMeter;
    uint32_t biYPelsPerMeter;
    uint32_t biClrUsed;
    uint32_t biClrImportant;
};

typedef struct mBitmapHeader
{
    mBitmapFileHdr_t hdr;
    mBitmapInfoHdr_t info;
};
#pragma pop

void dispBmpInfo(char *filename)
{
    FILE *fp;
    long fileSize;
    char *rawData;

    fp = fopen(filename, "rb");
    fseek(fp, 0, SEEK_END);
    fileSize = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    rawData = (char*)malloc( fileSize * sizeof(char) );
    fread(rawData, sizeof(char), fileSize, fp);

    mBitmapHeader *bmHdr;
    bmHdr = (mBitmapHeader*)rawData;

    printf("Width: %d\n", bmHdr->info.biWidth);
    printf("Height: %d\n", bmHdr->info.biHeight);
    printf("Planes: %d\n", bmHdr->info.biPlanes);
    printf("Bits/Pixel: %d\n", bmHdr->info.biBitCount);
    printf("Colors used: %d\n", bmHdr->info.biClrUsed);
    printf("Important Colors: %d\n", bmHdr->info.biClrImportant);
    printf("Offset of pallete: %d\n", sizeof(mBitmapFileHdr_t) + sizeof(mBitmapInfoHdr_t));
    printf("Offset of colour data: %d\n", bmHdr->hdr.bfOffsetBits);

    free(rawData);
    fclose(fp);
}

int main()
{
    dispBmpInfo("2x2.bmp");
    return 0;
}

Output when applied to 2x2.bmp shown in the source

Width: 2
Height: 2
Planes: 1
Bits/Pixel: 4
Colors used: 4
Important Colors: 4
Offset of pallete: 54
Offset of colour data: 70

Output when applied to 24bit bmp

Width: 243
Height: 61
Planes: 1
Bits/Pixel: 24
Colors used: 0
Important Colors: 0
Offset of pallete: 54
Offset of colour data: 54

Output when applied to 8bit version of above bmp

Width: 243
Height: 61
Planes: 1
Bits/Pixel: 8
Colors used: 256
Important Colors: 256
Offset of pallete: 54
Offset of colour data: 1078

EDIT: Code to display a bitmap added.

typedef uint32_t BGRA;

void dispBmp(char *filename, HDC hdc)
{
    FILE *fp;
    long fileSize;
    char *rawData;

    fp = fopen(filename, "rb");
    fseek(fp, 0, SEEK_END);
    fileSize = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    rawData = (char*)malloc( fileSize * sizeof(char) );
    fread(rawData, sizeof(char), fileSize, fp);

    mBitmapHeader *bmHdr;
    bmHdr = (mBitmapHeader*)rawData;

    BGRA *colour_table_offset;
    colour_table_offset = (BGRA*) (rawData + (sizeof(mBitmapFileHdr_t) + sizeof(mBitmapInfoHdr_t)));

    int bytesPerRow = bmHdr->info.biWidth + (bmHdr->info.biWidth%2);    // rows must be a multiple of 2 bytes

    int y, x;
    for (y=0; y<bmHdr->info.biHeight; y++)
    {
        uint8_t *curRow;
        curRow = (uint8_t*) (bmHdr->hdr.bfOffsetBits + (y*bytesPerRow) + rawData);
        for (x=0; x<bmHdr->info.biWidth; x++)
        {
            uint32_t palIndex;
            palIndex = curRow[x];
            BGRA curCol = colour_table_offset[palIndex];
            SetPixel(hdc, x, y, curCol);
        }
    }
    free(rawData);
    fclose(fp);
}

Here, I'm just passing off the RGB values however I receive them. You can clearly see that the wrong result is gained. This is a matter of reversing the order of the channels in your bgra struct, as I mentioned in a comment. The fact that the image is upside down is another matter entirely, I'll leave you to look into that one. :p

'Expected' result: (I knew it would be upside down and have the channels mixed up) enter image description here

Actual result:

enter image description here

If we then change the code of the inner-loop to swap the channels, we get the expected result (one that is still upside down - I haven't bothered to consider top-down or bottom-up bitmaps for the sake of the example)

            uint32_t palIndex;
            palIndex = curRow[x];
            BGRA curCol = colour_table_offset[palIndex];
//            SetPixel(hdc, x, y, curCol);

            uint8_t r, g, b;
            b = (curCol>>0) & 0xFF;
            g = (curCol>>8) & 0xFF;
            r = (curCol>>16) & 0xFF;
            SetPixel(hdc, x, y, RGB(r,g,b) );

enter image description here

Upvotes: 1

Mark Ransom
Mark Ransom

Reputation: 308452

The bitmap file header is always 14 bytes. Other than verifying the format to make sure you have a valid BMP file, you can safely skip over it.

The bitmap info header immediately follows and can have a variable size, although by far the most common will be 40 bytes. You can determine the size with the 4 bytes starting at offset 14:

uint32_t header_size = (bmp_data[14] & 0xff) | ((bmp_data[15] & 0xff) << 8) | ((bmp_data[16] & 0xff) << 16) | ((bmp_data[17] & 0xff) << 24);

This brings up another point, you're using unsigned short for 32-bit quantities. That's wrong.

The offset of the palette will be immediately after the bitmap info header. There is also the possibility of having 3 4-byte masks before the color table if the compression type is BI_BITFIELDS, but I've never seen that compression type used. You could easily get away with refusing to read the file if the compression type isn't BI_RGB.

BGRA *colour_table_offset = (BGRA *)(bmp_data + 14 + header_size);

Upvotes: 4

Related Questions