GDI text rendering to image

The saga of GDI text rendering issues continues, after solving Transparent text rendering with GDI I got a complain that using HTML Renderer to rendering images (PNG, bitmap, etc.) results in pixelated text as discussed in this HTML Renderer discussion.

The following code demonstrates the issue using WinForms TextRenderer class:

var image = new  Bitmap(190, 30, PixelFormat.Format32bppArgb);      
using (var g =  Graphics.FromImage(image))      
{      
    g.Clear(Color.WhiteSmoke);      
    TextRenderer.DrawText(g, "Test string  1", new Font("Arial", 12), new Point(5, 5), Color.Red,  Color.Wheat);      
    TextRenderer.DrawText(g, "Test string  2", new Font("Arial", 12), new Point(100, 5), Color.Red);      
}      

 
Resulted image:
clip_image001.png

  • The first DrawText provides background color (opaque text rendering) so the rendered text looks good.
  • The second DrawText does NOT provides background color (transparent text rendering) so the clear-type adjusted pixels in the rendered text get corrupted.

 
The root problem is that text rendering uses clear-type to improve text appearance by changing the actual color of text adjacent pixels. But when using graphics created from image object it fails to do so, unless a solid background color is used in the text rendering itself.
This problem is unique to image rendering as running the same example code using graphics object created from WinForms control works perfect, even if there is no background at all but what existed on the WinForms control area before rendering.
 
My hypothesis is that screen device uses the background pixels on the rendered frame to handle clear-type text rendering as the screen is never transparent, but because image rendering can create fully transparent "frame" it can't do that so it doesn't even try.
If you know something on this subject please, PLEASE let leave a comment.
 
The significance of the issue is to render text over complex background like gradient or background image, so just using the same solid color for text rendering as the background is not a solution.
 

Solution

The display device support of GDI text rendering over existing background can be used to create in-memory buffer compatible with display device context, render the text to it and then copy the fully rendered buffer into an image.

  1. Create in-memory bitmap buffer that is compatible with display device context (IntPtr.Zero handle)
  2. Fill the buffer background with solid color (see limitation)
  3. Render into the memory bitmap
  4. Copy from in-memory bitmap to image device context (BitBlt)

 
Limitation
Transparent image cannot be created, GDI clear-type text rendering must have non-transparent pixels to adjust its color and because GDI doesn't support alpha channel in the clear-type adjustment it assumes that the pixel is black so it creates the rough pixelated result.
If transparent image is required the only solution is to use GDI+ text rendering and not using TextRenderingHint.ClearTypeGridFit.
 

Sample code

public static class  Test      
{      
    public static Image Render()      
    {      
        // create the final image to render  into      
        var image = new Bitmap(190, 30,  PixelFormat.Format32bppArgb);      

        // create memory buffer from desktop  handle that supports alpha channel      
        IntPtr dib;      
        var memoryHdc =  CreateMemoryHdc(IntPtr.Zero, image.Width, image.Height, out dib);      
        try      
        {      
            // create memory buffer graphics to  use for HTML rendering      
            using (var memoryGraphics =  Graphics.FromHdc(memoryHdc))      
            {      
                // must not be transparent  background       
                memoryGraphics.Clear(Color.White);      

                // execute GDI text rendering      
                TextRenderer.DrawText(memoryGraphics, "Test string 1", new  Font("Arial", 12), new Point(5, 5), Color.Red, Color.Wheat);      
                TextRenderer.DrawText(memoryGraphics, "Test string 2", new  Font("Arial", 12), new Point(100, 5), Color.Red);      
            }      

            // copy from memory buffer to image      
            using (var imageGraphics =  Graphics.FromImage(image))      
            {      
                var imgHdc =  imageGraphics.GetHdc();      
                BitBlt(imgHdc, 0, 0,  image.Width, image.Height, memoryHdc, 0, 0, 0x00CC0020);      
                imageGraphics.ReleaseHdc(imgHdc);      
            }      
        }      
        finally      
        {      
            // release memory buffer      
            DeleteObject(dib);      
            DeleteDC(memoryHdc);      
        }      

        return image;      
    }      

    private static IntPtr  CreateMemoryHdc(IntPtr hdc, int width, int height, out IntPtr dib)      
    {      
        // Create a memory DC so we can work  off-screen      
        IntPtr memoryHdc =  CreateCompatibleDC(hdc);      
        SetBkMode(memoryHdc, 1);      

        // Create a device-independent bitmap  and select it into our DC      
        var info = new BitMapInfo();      
        info.biSize = Marshal.SizeOf(info);      
        info.biWidth = width;      
        info.biHeight = -height;      
        info.biPlanes = 1;      
        info.biBitCount = 32;      
        info.biCompression = 0; // BI_RGB      
        IntPtr ppvBits;      
        dib = CreateDIBSection(hdc, ref info,  0, out ppvBits, IntPtr.Zero, 0);      
        SelectObject(memoryHdc, dib);      

        return memoryHdc;      
    }      

