Skip to content

Instantly share code, notes, and snippets.

@ArthurHub
Last active December 6, 2021 21:58
Show Gist options
  • Save ArthurHub/10736580 to your computer and use it in GitHub Desktop.
Save ArthurHub/10736580 to your computer and use it in GitHub Desktop.
Class used for running text rendering methods comparison (http://theartofdev.com/2014/04/21/text-rendering-methods-comparison-or-gdi-vs-gdi-revised/)
public class Test
{
private const int BaseIterations = 3;
private const int RenderIterations = 10000;
private const string TestString = "Test-s.tri,ng m(g=j{}3)";
private static readonly Font _font1 = new Font("Arial", 11);
// private static readonly Font _font1 = new Font("Segoe UI", 9);
// private static readonly Font _font1 = new Font("Tahoma", 10);
// private static readonly Font _font1 = new Font("Microsoft Sans Serif", 9);
// private static readonly Font _font1 = SystemFonts.DefaultFont;
private const string TestStringS = "lllllllll HtmlLabel";
private static readonly Font _fontS = new Font("Microsoft Sans Serif", 9, FontStyle.Bold);
private static readonly Font _fontPerf1 = new Font("Arial", 11);
private static readonly Font _fontPerf2 = new Font(SystemFonts.DefaultFont.FontFamily, 12);
private static readonly Font _fontPerf3 = new Font("Arial", 12);
private static readonly Font[] _fontsPerf = new[] {_fontPerf1, _fontPerf2, _fontPerf3};
public static void Run()
{
try
{
RunVisualTest(TestString, _font1, "TestVisual.png");
RunVisualTest(TestStringS, _fontS, "TestVisual2.png");
RunGdiVsGdiPlusVisualTest(1);
RunGdiVsGdiPlusVisualTest(2);
RunGdiVsGdiPlusVisualTest(3);
RunGdiVsGdiPlusVisualTest(10);
RunGdiVsGdiPlusVisualTest(20);
RunGdiVsGdiPlusVisualTest(21);
RunGdiVsGdiPlusVisualTest(30);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
RunPerformanceTest();
}
catch(Exception e)
{
Console.WriteLine(e);
}
}
private static void RunVisualTest(String str, Font font, string file)
{
var image = new Bitmap(600, 400, PixelFormat.Format32bppArgb);
int xOffset = image.Width/2 + 5;
int yOffset = 15;
int yOffsetStep = font.Height + 15;
var offset = (int)( font.GetHeight()/6f );
using(var g = Graphics.FromImage(image))
{
using(var b = new LinearGradientBrush(new Point(0, 0), new Point(xOffset - 15, 0), Color.LightGray, Color.White))
g.FillRectangle(b, 0, 0, xOffset - 15, image.Height);
TextRenderer.DrawText(g, str + " (TextRenderer)", font, new Point(5, yOffset), Color.Red, Color.White);
TextRenderer.DrawText(g, str + " (TextRenderer)", font, new Point(xOffset, yOffset), Color.Red);
yOffset += yOffsetStep;
DrawNativeString(str + " (Native)", image, 5, yOffset, font);
DrawNativeString(str + " (Native)", image, xOffset, yOffset, font);
yOffset += yOffsetStep;
g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
g.DrawString(str + " (DrawString CT)", font, Brushes.Red, new Point(5, yOffset));
g.DrawString(str + " (DrawString CT)", font, Brushes.Red, new Point(xOffset, yOffset));
g.TextRenderingHint = TextRenderingHint.SystemDefault;
yOffset += yOffsetStep;
g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
g.DrawString(str + " (DrawString CT-T)", font, Brushes.Red, new Point(5 + offset, yOffset), StringFormat.GenericTypographic);
g.DrawString(str + " (DrawString CT-T)", font, Brushes.Red, new Point(xOffset + offset, yOffset), StringFormat.GenericTypographic);
g.TextRenderingHint = TextRenderingHint.SystemDefault;
yOffset += yOffsetStep;
g.TextRenderingHint = TextRenderingHint.AntiAlias;
g.DrawString(str + " (DrawString AA)", font, Brushes.Red, new Point(5, yOffset));
g.DrawString(str + " (DrawString AA)", font, Brushes.Red, new Point(xOffset, yOffset));
g.TextRenderingHint = TextRenderingHint.SystemDefault;
yOffset += yOffsetStep;
g.TextRenderingHint = TextRenderingHint.AntiAlias;
g.DrawString(str + " (DrawString AA-T)", font, Brushes.Red, new Point(5 + offset, yOffset), StringFormat.GenericTypographic);
g.DrawString(str + " (DrawString AA-T)", font, Brushes.Red, new Point(xOffset + offset, yOffset), StringFormat.GenericTypographic);
g.TextRenderingHint = TextRenderingHint.SystemDefault;
yOffset += yOffsetStep;
DrawGraphicsPathString(str + " (GraphicsPath D)", 5, yOffset, g);
DrawGraphicsPathString(str + " (GraphicsPath D)", xOffset, yOffset, g);
yOffset += yOffsetStep;
g.SmoothingMode = SmoothingMode.HighQuality;
g.CompositingQuality = CompositingQuality.HighQuality;
DrawGraphicsPathString(str + " (GraphicsPath HQ)", 5, yOffset, g);
DrawGraphicsPathString(str + " (GraphicsPath HQ)", xOffset, yOffset, g);
g.SmoothingMode = SmoothingMode.Default;
}
image.Save(file, ImageFormat.Png);
}
private static void RunGdiVsGdiPlusVisualTest(int type)
{
var image = new Bitmap(3400, 3100, PixelFormat.Format32bppArgb);
using(var g = Graphics.FromImage(image))
{
g.Clear(Color.White);
List<Font> fonts = new List<Font>();
foreach(var fontFamily in FontFamily.Families)
{
try
{
FontStyle fontStyle = FontStyle.Regular;
if( type == 2 )
fontStyle = FontStyle.Italic;
else if( type == 3 )
fontStyle = FontStyle.Bold;
fonts.Add(new Font(fontFamily, 10, fontStyle));
}
catch
{}
}
int xOffset = 5;
int yOffset = 5;
foreach(var font in fonts)
{
var fh = (int)font.GetHeight();
var str = type != 20 ? font.Name : font.Name + " " + "שלום";
if( type == 21 )
str = "wwwwwwwwwwww llllllllllll wlewlewlewlewlewlewle";
if( type == 10 )
{
var s = MeasureStringNative(str, font, g);
g.DrawRectangle(Pens.Black, xOffset + (int)( _font1.GetHeight()/6f ), yOffset, s.Width, s.Height);
s = MeasureString(str, font, g, StringFormat.GenericTypographic);
g.DrawRectangle(Pens.Black, xOffset + (int)( _font1.GetHeight()/6f ), yOffset + fh, s.Width, s.Height);
g.DrawRectangle(Pens.Black, xOffset + (int)( _font1.GetHeight()/6f ), yOffset + fh*2, s.Width, s.Height);
s = MeasureString(str, font, g, StringFormat.GenericDefault);
g.DrawRectangle(Pens.Black, xOffset + (int)( _font1.GetHeight()/6f ), yOffset + fh*3, s.Width, s.Height);
}
DrawNativeString(str, image, xOffset, yOffset, font);
g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
g.DrawString(str, font, Brushes.Red, new Point(xOffset + (int)( font.GetHeight()/6f ), yOffset + fh), StringFormat.GenericTypographic);
g.TextRenderingHint = TextRenderingHint.AntiAlias;
g.DrawString(str, font, Brushes.Red, new Point(xOffset + (int)( font.GetHeight()/6f ), yOffset + fh*2), StringFormat.GenericTypographic);
g.DrawString(str, font, Brushes.Red, new Point(xOffset, yOffset + fh*3));
yOffset += 4*fh + 10;
if( yOffset > 3000 )
{
yOffset = 5;
xOffset += 300;
}
}
}
image.Save("GdiVsGdiPlus_" + type + ".png", ImageFormat.Png);
}
private static void RunPerformanceTest()
{
GC.Collect();
long tr = RunPerformanceIterations(1);
GC.Collect();
long gp = RunPerformanceIterations(2);
GC.Collect();
long gp_hq = RunPerformanceIterations(3);
GC.Collect();
long n = RunPerformanceIterations(4);
GC.Collect();
long ds_ct = RunPerformanceIterations(5);
GC.Collect();
long ds_aa = RunPerformanceIterations(6);
GC.Collect();
long ds_aat = RunPerformanceIterations(7);
var sb = new StringBuilder();
sb.AppendFormat("Native GDI: {0}", n).AppendLine();
sb.AppendFormat("DrawString AA: {0} ({1:N1})", ds_aa, ds_aa/(double)n).AppendLine();
sb.AppendFormat("DrawString AAT: {0} ({1:N1})", ds_aat, ds_aat/(double)n).AppendLine();
sb.AppendFormat("DrawString CT: {0} ({1:N1} - {2:N1})", ds_ct, ds_ct/(double)n, ds_ct/(double)ds_aa).AppendLine();
sb.AppendFormat("TextRenderer: {0} ({1:N1} - {2:N1})", tr, tr/(double)n, tr/(double)ds_aa).AppendLine();
sb.AppendFormat("GraphicsPath: {0} ({1:N1} - {2:N1})", gp, gp/(double)n, gp/(double)ds_aa).AppendLine();
sb.AppendFormat("GraphicsPath HQ: {0} ({1:N1} - {2:N1})", gp_hq, gp_hq/(double)n, gp_hq/(double)ds_aa).AppendLine();
MessageBox.Show(sb.ToString());
}
#region Performance tests
public static long RunPerformanceIterations(int type)
{
var sw = Stopwatch.StartNew();
for(int i = 0; i < BaseIterations; i++)
{
var font = _fontsPerf[i%_fontsPerf.Length];
var image = new Bitmap(500, 500, PixelFormat.Format32bppArgb);
using(var g = Graphics.FromImage(image))
{
switch( type )
{
case 1:
RunTextRenderer(g, font);
break;
case 2:
RunGraphicsPathNormal(g, font);
break;
case 3:
RunGraphicsPathHighQuiality(g, font);
break;
case 4:
RunNative(image, font);
break;
case 5:
RunDrawStringCT(g, font);
break;
case 6:
RunDrawStringAA(g, font);
break;
case 7:
RunDrawStringAAT(g, font);
break;
}
}
}
return sw.ElapsedMilliseconds;
}
private static void RunTextRenderer(Graphics g, Font font)
{
for(int i = 0; i < RenderIterations; i++)
{
TextRenderer.DrawText(g, TestString, font, new Point(5, 5), Color.Red);
}
}
private static void RunDrawStringCT(Graphics g, Font font)
{
g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
for(int i = 0; i < RenderIterations; i++)
{
g.DrawString(TestString, font, Brushes.Red, new Point(5, 5));
}
g.TextRenderingHint = TextRenderingHint.SystemDefault;
}
private static void RunDrawStringAA(Graphics g, Font font)
{
g.TextRenderingHint = TextRenderingHint.AntiAlias;
for(int i = 0; i < RenderIterations; i++)
{
g.DrawString(TestString, font, Brushes.Red, new Point(5, 5));
}
g.TextRenderingHint = TextRenderingHint.SystemDefault;
}
private static void RunDrawStringAAT(Graphics g, Font font)
{
g.TextRenderingHint = TextRenderingHint.AntiAlias;
for(int i = 0; i < RenderIterations; i++)
{
g.DrawString(TestString, font, Brushes.Red, new Point(5, 5), StringFormat.GenericTypographic);
}
g.TextRenderingHint = TextRenderingHint.SystemDefault;
}
private static void RunGraphicsPathNormal(Graphics g, Font font)
{
var fontFamily = font.FontFamily;
float emSize = g.DpiY*_font1.Size/72f;
for(int i = 0; i < RenderIterations; i++)
{
using(var path = new GraphicsPath())
{
path.AddString(TestString, fontFamily, (int)font.Style, emSize, new Point(5, 5), StringFormat.GenericDefault);
g.FillPath(Brushes.Red, path);
}
}
}
private static void RunGraphicsPathHighQuiality(Graphics g, Font font)
{
var fontFamily = font.FontFamily;
float emSize = g.DpiY*_font1.Size/72f;
g.SmoothingMode = SmoothingMode.HighQuality;
for(int i = 0; i < RenderIterations; i++)
{
using(var path = new GraphicsPath())
{
path.AddString(TestString, fontFamily, (int)font.Style, emSize, new Point(5, 5), StringFormat.GenericDefault);
g.FillPath(Brushes.Red, path);
}
}
g.SmoothingMode = SmoothingMode.Default;
}
private static void RunNative(Image image, Font font)
{
// create memory buffer from desktop handle that supports alpha channel
IntPtr dib;
var memoryHdc = CreateMemoryHdc(IntPtr.Zero, image.Width, image.Height, out dib);
try
{
// execute GDI text rendering
var hFont = font.ToHfont();
SelectObject(memoryHdc, hFont);
SetTextColor(memoryHdc, ( Color.Red.B & 0xFF ) << 16 | ( Color.Red.G & 0xFF ) << 8 | Color.Red.R);
for(int i = 0; i < RenderIterations; i++)
{
TextOut(memoryHdc, 5, 5, TestString, TestString.Length);
}
// 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);
}
DeleteObject(hFont);
}
finally
{
// release memory buffer
DeleteObject(dib);
DeleteDC(memoryHdc);
}
}
#endregion
#region Draw helper methods
public static Size MeasureStringNative(string str, Font font, Graphics g)
{
var size = new Size();
var hdc = g.GetHdc();
var hFont = font.ToHfont();
SelectObject(hdc, hFont);
GetTextExtentPoint32(hdc, str, str.Length, ref size);
DeleteObject(hFont);
g.ReleaseHdc(hdc);
return size;
}
public static Size MeasureString(string str, Font font, Graphics g, StringFormat format)
{
var characterRanges = new CharacterRange[1];
characterRanges[0] = new CharacterRange(0, str.Length);
var stringFormat = new StringFormat(format);
stringFormat.FormatFlags = StringFormatFlags.NoClip | StringFormatFlags.MeasureTrailingSpaces;
stringFormat.SetMeasurableCharacterRanges(characterRanges);
var size = g.MeasureCharacterRanges(str, font, RectangleF.Empty, stringFormat)[0].GetBounds(g).Size;
return new Size((int)Math.Round(size.Width), (int)Math.Round(size.Height));
}
private static void DrawNativeString(string text, Image image, int x, int y, Font font)
{
// adjust for native offset
x += (int)( _font1.GetHeight()/6f );
IntPtr dib;
var memoryHdc = CreateMemoryHdc(IntPtr.Zero, image.Width, image.Height, out dib);
try
{
using(var imageGraphics = Graphics.FromImage(image))
{
// copy image background to memory HDC so when copied back it will have the proper background
using(var memGraphics = Graphics.FromHdc(memoryHdc))
memGraphics.DrawImageUnscaled(image, 0, 0);
// execute GDI text rendering
var hFont = font.ToHfont();
SelectObject(memoryHdc, hFont);
SetTextColor(memoryHdc, ( Color.Red.B & 0xFF ) << 16 | ( Color.Red.G & 0xFF ) << 8 | Color.Red.R);
TextOut(memoryHdc, x, y, text, text.Length);
DeleteObject(hFont);
// copy from memory buffer to image
var imgHdc = imageGraphics.GetHdc();
BitBlt(imgHdc, x, y, image.Width, (int)( _font1.GetHeight() + 5 ), memoryHdc, x, y, 0x00CC0020);
imageGraphics.ReleaseHdc(imgHdc);
}
}
finally
{
// release memory buffer
DeleteObject(dib);
DeleteDC(memoryHdc);
}
}
private static void DrawGraphicsPathString(string text, int x, int y, Graphics g)
{
float emSize = g.DpiY*_font1.Size/72f;
using(var path = new GraphicsPath())
{
path.AddString(text, _font1.FontFamily, (int)_font1.Style, emSize, new Point(x, y), StringFormat.GenericDefault);
g.FillPath(Brushes.Red, path);
}
}
#endregion
#region GDI Interop
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 int SetTextColor(IntPtr hdc, int color);
[DllImport("gdi32.dll", EntryPoint = "GetTextExtentPoint32W")]
public static extern int GetTextExtentPoint32(IntPtr hdc, [MarshalAs(UnmanagedType.LPWStr)] string str, int len, ref Size size);
[DllImport("gdi32.dll", EntryPoint = "TextOutW")]
private static extern bool TextOut(IntPtr hdc, int x, int y, [MarshalAs(UnmanagedType.LPWStr)] string str, int len);
[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, long 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;
}
#endregion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment