My research into text rendering in .NET/Windows started with HTML Renderer, I have noticed text issues in the rendered HTML: fonts were pixelated, letter were too close, words were either too close or too far apart (Figure 1). Although minor issues they were bugging me, also I was hoping it will lead to some performance improvements as from my last optimization session text draw/measure were the most time consuming operations.
The resulted improvement exceeded my wildest expectations and worth a post on.
GDI vs. GDI+
There are two ways of drawing/measuring text in .NET using two libraries:
- GDI+ ( Graphics.DrawString , Graphics.MeasureString and Graphics.MeasureCharacterRanges )
- GDI ( TextRenderer.DrawText and TextRenderer.MeasureText )
Figure 1: GDI+ vs. GDI text rendering, note the issues in GDI+ rendered text.
Apparently until .NET 2.0 the framework used GDI+ text rendering methods but in .NET 2.0 GDI text rendering was introduced to be used by all WinForms control, because of backward compatibility it was added side-by-side with legacy GDI+ methods so today we have both options (see SetCompatibleTextRenderingDefault).
There are 3 issues with GDI+ that makes GDI text rendering better:
- GDI+ resolution-independent grid-fitted rendering may cause glyphs to increase in width tightening up the text and in some cases removing spaces between letters resulting in worse looking text (see here and here ).
- The shaping engines for international text have been updated many times for Windows/Uniscribe and for WPF, but have not been updated for GDI+, which causes international rendering support for new languages not to have the same level of quality (see here ).
- There are some performance issues caused by the somewhat stateless nature of GDI+, where device contexts would be set and then the original restored after each call (see here ).
Hence there is no reason using any of the GDI+ draw/measure methods so I won't go into the difference between Graphics.MeasureString and Graphics.MeasureCharacterRanges or the effect of StringFormat.GenericTypographic, if you need to render text the GDI+ methods are a mess you better avoid.
Introduced in .NET 2.0 TextRenderer is the recommended text rendering method, using GDI library under the hood it provided the best looking text, accurate measurement and proper international support.
Using TextRenderer.DrawText and TextRenderer.MeasureText is straightforward and well documented so I won't go into details. The only annoying issue is the padding added to the left (1/6 em) and right (1/4 em) of the drawn/measured text as described here. To have exact draw/measure of text you need the subtract (1/6font.GetHeight()) from left corner during draw and (2.5/6font.GetHeight()) from the measured width (Figure 2).
Figure 2: Using TextRenderer draw/measure without and with adjustments.
While reading on the GDI+ vs. GDI stuff the consensus was that GDI is about 10x faster than GDI+. But switching HTML Renderer from GDI+ to GDI using TextRenderer resulted in ~30% slowdown, obviously performance research is in order.
I set up a quick performance test to draw/measure the same string ("Hello World") using the same font (Arial 10p) 5000 times, comparing each draw/measure method to GDI+ Graphics.DrawString and Graphics.MeasureCharacterRanges as they were originally used in HTML Renderer (Figure 3, Figure 4).
|Method||Avg. time msec||Compared|
Figure 3: Performance comparison for drawing text.
|Method||Avg. time msec||Compared|
Figure 4: Performance comparison for measuring text.
Although TextRenderer.MeasureText is 59% faster, TextRenderer.DrawText is 45% slower and it's much more expansive so that explains HTML Renderer result. There isn't much room to improve TextRenderer performance using the different overloads and flags it can receive so a different approach is needed.
Note, although the performance of Graphics.MeasureString is significantly better (98%) than Graphics.MeasureCharacterRanges its measurement data is extremely inaccurate to the drawn text so it is useless for any practical use.
The next step was to get the hands dirty and call GDI functions directly using p/invoke. It's much more complicated than using TextRenderer so I will dedicate my next post to the details and present a utility class to simplify it.
The important thing to know for now is that DrawText is the function used under-the-hood by TextRenderer and it allows the same rich set of capabilities as TextRenderer. TextOut is very close to the device driver's own TextOut so the call is quicker but also much less powerful (see here), and there is more details here about measuring text.
The performance of native draw/measure is amazing, 56% – 85% faster draw and 99.4% faster measure!
I haven't figured-out why TextRenderer is so much slower than native DrawText (225% slower) as the .NET source code doesn't seems very complicated, but the numbers don't lie.
HTML Renderer performance
Because HTML Renderer has all the logic for text align, line wrapping, etc. the actual displayed text is split into words and each word is measured and drawn separately so the most straightforward text draw/measure functions are required, GetTextExtentPoint32 and TextOut fit that requirement perfectly.
The final result is 60%-70% performance improvement, text measuring takes less than 1% and text drawing takes 25%-30% of all HTML Renderer time, a considerable improvement that switches the focus to other areas of the code for next time (and there will be a next time).
It is worth mentioning that to get the most out of using native GDI I added code to cache the native fonts, minimize the number of times HDC is created and font and color is selected. If to initialize and release everything for each string draw/measure the performance will actually be worse than using TextRenderer.
Wrap it up
- GDI provides better text rendering than GDI+.
- From .NET 2.0 you should always use TextRenderer to draw/measure text.
- For better performance use native call to GDI functions, see Using native GDI for text rendering in C# .
- Integrating native GDI into HTML Renderer resulted in better looking text and 60%-70% performance improvement.
Why text appears different when drawn with GDI+ versus GDI
GDI vs. GDI+ Text Rendering Performance
MSDN: About Text Output
A Guide to WIN32 Clipping Regions
Why is Graphics.MeasureString() returning a higher than expected number?
How to get the exact text margins used by TextRenderer