    [DllImport("gdi32.dll")]      
    public static extern int SetBkMode(IntPtr  hdc, int mode);      

    [DllImport("gdi32.dll",  ExactSpelling = true, SetLastError = true)]      
    private static extern IntPtr  CreateCompatibleDC(IntPtr hdc);      

    [DllImport("gdi32.dll")]      
    private static extern IntPtr  CreateDIBSection(IntPtr hdc, [In] ref BitMapInfo pbmi, uint iUsage, out IntPtr  ppvBits, IntPtr hSection, uint dwOffset);      

    [DllImport("gdi32.dll")]      
    public static extern int  SelectObject(IntPtr hdc, IntPtr hgdiObj);      

    [DllImport("gdi32.dll")]      
    [return: MarshalAs(UnmanagedType.Bool)]      
    public static extern bool BitBlt(IntPtr  hdc, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc,  int nYSrc, int dwRop);      

    [DllImport("gdi32.dll")]      
    public static extern bool  DeleteObject(IntPtr hObject);      

    [DllImport("gdi32.dll",  ExactSpelling = true, SetLastError = true)]      
    public static extern bool DeleteDC(IntPtr  hdc);      

    [StructLayout(LayoutKind.Sequential)]      
    internal struct BitMapInfo      
    {      
        public int biSize;      
        public int biWidth;      
        public int biHeight;      
        public short biPlanes;      
        public short biBitCount;      
        public int biCompression;      
        public int biSizeImage;      
        public int biXPelsPerMeter;      
        public int biYPelsPerMeter;      
        public int biClrUsed;      
        public int biClrImportant;      
        public byte bmiColors_rgbBlue;      
        public byte bmiColors_rgbGreen;      
        public byte bmiColors_rgbRed;      
        public byte bmiColors_rgbReserved;      
    }      
}      

