2

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?

Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
andlabs
  • 11,290
  • 1
  • 31
  • 52
  • 1
    It is possible, just don't count on it looking like anti-aliased text. The typical outcome is a black blob, the result of the background being black with an alpha of 0. Anti-aliasing from black to black. Core problem of course is that you can never guess at the actual background behind the window or bitmap, that requires a time machine. Note how Windows solved it for glass, the title of the window is rendered on a milky background that is roughly the same shape as the text. DrawThemeTextEx() does that with DTT_GLOWSIZE. – Hans Passant Sep 24 '14 at 18:59
  • I've always wondered if it was possible, using the power of **maths**, to convert a bitmap containing text anti-aliased against a known solid background color into text with an alpha channel. If you come up with a way to do it please let us know :) – Jonathan Potter Sep 24 '14 at 19:47
  • Detecting the antialiasing pixels should be easy (just search for any color that isn't the input color), though that creates issues when the color in question is close enough to white. @Hans I know my blob color is full white because I'm manually filling in `ppvBits` with `memset()`, but other than that, yes, that's exactly my problem! – andlabs Sep 24 '14 at 19:59
  • I have to ask this: Why not simply use Cairo and the go-cairo bindings? Or write bindings for AGG (Anti-Grain Geometry)? – datenwolf Sep 24 '14 at 20:39
  • I'm already using cairo on Unix systems. My idea was to remain dependency-less. AGG might work, maybe, hm... – andlabs Sep 24 '14 at 21:29

1 Answers1

4

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;
arx
  • 16,686
  • 2
  • 44
  • 61
  • @andlabs I neglected to mention that my code calculates the transparency. Does it work for you? – arx Sep 25 '14 at 09:16
  • I haven't tested it yet but i take your word for it. +1 – γηράσκω δ' αεί πολλά διδασκόμε Sep 25 '14 at 19:43
  • (Sorry for the delay.) Seems to work for me, though is it just me or is this comma too transparent? Helvetica, 12-point, either antialiased or ClearType quality (they look the same for the comma). http://imgur.com/A7GYmmO (in wine) Thanks again! That's a clever approach and I wish I had thought of that =P – andlabs Sep 30 '14 at 13:34
  • @andlabs Cheers. The calculations in the code assume linear brightness so you could possibly improve the result by adding gamma correction. That said, the comma in your bitmap looks fine to me. You could create a more objective test by also drawing with `TextOut` on the same bitmap. – arx Sep 30 '14 at 14:37
  • I'll make a note to do that soon and will report back. – andlabs Sep 30 '14 at 14:47