在位图上绘制长字符串会导致绘图问题
Drawing a Long String on to a Bitmap results in Drawing Issues
我正在将一个长字符串绘制到位图(超过一百万个字符),包括多行字符 \r\n
,由 StringBuilder
.
编写
我的Text to Bitmap代码如下:
public static Bitmap GetBitmap(string input, Font inputFont,
Color bmpForeground, Color bmpBackground) {
Image bmpText = new Bitmap(1, 1);
try {
// Create a graphics object from the image.
Graphics g = Graphics.FromImage(bmpText);
// Measure the size of the text when applied to image.
SizeF inputSize = g.MeasureString(input, inputFont);
// Create a new bitmap with the size of the text.
bmpText = new Bitmap((int)inputSize.Width,
(int)inputSize.Height);
// Instantiate graphics object, again, since our bitmap
// was modified.
g = Graphics.FromImage(bmpText);
// Draw a background to the image.
g.FillRectangle(new Pen(bmpBackground).Brush,
new Rectangle(0, 0,
Convert.ToInt32(inputSize.Width),
Convert.ToInt32(inputSize.Height)));
// Draw the text to the image.
g.DrawString(input, inputFont,
new Pen(bmpForeground).Brush, new PointF(0, 0));
} catch {
// Draw a blank image with background.
Graphics.FromImage(bmpText).FillRectangle(
new Pen(bmpBackground).Brush,
new Rectangle(0, 0, 1, 1));
}
return (Bitmap)bmpText;
}
通常情况下,它会按预期工作 -- 但仅当用于单个字符时。但是,问题出在使用多个字符时。简单地说,在图像上绘制时,额外的线条会在垂直和水平方向上出现。
这里演示了这个效果,放大了1:1(见full Image):
但是,我可以在 Notepad++ 中仅使用字符串输出来呈现相同的文本,这基本上符合预期:
我可以在任何其他文本查看器中查看;结果是一样的。
那么程序如何以及为什么使用那些 'extra' 行渲染位图?
当使用 Graphics.DrawString() 和固定大小的字体绘制字符串(ASCII 或一种 Unicode 编码符号)时,生成的图形似乎生成了一种网格,降低了视觉质量渲染。
一个解决方案是用 GDI 方法替换 GDI+ 图形方法,使用 TextRenderer.MeasureText() and TextRenderer.DrawText()。
纠正问题并重现问题的示例代码。
使用默认的本地代码页编码加载文本文件。源文本已在没有任何 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 is specified, as an effort to reduce the visual defect. 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, also known as hinting, is the process of adjusting the
position of pixels in a rendered glyph to make the glyph easily
legible at smaller sizes. Techniques include aligning glyph stems on
whole pixels and ensuring similar features of a glyph are affected
equally.
为了补偿网格拟合,尝试为文本实现最佳外观,修改了排版跟踪(通常称为 letter-spacing)。
When GDI+ displays a line of grid fitted glyphs that are shorter than
their design width, it follows these general rules:
- The line is allowed to contract by up to an
em
without any change of glyph spacing.
- Remaining contraction is made up by increasing the width of any spaces between words, to a maximum of doubling.
- Remaining contraction is made up by introducing blanks pixels between glyphs.
这种“努力”似乎被推到了修改 kerning 对字形的地步。
在比例字体中,视觉渲染有好处,但是对于固定大小的字体,前面提到的计算会产生一种网格对齐,当相同的符号重复多次时清晰可见。
TextRenderer GDI 方法,基于清晰类型呈现 - 旨在 屏幕上文本的视觉呈现 - 使用字形的亚像素表示。 letter-spacing的计算完全不同
ClearType works by accessing the individual vertical color stripe
elements in every pixel of an LCD screen. Before ClearType, the
smallest level of detail that a computer could display was a single
pixel, but with ClearType running on an LCD monitor, we can now
display features of text as small as a fraction of a pixel in width.
The extra resolution increases the sharpness of the tiny details in
text display, making it much easier to read over long durations.
缺点是这种计算字母间距的方法不适合从 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
Whosebug 回答:
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)
我正在将一个长字符串绘制到位图(超过一百万个字符),包括多行字符 \r\n
,由 StringBuilder
.
我的Text to Bitmap代码如下:
public static Bitmap GetBitmap(string input, Font inputFont,
Color bmpForeground, Color bmpBackground) {
Image bmpText = new Bitmap(1, 1);
try {
// Create a graphics object from the image.
Graphics g = Graphics.FromImage(bmpText);
// Measure the size of the text when applied to image.
SizeF inputSize = g.MeasureString(input, inputFont);
// Create a new bitmap with the size of the text.
bmpText = new Bitmap((int)inputSize.Width,
(int)inputSize.Height);
// Instantiate graphics object, again, since our bitmap
// was modified.
g = Graphics.FromImage(bmpText);
// Draw a background to the image.
g.FillRectangle(new Pen(bmpBackground).Brush,
new Rectangle(0, 0,
Convert.ToInt32(inputSize.Width),
Convert.ToInt32(inputSize.Height)));
// Draw the text to the image.
g.DrawString(input, inputFont,
new Pen(bmpForeground).Brush, new PointF(0, 0));
} catch {
// Draw a blank image with background.
Graphics.FromImage(bmpText).FillRectangle(
new Pen(bmpBackground).Brush,
new Rectangle(0, 0, 1, 1));
}
return (Bitmap)bmpText;
}
通常情况下,它会按预期工作 -- 但仅当用于单个字符时。但是,问题出在使用多个字符时。简单地说,在图像上绘制时,额外的线条会在垂直和水平方向上出现。
这里演示了这个效果,放大了1:1(见full Image):
但是,我可以在 Notepad++ 中仅使用字符串输出来呈现相同的文本,这基本上符合预期:
我可以在任何其他文本查看器中查看;结果是一样的。
那么程序如何以及为什么使用那些 'extra' 行渲染位图?
当使用 Graphics.DrawString() 和固定大小的字体绘制字符串(ASCII 或一种 Unicode 编码符号)时,生成的图形似乎生成了一种网格,降低了视觉质量渲染。
一个解决方案是用 GDI 方法替换 GDI+ 图形方法,使用 TextRenderer.MeasureText() and TextRenderer.DrawText()。
纠正问题并重现问题的示例代码。
使用默认的本地代码页编码加载文本文件。源文本已在没有任何 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 is specified, as an effort to reduce the visual defect. 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, also known as hinting, is the process of adjusting the position of pixels in a rendered glyph to make the glyph easily legible at smaller sizes. Techniques include aligning glyph stems on whole pixels and ensuring similar features of a glyph are affected equally.
为了补偿网格拟合,尝试为文本实现最佳外观,修改了排版跟踪(通常称为 letter-spacing)。
When GDI+ displays a line of grid fitted glyphs that are shorter than their design width, it follows these general rules:
- The line is allowed to contract by up to an
em
without any change of glyph spacing.- Remaining contraction is made up by increasing the width of any spaces between words, to a maximum of doubling.
- Remaining contraction is made up by introducing blanks pixels between glyphs.
这种“努力”似乎被推到了修改 kerning 对字形的地步。
在比例字体中,视觉渲染有好处,但是对于固定大小的字体,前面提到的计算会产生一种网格对齐,当相同的符号重复多次时清晰可见。
TextRenderer GDI 方法,基于清晰类型呈现 - 旨在 屏幕上文本的视觉呈现 - 使用字形的亚像素表示。 letter-spacing的计算完全不同
ClearType works by accessing the individual vertical color stripe elements in every pixel of an LCD screen. Before ClearType, the smallest level of detail that a computer could display was a single pixel, but with ClearType running on an LCD monitor, we can now display features of text as small as a fraction of a pixel in width.
The extra resolution increases the sharpness of the tiny details in text display, making it much easier to read over long durations.
缺点是这种计算字母间距的方法不适合从 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
Whosebug 回答:
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)