c++winapiopenglsoil

Broken BMP when save bitmap by SOIL. Screenshot area


This is continuation of my last question about saving screenshot to SOIL .here Now I wonder, how to make screenshot of part of screen and eliminate the reason that strange behaviour. My code:

bool saveTexture(string path, glm::vec2 startPos, glm::vec2 endPos)
{
   const char *charPath = path.c_str();

   GLuint widthPart = abs(endPos.x - startPos.x); 
   GLuint heightPart = abs(endPos.y - startPos.y);

   BITMAPINFO bmi;
   auto& hdr = bmi.bmiHeader;
   hdr.biSize = sizeof(bmi.bmiHeader);
   hdr.biWidth = widthPart;
   hdr.biHeight = -1.0 * heightPart;
   hdr.biPlanes = 1;
   hdr.biBitCount = 24;
   hdr.biCompression = BI_RGB;
   hdr.biSizeImage = 0;
   hdr.biXPelsPerMeter = 0;
   hdr.biYPelsPerMeter = 0;
   hdr.biClrUsed = 0;
   hdr.biClrImportant = 0;

   unsigned char* bitmapBits = (unsigned char*)malloc(3 * widthPart * heightPart);

   HDC hdc = GetDC(NULL);
   HDC hBmpDc = CreateCompatibleDC(hdc);
   HBITMAP hBmp = CreateDIBSection(hdc, &bmi, DIB_RGB_COLORS, (void**)&bitmapBits, nullptr, 0);
   SelectObject(hBmpDc, hBmp);
   BitBlt(hBmpDc, 0, 0, widthPart, heightPart, hdc, startPos.x, startPos.y, SRCCOPY);

   //UPDATE:
   - int bytes = widthPart * heightPart * 3;
   - // invert R and B chanels
   - for (unsigned i = 0; i< bytes - 2; i += 3)
   - {
   -   int tmp = bitmapBits[i + 2];
   -   bitmapBits[i + 2] = bitmapBits[i];
   -   bitmapBits[i] = tmp;
   - }

   + unsigned stride = (widthPart * (hdr.biBitCount / 8) + 3) & ~3;
   + // invert R and B chanels
   + for (unsigned row = 0; row < heightPart; ++row) {
   +     for (unsigned col = 0; col < widthPart; ++col) {
   +         // Calculate the pixel index into the buffer, taking the 
             alignment into account
   +         const size_t index{ row * stride + col * hdr.biBitCount / 8 };
   +         std::swap(bitmapBits[index], bitmapBits[index + 2]);
   +      }
   + }

   int texture = SOIL_save_image(charPath, SOIL_SAVE_TYPE_BMP, widthPart, heightPart, 3, bitmapBits);

   return texture;
}

When I run this if widthPart and heightPart is even number, that works perfect. But if something from this is odd number I get this BMP's.:

Broken BMP1

Broken BMP2

I checked any converting and code twice, but it seems to me the reason is in my wrong blit functions. Function of converting RGB is not affect on problem. What can be a reason? It's the right way blitting of area in BitBlt ?

Update No difference even or odd numbers. Correct picture produces when this numbers is equal. I don't know where is a problem.((

Update2

SOIL_save_image functions check parameters for errors and send to stbi_write_bmp:

int stbi_write_bmp(char *filename, int x, int y, int comp, void *data)
{
   int pad = (-x*3) & 3;
   return outfile(filename,-1,-1,x,y,comp,data,0,pad,
       "11 4 22 4" "4 44 22 444444",
       'B', 'M', 14+40+(x*3+pad)*y, 0,0, 14+40,  // file header
        40, x,y, 1,24, 0,0,0,0,0,0);             // bitmap header
}

outfile function:

static int outfile(char const *filename, int rgb_dir, int vdir, int x, int 
y, int comp, void *data, int alpha, int pad, char *fmt, ...)
{
   FILE *f = fopen(filename, "wb");
   if (f) {
      va_list v;
      va_start(v, fmt);
      writefv(f, fmt, v);
      va_end(v);
      write_pixels(f,rgb_dir,vdir,x,y,comp,data,alpha,pad);
      fclose(f);
   }
   return f != NULL;
}

Solution

  • The broken bitmap images are the result of a disagreement of data layout between Windows bitmaps and what the SOIL library expects1. The pixel buffer returned from CreateDIBSection follows the Windows rules (see Bitmap Header Types):

    The scan lines are DWORD aligned [...]. They must be padded for scan line widths, in bytes, that are not evenly divisible by four [...].

    In other words: The width, in bytes, of each scanline is (biWidth * (biBitCount / 8) + 3) & ~3. The SOIL library, on the other hand, doesn't expect pixel buffers to be DWORD aligned.

    To fix this, the pixel data needs to be converted before being passed to SOIL, by stripping (potential) padding and exchanging the R and B color channels. The following code does so in-place2:

    unsigned stride = (widthPart * (hdr.biBitCount / 8) + 3) & ~3;
    
    for (unsigned row = 0; row < heightPart; ++row) {
        for (unsigned col = 0; col < widthPart; ++col) {
            // Calculate the source pixel index, taking the alignment into account
            const size_t index_src{ row * stride + col * hdr.biBitCount / 8 };
            // Calculate the destination pixel index (no alignment)
            const size_t index_dst{ (row * width + col) * (hdr.biBitCount / 8) };
            // Read color channels
            const unsigned char b{ bitmapBits[index_src] };
            const unsigned char g{ bitmapBits[index_src + 1] };
            const unsigned char r{ bitmapBits[index_src + 2] };
            // Write color channels switching R and B, and remove padding
            bitmapBits[index_dst] = r;
            bitmapBits[index_dst + 1] = g;
            bitmapBits[index_dst + 2] = b;
        }
    }
    

    With this code, index_src is the index into the pixel buffer, which includes padding to enforce proper DWORD alignment. index_dst is the index without any padding applied. Moving pixels from index_src to index_dst removes (potential) padding.


    1 The tell-tale sign is scanlines moving to the left or right by one or two pixels (or individual color channels at different speeds). This is usually a safe indication, that there is a disagreement of scanline alignment.
    2 This operation is destructive, i.e. the pixel buffer can no longer be passed to Windows GDI functions once converted, although the original data can be reconstructed, even if a bit more involved.