Search code examples
cwinapigdialphatext-rendering

Is it possible to render antialiased text onto a transparent background with pure GDI?


I've been asking a lot of questions about text aliasing and line aliasing and transparency lately because I wanted to write a platform agnostic vector graphics system for Go; the Windows code is written in C. Premultiplication shenanigans have led me to change the focus over to just rendering text (so I can access system fonts).

Right now I have something that draws text to an offscreen bitmap. This works, except for the antialiased bits. In my code, as I fill the memory buffer with 0xFF to flip the alpha byte (which GDI sets to 0x00 for a pixel that is drawn), the antialiasing is to white. Other people have seen antialiasing to black. This happens with both ANTIALIASED_QUALITY and CLEARTYPE_QUALITY.

I am drawing with TextOut() into a DIB in this case. The DIB is backed by a copy of the screen DC (GetDC(NULL)).

Is there anything I can do to just get text transparent? Can I somehow detect the white pixels, unblend them, and convert that to an alpha? How would I do that for colors too similar to white?


Solution

  • I wrote some code to do this.

    The AntialiasedText function draws anti-aliased text onto an off-screen bitmap. It calculates the transparency so that the text can be blended with any background using the AlphaBlend API function.

    The function is followed by a WM_PAINT handler illustrating its use.

    // Yeah, I'm lazy...
    const int BitmapWidth = 500;
    const int BitmapHeight = 128;
    
    // Draw "text" using the specified font and colour and return an anti-aliased bitmap
    HBITMAP AntialiasedText(LOGFONT* plf, COLORREF colour, LPCWSTR text)
    {
        BITMAPINFO bmi = {0};
        bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader);
        bmi.bmiHeader.biWidth = BitmapWidth;
        bmi.bmiHeader.biHeight = BitmapHeight;
        bmi.bmiHeader.biPlanes = 1;
        bmi.bmiHeader.biBitCount = 32;
    
        LPBYTE pBits;
    
        HBITMAP hDIB = CreateDIBSection(0, &bmi, DIB_RGB_COLORS, (LPVOID*)&pBits, 0, 0);
    
        // Don't want ClearType
        LOGFONT lf = *plf;
        lf.lfQuality = ANTIALIASED_QUALITY;
        HFONT hFont = CreateFontIndirect(&lf);
    
        HDC hScreenDC = GetDC(0);
        HDC hDC = CreateCompatibleDC(hScreenDC);
        ReleaseDC(0, hScreenDC);
    
        HBITMAP hOldBMP = (HBITMAP)SelectObject(hDC, hDIB);
        HFONT hOldFont = (HFONT)SelectObject(hDC, hFont);
    
        RECT rect = {0, 0, BitmapWidth, BitmapHeight};
        FillRect(hDC, &rect, WHITE_BRUSH);
    
        TextOut(hDC, 2, 2, text, wcslen(text));
    
        // Flush drawing
        GdiFlush();
    
        // Calculate alpha
        LPBYTE pixel = pBits;
        int pixelCount = BitmapWidth * BitmapHeight;
        BYTE r = GetRValue(colour);
        BYTE g = GetGValue(colour);
        BYTE b = GetBValue(colour);
        for (int c = 0; c != pixelCount; ++c)
        {
            // Set alpha
            BYTE alpha = 255 - pixel[0];
            pixel[3] = alpha;
            // Set colour
            pixel[0] = b * alpha / 255;
            pixel[1] = g * alpha / 255;
            pixel[2] = r * alpha / 255;
            pixel += 4;
        }
    
        SelectObject(hDC, hOldFont);
        SelectObject(hDC, hOldBMP);
    
        DeleteDC(hDC);
    
        DeleteObject(hFont);
    
        return hDIB;
    }
    

    Here's a WM_PAINT handler to exercise the function. It draws the same text twice, first using TextOut and then using the anti-aliased bitmap. They look much the same, though not as good as ClearType.

    case WM_PAINT:
        {
            LPCWSTR someText = L"Some text";
    
            hdc = BeginPaint(hWnd, &ps);
    
            LOGFONT font = {0};
            font.lfHeight = 40;
            font.lfWeight = FW_NORMAL;
            wcscpy_s(font.lfFaceName, L"Comic Sans MS");
    
            // Draw the text directly to compare to the bitmap
            font.lfQuality = ANTIALIASED_QUALITY;
            HFONT hFont = CreateFontIndirect(&font);
            font.lfQuality = 0;
            HFONT hOldFont = (HFONT)SelectObject(hdc, hFont);
            TextOut(hdc, 2, 10, someText, wcslen(someText));
            SelectObject(hdc, hOldFont);
            DeleteObject(hFont);
    
            // Get an antialiased bitmap and draw it to the screen
            HBITMAP hBmp = AntialiasedText(&font, RGB(0, 0, 0), someText);
            HDC hScreenDC = GetDC(0);
            HDC hBmpDC = CreateCompatibleDC(hScreenDC);
            ReleaseDC(0, hScreenDC);
    
            HBITMAP hOldBMP = (HBITMAP)SelectObject(hBmpDC, hBmp);
    
            BLENDFUNCTION bf;
            bf.BlendOp = AC_SRC_OVER;
            bf.BlendFlags = 0;
            bf.SourceConstantAlpha = 255;
            bf.AlphaFormat = AC_SRC_ALPHA;
    
            int x = 0;
            int y = 40;
    
            AlphaBlend(hdc, x, y, BitmapWidth, BitmapHeight, hBmpDC, 0, 0, BitmapWidth, BitmapHeight, bf);
    
            SelectObject(hBmpDC, hOldBMP);
            DeleteDC(hBmpDC);
    
            DeleteObject(hBmp);
    
            EndPaint(hWnd, &ps);
        }
        break;