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); }
- 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.
- Create in-memory bitmap buffer that is compatible with display device context (IntPtr.Zero handle)
- Fill the buffer background with solid color (see limitation)
- Render into the memory bitmap
- 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; } }
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!
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.
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”)
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”)
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 🙂
It took me too long to check it out by i finally did: https://theartofdev.wordpress.com/2014/04/21/text-rendering-methods-comparison-or-gdi-vs-gdi-revised/
In a nutshell (see the post for details)
– GDI+ text rendering visual quality is better than GraphicsPath for almost all fonts.
– GDI+ rendering is about 5-7 times slower than GDI+ text rendering.
– Comparing with GDI TextRenderer is unfair as it is super slow 😦
Also you have a couple of benchmark mistakes in your code:
1. Using DateTime and not Stopwatch.
2. Including image and graphics creation in each iteration.
See the excellent posts series: http://tech.pro/blog/1293/c-performance-benchmark-mistakes-part-one by Eric Lippert.
Cheers,
Arthur.
[…] the comments on my previous post: GDI text rendering to image, I will later post my finding on this […]
[…] 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 […]
[…] GDI text rendering to image. […]
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.
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 ().
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?
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.
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.
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.
[…] 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 […]
[…] GDI text rendering to image . […]