1

I've written myself a little C++ program to write a bitmap file from scratch. It works partially and I can't seem to find out why it doesn't work in certain cases. What works:

  • Setting a constant color
  • Setting a variable color using the loop index WHEN AT THE SAME TIME specifying an offset larger than 10.

What does not work:

  • Setting a variable color without offset or an offset smaller or equal to 10

What's (probably) not the problem:

  • image size is 1024 x 1024 -> no padding bytes required !
  • The bitmap file is recognized by the windows "photo viewer". I also tried paint and gimp: the format is recognised and I get the same images.

For the whole code see below, here are the parts I deem important:

#pragma pack(push, 1) // no padding !
struct bmpHeader {
    uint16_t hdrField; // set to 'B'+'M'
    uint32_t fileSize;
    uint16_t res1;
    uint16_t res2;
    uint32_t imgDataOffset;
};

struct bmpCoreHeader {
    uint32_t hdrSize;
    uint16_t width;
    uint16_t height;
    uint16_t colorPlanes; // 1
    uint16_t bitsPerPixel; // 24
};

struct pixel {
    uint8_t blue;
    uint8_t green;
    uint8_t red;
};

And the loop used for filling:

for (uint32_t i = 0; i < cHdr.width; i++)
    {
        for (uint32_t j = 0; j < cHdr.height; j++)
        {
            imageData->at(i).at(j).blue = 0;//j % 256;
            imageData->at(i).at(j).red = i % 128 + 0;
            imageData->at(i).at(j).green = 0;
        }
        //std::cout << i << ": ";
        //std::cout << +imageData->at(i).at(0).blue << std::endl;
    }

I expect the above loop to produce an image that creates black-to-red fades. What it actually creates: Produced bitmap (cropped image to half height, full width)

You can find the following artifacts on the boundary between the colors: enter image description here

HOWEVER, when I set an offset like the following: imageData->at(i).at(j).red = i % 128 + 11; I get: Produced bitmap with offset 11 Any offset below 11 produces the same faulty picture. I have no idea of what's going on here.

Here is the full code:

#include <iostream>
#include <fstream>
#include <cstring>
#include <cstdint>
#include <array>

constexpr uint16_t width = 1024;
constexpr uint16_t height = 1024;

#pragma pack(push, 1)
struct bmpHeader {
    uint16_t hdrField;
    uint32_t fileSize;
    uint16_t res1;
    uint16_t res2;
    uint32_t imgDataOffset;
};

struct bmpCoreHeader {
    uint32_t hdrSize;
    uint16_t width;
    uint16_t height;
    uint16_t colorPlanes;
    uint16_t bitsPerPixel;
};

struct pixel {
    uint8_t blue;
    uint8_t green;
    uint8_t red;
    //uint8_t padding;
};

#pragma pack(pop)

int main()
{
    bmpHeader hdr;
    hdr.hdrField = 0x4d42;
    // size is set at the end
    hdr.fileSize = 0;
    hdr.res1 = 0;
    hdr.res2 = 0;
    hdr.imgDataOffset = sizeof(bmpHeader) + sizeof(bmpCoreHeader);

    bmpCoreHeader cHdr = { 0 };
    cHdr.hdrSize = sizeof(cHdr);
    cHdr.width = width;
    cHdr.height = height;
    cHdr.colorPlanes = 1;
    cHdr.bitsPerPixel = 24;

    std::array<std::array<pixel, width>, height>* imageData = new std::array<std::array<pixel, width>, height>;

    hdr.fileSize = sizeof(bmpHeader) + sizeof(bmpCoreHeader) + sizeof(*imageData);

    for (uint32_t i = 0; i < cHdr.width; i++)
    {
        for (uint32_t j = 0; j < cHdr.height; j++)
        {
            imageData->at(i).at(j).blue = 0;//j % 256;
            imageData->at(i).at(j).red = i % 128 + 9;
            imageData->at(i).at(j).green = 0;
        }
        //std::cout << i << ": ";
        //std::cout << +imageData->at(i).at(0).blue << std::endl;
    }

    char* bmpContent = new char[sizeof(bmpHeader) + sizeof(bmpCoreHeader) + sizeof(*imageData)];
    std::cout << "Image data size: " << sizeof(*imageData) << std::endl;

    memcpy(bmpContent, &hdr, sizeof(bmpHeader));
    memcpy(bmpContent + sizeof(bmpHeader), &cHdr, sizeof(bmpCoreHeader));
    memcpy(bmpContent + sizeof(bmpHeader) + sizeof(bmpCoreHeader), imageData, sizeof(*imageData));
    
    std::fstream bmpFile("image.bmp", std::fstream::out);
    if (!bmpFile.is_open())
    {
        std::cout << "Could not open / create BMP file" << std::endl;
        return 1;
    }
    bmpFile.write(bmpContent, hdr.fileSize);

    return 0;
}

