Problems writing a BMP exporter

Started by
7 comments, last by MarkS_ 8 years, 6 months ago

I am having some troubles when generating a .bmp file, from the code below i expected it to come out in shades of red, but as you'll see from the included picture it becomes both blue and green instead. Any ideas why this is happening?


struct Pixel
{
   unsigned char r; 
   unsigned char g;
   unsigned char b;
};

The main function generates the vector that holds all the pixel data, the color depends on the height of the image.


int main()
{
   std::vector<Pixel> ImageData;
   unsigned int Width = 640;
   unsigned int Height = 480;
 
   for (unsigned int i = 0; i < Height; i++)
   {
      for (unsigned int j = 0; j < Width; j++)
      {
         Pixel temp;
         temp.r = i % 255;
         temp.g = 0;
         temp.b = 0;
         ImageData.push_back(temp);
      }
   }
 
   WriteBitMap("raytracer.bmp", Width, Height, ImageData);
 
   return 0;
}
Writes the 2 headers needed for BMP files and then the image data.

bool WriteBitMap(std::string Filename, const unsigned int Width, const unsigned int Height, std::vector<Pixel> ImageData)
{
   //Create fileformat headers
 
   BitmapFileHeader fh;
   BitmapInfoHeader ih;
 
   fh.bmtype[0] = 'B';
   fh.bmtype[1] = 'M';
   fh.iFileSize = sizeof(BitmapFileHeader) + sizeof(BitmapInfoHeader) + (Width*Height * 3);
   fh.iOffsetBits = sizeof(BitmapFileHeader) + sizeof(BitmapInfoHeader);
 
   ih.iSizeHeader = 40;
   ih.iWidth = Width;
   ih.iHeight = Height;
   ih.iPlanes = 1;
   ih.iBitCount = 24;
   ih.Compression = 0;
 
   //Open file for writing
 
   std::ofstream file;
   file.open(Filename, std::ios::out);
 
   file.write(reinterpret_cast<char*>(&fh), sizeof(BitmapFileHeader));
   file.write(reinterpret_cast<char*>(&ih), sizeof(BitmapInfoHeader));
 
   for (auto it = ImageData.begin(); it != ImageData.end(); ++it)
   {
      char red = it->r;
      char green = 0;
      char blue = 0;
      file.write(&blue, sizeof(char));
      file.write(&green, sizeof(char));
      file.write(&red, sizeof(char));
   }
 
   file.close();
   return false;
}

Edit:

See solution further down smile.png

File needed to be opened in binary mode.

Advertisement

First:

Bitmaps are stored BGR and not RGB.

Second:

Bitmaps are 4 byte aligned. This means that each horizontal line comes out to a multiple of 4 bytes.

Typically this is called "scanline size". This means that ( y * w + x ) *3 doesn't work.

You will have to find the scanline size by finding the smallest multiple of 4 larger than the width in bytes.

Like for( int scansize = 0; scansize < w_bytes; scansize += 4); will work (but not the best way).

You will need to allocate a buffer of (height * scanline_size) bytes and find the offset for each pixel by ( y * scaline_size + x * bytes_per_pixel).

In other words a generic 2d array wont work unless you can be sure that the scanline size is the same as (width * bytes per pixel).

Oh.... also bitmaps are stored upside down. This means that the first pixel in the file is actually the first pixel on the last scanline.

To add on on the upside down:

If you store height as negative the bitmap data is top down instead.

Also, for iFileSize you must use the size calculated with scanline size as Gl2eenDl2agon described.

Fruny: Ftagn! Ia! Ia! std::time_put_byname! Mglui naflftagn std::codecvt eY'ha-nthlei!,char,mbstate_t>

Thank you for your answers. =)

First:

Bitmaps are stored BGR and not RGB.

I do store them that way:


for (auto it = ImageData.begin(); it != ImageData.end(); ++it)
{
   char red = it->r;
   char green = 0;
   char blue = 0;
   file.write(&blue, sizeof(char));
   file.write(&green, sizeof(char));
   file.write(&red, sizeof(char));
}

Second:

Bitmaps are 4 byte aligned. This means that each horizontal line comes out to a multiple of 4 bytes.

Typically this is called "scanline size". This means that ( y * w + x ) *3 doesn't work.

You will have to find the scanline size by finding the smallest multiple of 4 larger than the width in bytes.

Like for( int scansize = 0; scansize < w_bytes; scansize += 4); will work (but not the best way).

You will need to allocate a buffer of (height * scanline_size) bytes and find the offset for each pixel by ( y * scaline_size + x * bytes_per_pixel).

In other words a generic 2d array wont work unless you can be sure that the scanline size is the same as (width * bytes per pixel).

