If I were Michael Kaplan, I'd have a more clever title like I'm not touching you! or Look but don't touch or maybe Looking at a DIB through BITMAPINFO-colored glasses.

We saw some time ago that you can manipulate the DIB color table to perform wholesale color remapping. But in fact you can do this even without modifying the DIB color table, which is a handy trick if you want to do color remapping but you don't want to change the bitmap itself. For example, the bitmap is not one that is under your control (so you shouldn't be modifying it), or the bitmap might be in use on multiple threads (so modifying it will result in race conditions).

Let's demonstrate this technique by converting the "Gone Fishing" bitmap to grayscale, but doing so without actually modifying the bitmap. As always, we start with our scratch program and make the following changes:

HBITMAP g_hbm;

OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
 // change path as appropriate
 g_hbm = (HBITMAP)LoadImage(g_hinst,
                      TEXT("C:\\Windows\\Gone Fishing.bmp"),
                      IMAGE_BITMAP, 0, 0,
 return TRUE;

OnDestroy(HWND hwnd)
 if (g_hbm) DeleteObject(g_hbm);

PaintContent(HWND hwnd, PAINTSTRUCT *pps)
 if (g_hbm) {
  BITMAP bm;
  if (GetObject(g_hbm, sizeof(bm), &bm) == sizeof(bm) &&
                bm.bmBits != NULL &&
                bm.bmPlanes * bm.bmBitsPixel <= 8) {
   struct BITMAPINFO256 {
    RGBQUAD bmiColors[256];
   } bmiGray;
   ZeroMemory(&bmiGray, sizeof(bmiGray));
   HDC hdc = CreateCompatibleDC(NULL);
   if (hdc) {
    HBITMAP hbmPrev = SelectBitmap(hdc, g_hbm);
    UINT cColors = GetDIBColorTable(hdc, 0, 256, bmiGray.bmiColors);
    for (UINT iColor = 0; iColor < cColors; iColor++) {
     BYTE b = (BYTE)((30 * bmiGray.bmiColors[iColor].rgbRed +
                      59 * bmiGray.bmiColors[iColor].rgbGreen +
                      11 * bmiGray.bmiColors[iColor].rgbBlue) / 100);
     bmiGray.bmiColors[iColor].rgbRed   = b;
     bmiGray.bmiColors[iColor].rgbGreen = b;
     bmiGray.bmiColors[iColor].rgbBlue  = b;
    bmiGray.bmiHeader.biSize        = sizeof(bmiGray.bmiHeader);
    bmiGray.bmiHeader.biWidth       = bm.bmWidth;
    bmiGray.bmiHeader.biHeight      = bm.bmHeight;
    bmiGray.bmiHeader.biPlanes      = bm.bmPlanes;
    bmiGray.bmiHeader.biBitCount    = bm.bmBitsPixel;
    bmiGray.bmiHeader.biCompression = BI_RGB;
    bmiGray.bmiHeader.biClrUsed     = cColors;
    SetDIBitsToDevice(pps->hdc, 0, 0,
                      bmiGray.bmiHeader.biHeight, 0, 0,
                      0, bmiGray.bmiHeader.biHeight,
                     (BITMAPINFO*)&bmiGray, DIB_RGB_COLORS);

    BitBlt(pps->hdc, bm.bmWidth, 0, bm.bmWidth, bm.bmHeight,
           hdc, 0, 0, SRCCOPY);
    SelectBitmap(hdc, hbmPrev);

Things start off innocently enough, loading the bitmap into a DIB section for use during painting.

We do our work at paint time. First, we confirm that we indeed have a DIB section and that it is 8bpp or lower, because bitmaps at higher than 8bpp do not use color tables.

We then select the bitmap into a DC so we can call GetDIBColorTable to get its current color table. (This is the only step that requires the bitmap to be selected into a device context.) We then edit the color table to convert each color to its grayscale equivalent.

Finally, we fill in the BITMAPINFO structure with the description of the bitmap bits, and then we call SetDIBitsToDevice to send the pixels to the destination DC.

Just for good measure, we also BitBlt the original unmodified bitmap, to prove that the original bitmap is intact and unchanged.

This mini-program is really just a stepping stone to other things you can do with this technique of separating the metadata (the BITMAPINFO) from the pixels. We'll continue our investigations tomorrow.

(Before you all run out and use this technique everywhere you can imagine, wait for the remarks in Friday's installment.)