Manipulating the DIB color table for fun and profit


If you create a DIB section at 8bpp or lower, then it will come with a color table. Pixels in the bitmap are represented not by their red/blue/green component values, but are instead indices into the color table. For example, a 4bpp DIB section can have up to sixteen colors in its color table.

Although displays that use 8bpp or lower are considered woefully outdated nowadays, bitmaps in that format are actually quite useful precisely due to the fact that you can manipulate colors in the bitmap, not by manipulating the bits themselves, but instead by manipulating the color table.

Let's demonstrate this by taking the "Gone Fishing" bitmap and converting it to grayscale. Start with our scratch program and make these changes:

HBITMAP g_hbm;

BOOL
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,
                      LR_CREATEDIBSECTION | LR_LOADFROMFILE);
 if (g_hbm) {
  HDC hdc = CreateCompatibleDC(NULL);
  if (hdc) {
   HBITMAP hbmPrev = SelectBitmap(hdc, g_hbm);
   RGBQUAD rgbColors[256];
   UINT cColors = GetDIBColorTable(hdc, 0, 256, rgbColors);
   for (UINT iColor = 0; iColor < cColors; iColor++) {
    BYTE b = (BYTE)((30 * rgbColors[iColor].rgbRed +
                     59 * rgbColors[iColor].rgbGreen +
                     11 * rgbColors[iColor].rgbBlue) / 100);
    rgbColors[iColor].rgbRed = b;
    rgbColors[iColor].rgbGreen = b;
    rgbColors[iColor].rgbBlue = b;
   }
   SetDIBColorTable(hdc, 0, cColors, rgbColors);
   SelectBitmap(hdc, hbmPrev);
   DeleteDC(hdc);
  }
 }
 return TRUE;
}

void
OnDestroy(HWND hwnd)
{
 if (g_hbm) DeleteObject(g_hbm);
 PostQuitMessage(0);
}

void
PaintContent(HWND hwnd, PAINTSTRUCT *pps)
{
 if (g_hbm) {
  HDC hdc = CreateCompatibleDC(NULL);
  if (hdc) {
   HBITMAP hbmPrev = SelectBitmap(hdc, g_hbm);
   BITMAP bm;
   if (GetObject(g_hbm, sizeof(bm), &bm) == sizeof(bm)) {
    BitBlt(pps->hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdc, 0, 0, SRCCOPY);
   }
   SelectBitmap(hdc, hbmPrev);
   DeleteDC(hdc);
  }
 }
}

The OnDestroy function merely cleans up, and the PaintContent function simply draws the bitmap to the window's client area. All the work really happens in the OnCreate function.

First, we load the bitmap as a DIB section by passing the LR_CREATEDIBSECTION flag. This opens up the exciting world of DIB sections, but all we care about is the color table. That happens when we call GetDIBColorTable. Since color tables are supported only up to 8bpp, a color table of size 256 is big enough to handle the worst case. Once we get the color table, we go through each color in it and convert it to grayscale, then set the new color table into the DIB section. That's all.

Notice that we were able to change the color of every single pixel in the bitmap by modifying just 1KB of data. (Four bytes per RGBQUAD times a worst-case of 256 colors.) Even if the bitmap were 1024 × 768, modifying just the color table is enough to change all the colors in the bitmap.

Manipulating the DIB color table is how flags like LR_LOADMAP3DCOLORS and LR_LOADTRANSPARENT do their work. They don't walk the bitmap updating every single pixel; instead, they just load the color table, look for the colors they are interested in, and change that entry in the color table. This technique of editing the color table is what I was referring to when I suggested you could use DIB sections to avoid the pesky DSna raster operation. And it's faster, too. But it only works on bitmaps that are 8bpp or lower.

