1

First off, this is not a duplicate. I have already read Converting 1-bit bmp file to array in C/C++ and my question is about an inconsistency I'm seeing in the formulas provided with the one that works for me.

The Issue

I am trying to read in a 1-bit Bitmap image that was created in MS Paint. I've used the code provided by other answers on this site, but there are a few things I had to change to get it to work, and I want to understand why,

Change 1: lineSize must be doubled

Original

int lineSize = (w / 8 + (w / 8) % 4);

Mine:

int lineSize = (w/ 8 + (w / 8) % 4) * 2;

Change 2: Endianness must be reversed

Original:

for(k = 0 ; k < 8 ; k++)
    ... (data[fpos] >> k ) & 1;

Mine:

for (int k = 7; k >= 0; --k) {
    ... (data[rawPos] >> k) & 1;

Full Code

NOTE: This code works. There are some changes from the original, but the core read part is the same.

vector<vector<int>> getBlackAndWhiteBmp(string filename) {
    BmpHeader head;
    ifstream f(filename, ios::binary);

    if (!f) {
        throw "Invalid file given";
    }

    int headSize = sizeof(BmpHeader);
    f.read((char*)&head, headSize);

    if (head.bitsPerPixel != 1) {
        f.close();
        throw "Invalid bitmap loaded";
    }

    int height = head.height;
    int width = head.width;
    
    // Lines are aligned on a 4-byte boundary
    int lineSize = (width / 8 + (width / 8) % 4) * 2;
    int fileSize = lineSize * height;

    vector<unsigned char> rawFile(fileSize);
    vector<vector<int>> img(head.height, vector<int>(width, -1));

    // Skip to where the actual image data is
    f.seekg(head.offset);

    // Read in all of the file
    f.read((char*)&rawFile[0], fileSize);

    // Decode the actual boolean values of the pixesl
    int row;
    int reverseRow; // Because bitmaps are stored bottom to top for some reason
    int columnByte;
    int columnBit;

    for (row = 0, reverseRow = height - 1; row < height; ++row, --reverseRow) {
        columnBit = 0;
        for (columnByte = 0; columnByte < ceil((width / 8.0)); ++columnByte) {
            int rawPos = (row * lineSize) + columnByte;

            for (int k = 7; k >= 0 && columnBit < width; --k, ++columnBit) {
                img[reverseRow][columnBit] = (rawFile[rawPos] >> k) & 1;
            }
        }
    }

    f.close();
    return img;
}

#pragma pack(1)
struct BmpHeader {
    char magic[2];          // 0-1
    uint32_t fileSize;      // 2-5
    uint32_t reserved;      // 6-9
    uint32_t offset;        // 10-13
    uint32_t headerSize;    // 14-17
    uint32_t width;         // 18-21
    uint32_t height;        // 22-25
    uint16_t bitsPerPixel;  // 26-27
    uint16_t bitDepth;      // 28-29
};
#pragma pack()

Potentially relevant information:

  • I'm using Visual Studio 2017
  • I'm compiling for C++14
  • I'm on a Windows 10 OS

Thanks.

Community
  • 1
  • 1
David
  • 4,744
  • 5
  • 33
  • 64

1 Answers1

1

Both of those line size formulas are incorrect.

For example, for w = 1, (w / 8 + (w / 8) % 4) results in zero. It's still zero if you multiply by two. It's expected to be 4 for width = 1.

The correct formula for line size (or bytes per line) is

((w * bpp + 31) / 32) * 4 where bpp is bits per pixel, in this case it is 1.

By coincidence the values are sometimes the same, for some smaller width values.

See also MSDN example:

DWORD dwBmpSize = ((bmpScreen.bmWidth * bi.biBitCount + 31) / 32) * 4 * bmpScreen.bmHeight;

Also, 1-bit image has 2 palette entries, for a total of 8 bytes. It seems you are ignoring the palette and assuming that 0 is black, and 1 is white, always.

The part where you flip the bits is correct, the other code appears to be incorrect.

Lets say we have a single byte 1000 0000 This is mean to be a single row, starting with 7 zeros and ending in 1.

Your code is a bit confusing for me (but seems okay when you fix linesize). I wrote my own version:

void test(string filename)
{
    BmpHeader head;
    ifstream f(filename, ios::binary);
    if(!f.good())
        return;

    int headsize = sizeof(BmpHeader);
    f.read((char*)&head, headsize);

    if(head.bitsPerPixel != 1) 
    {
        f.close();
        throw "Invalid bitmap loaded";
    }

    int height = head.height;
    int width = head.width;

    int bpp = 1;
    int linesize = ((width * bpp + 31) / 32) * 4;
    int filesize = linesize * height;

    vector<unsigned char> data(filesize);

    //read color table
    uint32_t color0;
    uint32_t color1;
    uint32_t colortable[2];
    f.seekg(54);
    f.read((char*)&colortable[0], 4);
    f.read((char*)&colortable[1], 4);
    printf("colortable: 0x%06X 0x%06X\n", colortable[0], colortable[1]);

    f.seekg(head.offset);
    f.read((char*)&data[0], filesize);

    for(int y = height - 1; y >= 0; y--)
    {
        for(int x = 0; x < width; x++)
        {
            int pos = y * linesize + x / 8;
            int bit = 1 << (7 - x % 8);
            int v = (data[pos] & bit) > 0;
            printf("%d", v);
        }
        printf("\n");
    }

    f.close();
}


Test image:
enter image description here
(33 x 20 monochrome bitmap)
Output:
colortable: 0x000000 0xFFFFFF
000000000000000000000000000000000
000001111111111111111111111111110
000001111111111111111111111111110
000001111111111111111111111111110
000001111111111111111111111111110
011111111111111111111111111111110
011111111111111111111111111111110
011111111111111111111111111111110
011111111111111111111111111111110
011111111111111111111111111111110
011111111111111111111111111111110
011111111111111111111111111111110
011111111111111111111111111111110
011111111111111111111111111111110
011111111111111111111111111111110
011111111111111111111111111111110
011111111111111111111111111110010
011111111111111111111111111110010
011111111111111111111111111111110
000000000000000000000000000000000

Notice this line in above code:

int pos = y * linesize + x / 8;
int bit = 1 << (7 - x % 8);
int v = (data[pos] & bit) > 0;
printf("%d", v);

First I wrote it as

int bit = 1 << (x % 8);

But this shows the bits in the wrong order, so I had to change to 1 << (7 - x % 8) which is basically what you did also. I don't know why it's designed like that. There must be some historical reasons for it!

(above code is for little-endian machines only)

Barmak Shemirani
  • 30,904
  • 6
  • 40
  • 77
  • Ah, well, that would explain a lot. Strange so many people were using that original one. Could you explain the new version? It works, but I don't understand at the hex level what all those other bits are for that are skipped. Also, for the palette, does that just mean the image could be yellow and purple, instead of black and white? – David Mar 11 '18 at 03:53
  • If you have a bitmap of size 1 x 1, then the with should be 4 bytes, or 32-bits. That means 31 bits are extra padding and have to be skipped. Is that what you mean? The palette entry is usually black and white. I edited to show how to retrieve the colors. I don't know either why the bits have to be flipped, but your method in that regard is correct. – Barmak Shemirani Mar 11 '18 at 05:26