17 comments on “GDI text rendering to image

  1. Kris says:

    I use a different approach … hit this several times … just render text to a graphics path and then use fillpath … with SmoothingMode=High it looks almost 100% like clear-type!

    • Arthur says:

      Nice one, but let me theorize that you use GDI+ text rendering for that so there are a couple consequence to this approach:
      1. GDI+ clear-type still look worse than GDI text rendering
      2. GDI+ text rendering is much slower.
      3. Fillpath rendering is even slower than text rendering so you get double the penalties
      4. You still can’t render text with transparent background.
      I didn’t checked it thoroughly so let me know if I’m wrong here.
      Cheers.

      • Kris says:

        Actually it is faster too…

        TextRenderer: 1870.2798 ms
        GraphicsPath: 1231.5641 ms

        Dim st = Date.Now
        For i = 1 To 10000
        Using b As New Bitmap(100, 100)
        Using g = Graphics.FromImage(b)
        TextRenderer.DrawText(g, “Hello”, SystemFonts.DefaultFont, New Point(0, 0), Color.Black)
        End Using
        End Using
        Next
        Debug.Print(“TextRenderer: ” & Date.Now.Subtract(st).TotalMilliseconds & ” ms”)

        st = Date.Now
        For i = 1 To 10000
        Using b As New Bitmap(100, 100)
        Using g = Graphics.FromImage(b)
        Using p As New System.Drawing.Drawing2D.GraphicsPath
        Dim f = SystemFonts.DefaultFont
        p.AddString(“hello”, f.FontFamily, f.Style, f.Size, New Point(0, 0), System.Drawing.StringFormat.GenericDefault)
        End Using
        ‘ TextRenderer.DrawText(g, “Hello”, SystemFonts.DefaultFont, New Point(0, 0), Color.Black)
        End Using
        End Using
        Next
        Debug.Print(“GraphicsPath: ” & Date.Now.Subtract(st).TotalMilliseconds & ” ms”)

      • Kris says:

        Whops forgot to actually render it … but still faster:

        TextRenderer: 1823.084 ms
        GraphicsPath: 1403.0619 ms

        Dim st = Date.Now
        For i = 1 To 10000
        Using b As New Bitmap(100, 100)
        Using g = Graphics.FromImage(b)
        TextRenderer.DrawText(g, “Hello”, SystemFonts.DefaultFont, New Point(0, 0), Color.Black)
        End Using
        End Using
        Next
        Debug.Print(“TextRenderer: ” & Date.Now.Subtract(st).TotalMilliseconds & ” ms”)

        st = Date.Now
        For i = 1 To 10000
        Using b As New Bitmap(100, 100)
        Using g = Graphics.FromImage(b)
        Using p As New System.Drawing.Drawing2D.GraphicsPath
        Dim f = SystemFonts.DefaultFont
        p.AddString(“hello”, f.FontFamily, f.Style, f.Size, New Point(0, 0), System.Drawing.StringFormat.GenericDefault)
        g.FillPath(Brushes.Black, p)
        End Using
        End Using
        End Using
        Next
        Debug.Print(“GraphicsPath: ” & Date.Now.Subtract(st).TotalMilliseconds & ” ms”)

  2. Kris says:

    And the final results with g.SmoothingMode = Drawing2D.SmoothingMode.HighQuality for GP rendering is:

    TextRenderer: 1869.7013 ms
    GraphicsPath: 1480.9865 ms

    … also the output (with transparent BG):


    In summary:

    1. GDI+ clear-type still look worse than GDI text rendering
    Maybe … but looks ALOT better on transparent BGs

    2. GDI+ text rendering is much slower.
    Don’t know didn’t test … but still has the issue with rendering on transparent BG, so no point

    3. Fillpath rendering is even slower than text rendering so you get double the penalties
    Incorrect

    4. You still can’t render text with transparent background.
    Yes you can 🙂

  3. […] the comments on my previous post: GDI text rendering to image, I will later post my finding on this […]

  4. […] response to my earlier post (see: GDI text rendering to image) Kris commented he uses GraphicsPath for text rendering to image to achieve better visual quality […]

  5. calloatti says:

    Hello there, I have been trying to solve this same issue for the last few days. I think I have found a siimple solution to this problem, and a reason why this happens.

    I use pure Windows API calls, no managed code. This is how i reproduce your problem:

    I use GdipCreateBitmapFromScan0 to create a gdi+ bitmap
    I use GdipGetImageGraphicsContext to get a graphics object
    I do some drawing using GdipDrawImageRectRectI

    Now I get an hdc using GdipGetDC.

    Call SetBkMode(m.hDC, BACKGROUND_TRANSPARENT)
    I create a font.
    Call SetTextColor(rgb(255,0,0)

    Call DrawText to draw some text.

    Clean up, save the bitmap, I get exactly the same result you posted. Why this happens?

    It happens because DrawText is using ClearType, and it is doing the anti aliasing against a black background!

    Now the question is, why the hell is DrawText thinking that there is a black background, if the bitmap already has something drawn in it with GDIPLUS functions?

    And the answer is, because GdipGetDC returns a DC that GDI functions used on it assume is totally empty, or black. Why? No idea.

    Since my bitmaps are rather small, my quick and dirty solution is this:

    BEFORE using GdipGetDC:

    1. Get an hbitmap handle of your bitmap using GdipCreateHBITMAPFromBitmap
    2. Use CreatePatternBrush with this hbitmap.
    3. Call GdipGetDC
    4. Do RectFill on this dc with the brush

    Basically I am copying the bitmap into itself right? Well, now DrawText will “see” the background, and will correctly apply cleartype dithering into it.

    Just do some cleaning up, and then call GdipReleaseDC and you are done.

    OF COURSE, I am doing this to the whole bitmap because it is small, you can do the same thing but only to the rect where your text goes, of course.

    And the funny thing is, the exact same thing happens when using DrawThemeBackground to draw a radio button. The radio button image has some pixels with alpha, and if you dont follow the exact some procedure, it blends to black.

    • Arthur says:

      Cool, I don’t think one method is better than the other, do you see advantages to the method you proposed?
      From a simple performance testing I did the overhead is small 10%-30% if I remember correctly.
      I come to using “CreateCompatibleDC” because the same general method can be used to create transparent text ().

  6. calloatti says:

    More:

    The only difference between the top row and the bottom row, is that I commented one line of code:

    FillRect(hDC, pRect, hBrush)

    How about that?

  7. calloatti says:

    And even more:

    “but because image rendering can create fully transparent “frame” it can’t do that so it doesn’t even try”

    Umm not really, if you use GDIP_PIXELFORMAT24BPPRGB in the call to tbzGdipCreateBitmapFromScan0, this still happens, so its not that.

    By the way, the above method works no matter what is behind the text, a solid background, an image, a gradient, anything.

    If your background is just a solid color, of course createsolidbrush can be used instead of CreatePatternBrush.

    • Arthur says:

      Well, like I said, don’t know why it doesn’t work out of the box, I don’t see a good reason for it.
      With my method you can also draw on any background, a simple BitBlt from the existing image into the memory buffer.

      • calloatti says:

        I guess both methods are valid as they obtain the same result. And… I found out why this happens:

        http://support.microsoft.com/kb/867631

        “When the Graphics::GetHDC() method is called, a memory HDC internal method is created, and then a new HBITMAP handle object is created and is put in the memory HDC internal method. This new memory bitmap is not initialized with the original image of the bitmap. This new memory bitmap is initialized with a sentinel pattern that enables GDI+ graphics object to track changes to the new memory bitmap.”

        So there, DrawText or DrawThemeBackground will never work correctly, as the bitmap is not really there.

        Case solved.

  8. […] response to my earlier post (see: GDI text rendering to image ) Kris commented he uses GraphicsPath for text rendering to image to achieve better visual quality […]

Leave a Reply to Blurry/Fuzzy GDI+ Text Rendering using AntiAlias and floating-point Y coordinates « The Art of Dev Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s