I thought using a width of 640 would be aligned since:


640 pixels * 3 chars = 1920bytes
 
1920 bytes % 4 = 0

Isnt this correct? ohmy.png

Edit: If I change to constant values it works, and generates a red picture as expected

   temp.r = i % 255;

to

   temp.r = 255;

A small thing:


//temp.r = i % 255;
temp.r = i % 256;

Sorry I don't have the time to have a good look at your code, but I wrote a "gdi memory bitmap" class a while ago, and it has a method which saves it to disk. You can find the code in this blog post, and below is the relevant method. I'm sorry for just posting code without useful answers, but I hope it helps.



/////////////////////////////////////////////////////////////////
// Desc: Save bitmap represented by the specified BITMAPINFO struct to a file.
BOOL CMemoryBitmap::SaveBMPToFile( const PBITMAPINFO pbmi, LPCTSTR sFileName, VOID* pPixels )
{
    FILE* pFile = _tfopen( sFileName, TEXT("wb") );
    if( pFile == NULL )
        return FALSE;

    INT nWidth = pbmi->bmiHeader.biWidth;
    INT nHeight = pbmi->bmiHeader.biHeight;
    if( nHeight < 0 )
        nHeight = -nHeight;

    INT rowSizeUnaligned = pbmi->bmiHeader.biBitCount/8 * pbmi->bmiHeader.biWidth;
    INT rowSize = DWordAlign( rowSizeUnaligned );

    BITMAPFILEHEADER bmfh;
    bmfh.bfType = 0x4d42;    // 0x42 = "B" 0x4d = "M", says MSDN.

    // compute size of bitmap file.
    bmfh.bfSize =    sizeof( BITMAPFILEHEADER ) +
                    sizeof( BITMAPINFO ) +
                    rowSize;
    bmfh.bfReserved1 = 0;
    bmfh.bfReserved2 = 0;
    bmfh.bfOffBits = sizeof( BITMAPFILEHEADER ) + sizeof( BITMAPINFO );

    if( 0 == fwrite( (VOID*)&bmfh, sizeof( BITMAPFILEHEADER ), 1, pFile ) )
    {
        //g_Log( "Failed to write file info header!" );
        fclose( pFile );
        return FALSE;
    }


    // NOTE: Some programs have problems loading bitmaps whose height is negative.
    // Therefore, this function will save the height as positive, causing the output
    // file to be vertically flipped.
    LONG oldHeight = pbmi->bmiHeader.biHeight;
    if( oldHeight < 0 )
        pbmi->bmiHeader.biHeight = -oldHeight;

    if( 0 == fwrite( (VOID*)pbmi, sizeof( BITMAPINFO ), 1, pFile ) )

    pbmi->bmiHeader.biHeight = oldHeight;



    // Now write the pixels:
    INT paddingBytes = rowSize - rowSizeUnaligned;
    DWORD dwZero = 0;

    BYTE* pBytes = (BYTE*)pPixels;
    for( INT y=0; y<nHeight; y++ )
    {
        fwrite( pBytes, rowSize - paddingBytes, 1, pFile );
        if( paddingBytes != 0 )
            fwrite( &dwZero, 1, paddingBytes, pFile );
        pBytes += rowSize;
    }
    fclose( pFile );

    return TRUE;
}


If I remember correctly, this code works with both 24- and 32-bit bitmaps, although you should always use 32-bit bitmaps since they are A LOT faster even if you don't need the alpha channel.

The problem was a simple one,

file.open(Filename, std::ios::out)

needed to be


file.open(Filename, std::ios::out | std::ios::binary);

since it needs to write the data in binary mode.

Please don't edit discussion topics as "solved". As these are discussion boards rather than StackExchange-style question/answer boards, there may be other solutions or discussion that can follow that won't happen if you edit topics that way.


If I remember correctly, this code works with both 24- and 32-bit bitmaps, although you should always use 32-bit bitmaps since they are A LOT faster even if you don't need the alpha channel.

I still have to argue with people who think that bitmaps can't have transparency.

There are alot of features of bitmaps that nobody supports though.

For example Microsoft allows for PNG and JPEG compression, but most of their software doesn't support it in a file (its used for GDI only). Its kind of funny how those two modes work, they have basically an entire complete PNG or JPEG file where the pixel data should be. (you can read in a JPEG file, stick it inside of a bitmap container in memory, and send it to GDI and it will render it)

Its kind of funny how those two modes work, they have basically an entire complete PNG or JPEG file where the pixel data should be. (you can read in a JPEG file, stick it inside of a bitmap container in memory, and send it to GDI and it will render it)


Most likely to get around patent issues.

This topic is closed to new replies.

Advertisement