You may also have noticed that LR_LOADTRANSPARENT doesn't actually load a transparent bitmap. Rather, it loads a bitmap that appears to be transparent provided that you draw it against a window whose color is COLOR_WINDOW. Why this misleading name? Because at the time this flag was invented, GDI didn't support transparent bitmaps. (And even today, it still doesn't really support then, with the notable exception of functions like AlphaBlend.) The best you could do was fake it.

Comments (17)
  1. BryanK says:

    Reminds me of the old 320x200x8bit VGA mode.  Just under 64K of framebuffer RAM required, so even the woefully inadequate DOS programs could address the entire display using one segment (at segment:offset addresses A000:0000 through A000:ffff, IIRC).  And it had the same 256-entry palette (though I think the palette was composed of three bytes per entry, not four).

    And of course the same effects were possible; you could modify the palette instead of the display if you needed to do a global color change.

  2. MichaelAbrashWasMyHero says:

    yeah remember how you used to program effects for games like this?  Like making a waterfall look like it was flowing with palette rotation?

  3. Ben Ryves says:

    who could forget that nifty wormhole effect in FC’s Unreal? :)

    It’s a shame that the .NET analogue (manipulating Bitmap.Palette) appears to be so horribly slow to retrieve and update; to the extent that it is significantly faster to just rewrite every pixel in a 32bpp Bitmap yourself than update the palette. (By significant, I mean reducing a "100%" CPU load and half the original framerate to about 10% – this is when called in a render loop).

  4. GregM says:

    Where did the magic numbers come from for converting to greyscale?  Is that a “standard” set of values, or something you came up with?

    [Response. -Raymond]
  5. Yes, palette rotations rocked.  :)  I remember many old games that used them.

    Hey, shouldn’t this post have been obvious to everybody?  I am still in College and I knew this. :-D

    256 colors an portray a lot of information.  As long as the material is not photographic (or photorealistic), it is enough for most images.  One reason that GIFs are still around!

  6. Mark says:

    GregM: Check out http://www.bersoft.com/bimagem/help/color_channels.htm

    "The coefficients 0.299, 0.587 and 0.114 properly computed luminance for monitors having phosphors that were contemporary at the introduction of NTSC television in 1953, however, these coefficients do not accurately compute luminance for contemporary monitors."

  7. Dewi Morgan says:

    Well, it wasn’t just games (ultime underworld 2, clearly the best game of all time, used it for fires, magic effects, and other things).

    Windows 95 used it too, on the loading screen. Remember the progress banner scrolly thingy at the bottom? Pallette rotated.

    Which meant that you could create cool, slightly-animated loading screens :D

  8. Brian says:

    I remember all the cool kids had "The Matrix" animated Windows 95/98 load screens.

  9. required says:

    Raymond, you know very well that there are a bunch of unfortunates
    out there who just copy & paste the first code they see off the
    ‘net, then you get the problems and write about how stupid they are
    some months down the road. Can’t you at least set a good example by not
    assuming that Windows lives in C:Windows, please?

    [That is not the point of the article. I’m
    assuming you’re smart enough to know that I’m hard-coding for
    expository purposes, in the same way that I often ignore error checking
    for the same reason. -Raymond
    ]
  10. J. Edward Sanchez says:

    BryanK: The stock 320×200×8bit MCGA/VGA video mode (0x13) used the address range from A000:0000 to A000:F9FF. The 256-color palette was set by writing directly to a couple of ports on the VGA. (But you had do it during vertical blank if you didn’t want any snow!) Each palette entry occupied three bytes, but only the lowest six bits of each byte were used. If you wanted to use a standard 24-bit palette, you would have to shift every byte two bits to the right before sending it to the VGA.

    That was the basic stuff. The VGA could actually do modes better than 320×200×8bit, but there was no BIOS support for them, so you’d have to program them in yourself by manipulating the VGA registers directly. The famous "Mode X" (320×240×8bit) needed more than a segment of memory to be addressed, so to get it to work you’d have to "unchain" the VGA and access the frame buffer as four parallel planes. It was actually similar to working with the stock 4-bit EGA and VGA modes. (Little known fact: VGA always used planes internally; the elegant linear frame buffer in mode 0x13 was implemented as a clever hack.)

    Good times, good times.

  11. . says:

    At some point, graphics cards with very slow palette switching started appearing, killing the great palette-rotation effects – the latest games using 256 colors usually included an option to disable it (StarCraft comes to mind, and Diablo got it added on a patch).

    If I had to guess, perhaps the culprit cards stored always RGB and when changing a palette entry they rescaned the whole image- wait, that wouldn’t even work at all. Perhaps they stored two copys – RGB and indexes?

  12. BryanK says:

    Ah — I knew about the "Mode X" stuff, but never learned how to work with it.  Since it’s planar and not linear, it sounds like it’s harder to work with (from the program’s POV) than 0x13 was.

    And right, it’s F9FF because it isn’t a full segment (64000 bytes, not 65536).  Minor little details.  ;-)

    I do remember the VESA interface, though — where you had to map a 64K chunk of the full large-resolution linear framebuffer into a fixed 64K segment at a time.  And if you had to draw across multiple chunks of the large framebuffer, you had to call into one of the VESA software interrupts to remap the 64K window.  That was also a PITA.

    Good times?  Well, maybe…  ;-)

  13. Nomenclature says:

    Colorkeying = transparency

    Alphablending = translucency

  14. Slow palette says:

    My old 286/12 with VGA could change 800k of palette entries per second (which was about ten colors per scanline, if I remember correctly). Same program on a P200 with PCI-card did write 1.2 MB/s. I would bet modern graphics cards isn’t much better.

  15. required says:

    > I’m assuming you’re smart enough to know that I’m hard-coding for expository purposes

    I am, but then I wouldn’t do that (hard code the path) anyway. It is
    the other n% of the software developing population you have to worry
    about (those who, like I said, just copy & paste source off the
    web).

    [Somebody who copies/pastes this code will have a
    hard-coded path to the Gone Fishing bitmap in their app. Unless they’re
    really interested in Gone Fishing, I strongly suspect they’re going to
    change the hard-coded path anyway. -Raymond
    ]
  16. Maybe I am just being picky today but shouldn’t you add 50 to make the B/W computation round off correctly, like this: (in theory you otherwise lose half a percent of luminance)

    BYTE b = (BYTE)((30 * rgbColors[iColor].rgbRed +

    59 * rgbColors[iColor].rgbGreen + 11 * rgbColors[iColor].rgbBlue + 50) / 100);

    Rgrds Henry

    [The point of this article was not the grayscale formula. Grayscale was just a demonstration. Feel free to use whatever formula you want. -Raymond]

Comments are closed.