Any input is greatly appreciated =)

MAPster
  • 68
  • 6
  • The system is windows 10, visual studio 2019 and the corresponding compiler. I did not check on a different operating system. One might have to adapt the "#pragma pack(push, 1)" for a different OS. – MAPster Oct 05 '21 at 20:34
  • What graphics system are you using? – Thomas Matthews Oct 05 '21 at 20:40
  • it's curious that it still recognizes it, probably because there is code in those apps for legacy DIB support. That DIB header is legacy OS/2 1.x (Windows 2.0) one. – Swift - Friday Pie Oct 05 '21 at 20:56
  • @ThomasMatthews What do you mean by graphics system ? I just write a file. I've checked the bitmap in paint, gimp and windows "photo viewer" – MAPster Oct 05 '21 at 21:08
  • @Swift-FridayPie Yes actually I wanted to play a bit with perlin noise and such stuff, ultimately for textures in OGL. Though I thought it would be simplest to go for uncompressed bitmap first because I can just open it. So I chose the simplest header I found on wikipedia: https://en.wikipedia.org/wiki/BMP_file_format Do you think the artifacts could come from the "too old" header ? – MAPster Oct 05 '21 at 21:08
  • Hm. Uncommenting ```pixel```'s padding attribute and setting the bit depth to 32 works too. There's probably some weird offset or missing padding somewhere. – TARN4T1ON Oct 05 '21 at 21:16
  • @MaPster it was long time I "played" with that directly and after some struggles with BMP which had like 8 different formats and some inherent limitations in code working on them I had settled on using a .tga file instead. ALbeit writing .tga files might be a little more tricky than just reading and processing resulting uncompressed array. today there are many libraries that do it for you, especially when OGL got extensions for compressed textures. – Swift - Friday Pie Oct 05 '21 at 21:16
  • one thing that is wrong with this code is that rows in the image data have to be padded to be multiples of four bytes so you can't write a 2D array of three byte pixels directly to memory and have it work for arbitrary image widths. hwoever that cant be the problem here because 1024 * 3 is a multiple of 4, no per-row padding is needed. I mean since you are literally only filling red pixels there has to be some kind of offset problem somewhere though. – jwezorek Oct 05 '21 at 21:22
  • I thought you were displaying the bitmaps on a screen or GUI. – Thomas Matthews Oct 05 '21 at 21:24
  • @TARN4T1ON Interesting, I didn't have the idea with setting the depth to 32. But you were right, it was a padding problem after all. Though I still don't fully understand why using certain offsets seemed to work. Well, see the answer... – MAPster Oct 05 '21 at 21:36
  • @TARN4T1ON it might be a) app's idiosyncrasy toward 24 bpp uncompressed, b) something with API. I wonder if that changes if you change screen format to 24 bpp. – Swift - Friday Pie Oct 05 '21 at 21:38
  • yeah if you use 32 bpp then per-row padding is not an issue – jwezorek Oct 05 '21 at 21:39
  • @jwezorek That's per-component issue is appearing here. except fact that 2.0 BMP should not support 32 bpp.. Or alpha channel. That's undocumented BITMAPV3INFOHEADER – Swift - Friday Pie Oct 05 '21 at 21:41

1 Answers1

4

The file needs to be opened as binary, otherwise any 0A is replaced with 0D0A which will mess up alignment

    std::ofstream bmpFile("image.bmp", std::ios_base::binary);

(ref https://learn.microsoft.com/en-us/cpp/standard-library/binary-output-files)

dirck
  • 838
  • 5
  • 10
  • Wow. You really don't want to know how many hours I was sitting over this problem... Thanks a lot, omg. Immediately worked. – MAPster Oct 05 '21 at 21:34
  • Oh NOW I get it. the fstream believes this is a line feed so it prepends a carriage return. I call that high level trolling, lol. Though I get the point. – MAPster Oct 05 '21 at 21:40
  • I've seen this kind of problem before, but it didn't occur to me until I looked: I set the width and height to 16 and then dumped the output file. When in doubt, look at the data. – dirck Oct 05 '21 at 21:44
  • lol... damn that was a good bug. – jwezorek Oct 05 '21 at 21:49
  • wow... explains why it was "kinda" working without a bug using gcc... it was using linux line ending. – Swift - Friday Pie Oct 07 '21 at 09:28
  • This problem comes up a lot when trying to do binary pipes < in or > out on Windows. [Here](https://stackoverflow.com/questions/23107609/is-there-way-to-set-stdout-to-binary-mode) is a relevant post. – dirck Oct 07 '21 at 14:46