How do I set the alpha channel of a GDI bitmap to 255?

Raymond

Most GDI operations will destroy the alpha channel, because GDI was invented back in the days of monochrome and CGA, where you had one, maybe two bits per pixel, and the paper introducing the concept of an alpha channel wouldn’t be published for another year.

Since the alpha channel hadn’t been invented yet, the top eight bits of 32bpp pixel formats were unused. Whenever GDI needed to generate a 32bpp pixel, say as the result of text rendering, the results had zero in the top eight bits because, well, the bits had no defined meaning. And if you thought those bits were your alpha channel, well, you just lost your alpha channel.

Okay, so you accept that GDI operations are going to wipe out your alpha channel. How do you get it back, assuming you just want to make your bitmap opaque again?

Fortunately, there’s still a GDI operation that doesn’t destroy the alpha channel: BitBlt. Probably because BltBlt is defined in terms of bitwise operations, so the work is done without really thinking about what the bits mean. And we can take advantage of that.

// hdc is a memory DC with a 32bpp bitmap selected into it.
// This function sets the alpha channel to 255 without
// affecting any of the color channels.

void MakeBitmapOpaque(
    HDC hdc, int x, int y, int cx, int cy)
{
    BITMAPINFO bi = {};
    bi.bmiHeader.biSize        = sizeof(BITMAPINFOHEADER);
    bi.bmiHeader.biWidth       = 1;
    bi.bmiHeader.biHeight      = 1;
    bi.bmiHeader.biPlanes      = 1;
    bi.bmiHeader.biBitCount    = 32;
    bi.bmiHeader.biCompression = BI_RGB;

    RGBQUAD bitmapBits = { 0x00, 0x00, 0x00, 0xFF };

    StretchDIBits(hdc, x, y, cx, cy,
                  0, 0, 1, 1, &bitmapBits, &bi,
                  DIB_RGB_COLORS, SRCPAINT);
}

The first step is to create a BITMAPINFO structure that describes a 1×1 32bpp bitmap.

We then create that single pixel consisting of an alpha channel (which resides in the reserved bits) of 255, and zero for the color channels.

And then we ask to stretch that 1×1 bitmap over the destination bitmap, using the SRCPAINT raster operation.

The secret sauce is that the SRCPAINT raster operation means that the source and destination should be OR’d together to form the result. Our source pixel is { 0x00, 0x00, 0x00, 0xFF }, so this means that for each destination pixel, the color channels are left unchanged (OR’d with zero) and the alpha channel is set to 255 (OR’d with 255).

Bingo, this sets the alpha channel of the entire bitmap back to 255.

This lets you use functions like FillRect or DrawText, and let them destroy the alpha channel of your opaque bitmap. Then come back and repair the alpha channel by setting the bitmap back to opaque without changing any of the color channels.

This trick makes the specified portion of the bitmap opaque. If you want it to have a different alpha channel, you could use two StretchBlt operations, one to zero out the alpha channel, and another to OR in the desired value.

// hdc is a memory DC with a 32bpp bitmap selected into it.
// This function sets the alpha channel without
// affecting any of the color channels.

void SetBitmapAlphaChannel(
    HDC hdc, int x, int y, int cx, int cy, BYTE alpha)
{
    BITMAPINFO bi = {};
    bi.bmiHeader.biSize        = sizeof(BITMAPINFOHEADER);
    bi.bmiHeader.biWidth       = 1;
    bi.bmiHeader.biHeight      = 1;
    bi.bmiHeader.biPlanes      = 1;
    bi.bmiHeader.biBitCount    = 32;
    bi.bmiHeader.biCompression = BI_RGB;

    if (alpha != 255) {
        RGBQUAD zeroAlpha = { 0xFF, 0xFF, 0xFF, 0x00 };
        StretchDIBits(hdc, x, y, cx, cy,
                      0, 0, 1, 1, &zeroAlpha, &bi,
                      DIB_RGB_COLORS, SRCAND);
    }

    RGBQUAD alphaOnly = { 0x00, 0x00, 0x00, alpha };
    StretchDIBits(hdc, x, y, cx, cy,
                  0, 0, 1, 1, &alphaOnly, &bi,
                  DIB_RGB_COLORS, SRCPAINT);
}

The SRCAND raster operation performs a logical AND of the source and destination. We set the entire alpha channel to zero by ANDing it with zero, and then we set the value to the desired value by ORing the new value in. (And we can skip the AND step if the desired alpha is 255.)

3 comments

Leave a comment

  • Joshua Hudson

    Too bad I can’t set a brush or a pen with an alpha channel and have it paint with that alpha channel.

    By which I do not mean handle the partial transparency bit merge. By which I mean paint over that part of the image and setting the alpha channel as I go. That way I can draw a 100% opaque text output with DrawText over a 40% opaque oval and have the rest of the oval remain 40% opaque.

  • Mikie

    There is something strange going on with StretchDIBits:

    According to my experiments, when using on top-down bitmaps with
    DestWidth==SrcWidth && DestHeight==SrcHeight && ySrc==0,
    the origin of the source bitmap (for xSrc and ySrc) is considered to be the upper-left corner!
    Otherwise (i.e. e.g. when ySrc>0) the origin is considered to be the lower-left corner (as in SetDIBitsToDevice).

    Can anybody confirm this behaviour?
    Is there any sane reason behind this inconsistency?
    It there a way to display say a bottom half of a top-down bitmap using StretchDIBits without actual stretching?

  • Mikie

    More fun with StretchDIBits and top-down bitmaps.
    Further experiments show even more wacky behaviour. Here’s the summary:

    Win3.11, Win3.11+Win32s – do not support top-down bitmaps
    WinNT3.1, WinNT3.51 – origin at the top (as in the _current_ documentation)
    Win95, Win98SE, WinMe – origin at the bottom (as described in Petzold’s books)
    WinNT4, Win2k, WinXP, WinXP64, Win7, Win10 – origin random (see the post above)

    So apparently we are still living with the bug introduced in 1996.