当使用具有固定大小字体的Graphics.DrawString() 绘制字符串(ASCII 或某种形式的 Unicode 编码符号)时,生成的图形似乎会生成一种网格,从而降低渲染的视觉质量。
一种解决方案是用 GDI 方法替换 GDI+ 图形方法,使用 TextRenderer.MeasureText() 和 TextRenderer.DrawText()。
纠正问题并重现问题的示例代码。
使用默认的本地 CodePage 编码加载文本文件。源文本已在没有任何 Unicode 编码的情况下保存。如果使用不同的编码,Encoding.Default 必须替换为实际的编码(例如Encoding.Unicode、Encoding.UTF8...)。
用于所有测试的固定大小字体是Lucida Console, 4em Regular。
通常可用的其他候选人是Consolas 和Courier New。
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Drawing.Text;
using System.IO;
using System.Text;
using System.Windows.Forms;
// Read the input text - assume UTF8 Encoding
string text = File.ReadAllText([Source text Path], Encoding.UTF8);
Font font = new Font("Lucida Console", 4, FontStyle.Regular, GraphicsUnit.Point);
// Use TextRenderer
using (var bitmap = ASCIIArtBitmap(text, font))
bitmap.Save(@"[FilePath1]", ImageFormat.Png);
// Use GDI+ Graphics
using (var bitmap = ASCIIArtBitmapGdiPlus(text, font))
bitmap.Save(@"[FilePath2]", ImageFormat.Png);
// Use GraphicsPath
using (var bitmap = ASCIIArtBitmapGdiPlusPath(text, font))
bitmap.Save(@"[FilePath3]", ImageFormat.Png);
font.Dispose();
TextRenderer首先用于消除报告的视觉缺陷。
请注意,根据 MSDN 文档,TextRederer.MeasureText() 和 Graphics.DrawString() 都应该用于测量单行文本。
无论如何,当文本由多行组成时,如果换行符将这些行分开,则文本被正确测量也是众所周知的。
它可以很容易地测试,将源文本以Environment.Newline 作为分隔符并将行数乘以单行的高度。结果总是一样的。
private Bitmap ASCIIArtBitmap(string text, Font font)
{
var flags = TextFormatFlags.Top | TextFormatFlags.Left |
TextFormatFlags.NoPadding | TextFormatFlags.NoClipping;
Size bitmapSize = TextRenderer.MeasureText(text, font, Size.Empty, flags);
var bitmap = new Bitmap(bitmapSize.Width, bitmapSize.Height, PixelFormat.Format24bppRgb)
using (var g = Graphics.FromImage(bitmap)) {
bitmapSize = TextRenderer.MeasureText(g, text, font, new Size(bitmap.Width, bitmap.Height), flags);
TextRenderer.DrawText(g, text, font, Point.Empty, Color.Black, Color.White, flags);
return bitmap;
}
}
低分辨率渲染,(150 x 55 字符)。没有可见的网格效果。
使用Graphics.DrawString(),重现报告的行为。
TextRenderingHint.AntiAlias 被指定,以减少视觉缺陷。 CompositingQuality.HighSpeed 似乎不合适,但实际上,在这种情况下,它的渲染效果比 HighQuality 更好。
TextContrast = 1 使生成的图像更暗一些。默认设置太亮并且丢失细节(不过我的看法)。
private Bitmap ASCIIArtBitmapGdiPlus(string text, Font font)
{
using (var modelbitmap = new Bitmap(10, 10, PixelFormat.Format24bppRgb))
using (var modelgraphics = Graphics.FromImage(modelbitmap))
{
modelgraphics.TextRenderingHint = TextRenderingHint.AntiAlias;
SizeF bitmapSize = modelgraphics.MeasureString(text, font, Point.Empty, StringFormat.GenericTypographic);
var bitmap = new Bitmap((int)bitmapSize.Width, (int)bitmapSize.Height, PixelFormat.Format24bppRgb);
using (var g = Graphics.FromImage(bitmap))
{
g.Clear(Color.White);
g.TextRenderingHint = TextRenderingHint.AntiAlias;
g.CompositingQuality = CompositingQuality.HighSpeed;
g.TextContrast = 1;
g.DrawString(text, font, Brushes.Black, PointF.Empty, StringFormat.GenericTypographic);
return bitmap;
}
}
}
中低分辨率(300 x 110 字符),网格效果可见。
Graphics.DrawString() TextRenderer.DrawText()
另一种方法,使用GraphicsPath.AddString()
生成的位图稍微好一些,但网格效果还是有的。
真正可以注意到的是速度的差异。 GraphicsPath 比所有其他测试方法慢。
private Bitmap ASCIIArtBitmapGdiPlusPath(string text, Font font)
{
using (var path = new GraphicsPath(FillMode.Alternate)) {
path.AddString(text, font.FontFamily, (int)font.Style, 4, Point.Empty, StringFormat.GenericTypographic);
var gpRect = Rectangle.Round(path.GetBounds());
var bitmap = new Bitmap(gpRect.Width, gpRect.Height);
using (var g = Graphics.FromImage(bitmap)) {
g.Clear(Color.White);
g.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;
g.SmoothingMode = SmoothingMode.HighQuality;
g.PixelOffsetMode = PixelOffsetMode.Half;
g.FillPath(Brushes.Black, path);
return bitmap;
}
}
}
为什么在这种情况下渲染质量如此不同?
一切都取决于 GDI+ 分辨率无关的网格拟合渲染的性质。
来自于 WayBack Machine 上的一份不知名的 Microsoft 文档:
GDI+ Text, Resolution Independence, and Rendering Methods.
Grid Fitting,也称为hinting,是调整网格的过程
渲染字形中像素的位置以轻松制作字形
在较小的尺寸下清晰易读。技术包括对齐字形词干
整个像素并确保字形的相似特征受到影响
平等。
为了补偿 Grid Fitting,试图为文本实现最佳外观,修改了印刷跟踪(通常称为 letter-spacing)。
当 GDI+ 显示一排比
它们的设计宽度,它遵循以下一般规则:
- 在不改变字形间距的情况下,该行最多可以收缩
em。
- 剩余的收缩是通过增加单词之间任何空格的宽度来弥补的,最多加倍。
- 剩余的收缩是通过在字形之间引入空白像素来弥补的。
这种“努力”似乎被推到了修改kerning字形对的地步。
在比例字体中,视觉渲染有好处,但是对于固定大小的字体,前面提到的计算会产生一种网格对齐,当同一个符号重复多次时清晰可见。
TextRenderer GDI 方法,基于清晰类型的渲染 - 旨在在屏幕上视觉渲染文本 - 使用字形的亚像素表示。字母间距的计算完全不同。
Microsoft ClearType overview.
ClearType 通过访问单个垂直颜色条纹来工作
LCD 屏幕的每个像素中的元素。在 ClearType 之前,
计算机可以显示的最小细节层次是单个
像素,但是在 LCD 显示器上运行 ClearType,我们现在可以
显示文本的特征,其宽度只有像素的几分之一。
额外的分辨率增加了细节的清晰度
文本显示,使其更易于长时间阅读。
缺点是这种计算字母间距的方法不适合从 WinForms 打印。 MSDN 文档反复声明了这一点。
关于该主题的其他有趣资源:
The Art of dev - Text rendering methods comparison or GDI vs. GDI+
Why does my text look different in GDI+ and in GDI?
GDI vs. GDI+ Text Rendering Performance
StackOverflow 答案:
Why is Graphics.MeasureString() returning a higher than expected number?
Modifying the kerning in System.Drawing.Graphics.DrawString()
用于生成 ASCII 艺术文本的应用程序:
ASCII Generator 2 on SourceForge (free software)