将长字符串绘制到位图会导致绘图问题

时间:2018-04-20 14:17:09

标签: c# text bitmap

我正在使用长字符串绘制位图(超过一百万个字符),包括由\r\n编写的多行字符StringBuilder

我的文本到位图代码如下:

    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(参见https://i.imgur.com/ITGA9WN.png):

Top left corner of image

但是,我可以在Notepad ++中使用字符串的输出呈现相同的文本,它基本上与预期一致:

Top left corner of text

我可以在任何其他文本查看器中查看它;结果将是相同的。

那么如何以及为什么用这些“额外”线渲染Bitmap的程序呢?

1 个答案:

答案 0 :(得分:4)

使用带有固定大小字体的Graphics.DrawString()绘制字符串(ASCII或Unicode编码符号形式)时,生成的图形似乎会生成一种网格,从而降低了渲染的视觉质量。

解决方案是使用TextRenderer.MeasureText()TextRenderer.DrawText()将GDI + Graphics方法替换为GDI方法。

用于纠正问题并重现问题的示例代码。

使用默认的本地CodePage编码加载文本文件。源文本已保存,没有任何Unicode编码。如果使用了不同的编码,Encoding.Default必须替换为实际编码(例如Encoding.UnicodeEncoding.UTF8 ...)。

用于所有测试的固定大小字体为Lucida Console, 4em Regular 其他通常可用的候选人是ConsolasCourier 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;

using (FileStream stream = new FileStream(openfile.FileName, FileMode.Open, FileAccess.Read, FileShare.None))
using (StreamReader reader = new StreamReader(stream, Encoding.Default))
    TextInput = reader.ReadToEnd();

Font font = new Font("Lucida Console", 4, FontStyle.Regular, GraphicsUnit.Point);

using (Bitmap bitmap = ASCIIArtBitmap(TextInput, font))
    bitmap.Save(@"[FilePath1]", ImageFormat.Png);

using (Bitmap bitmap = ASCIIArtBitmapGdiPlus(TextInput, font))
    bitmap.Save(@"[FilePath2]", ImageFormat.Png);

using (Bitmap bitmap = ASCIIArtBitmapGdiPlusPath(TextInput, font))
    bitmap.Save(@"[FilePath3]", ImageFormat.Png);

font.Dispose();

TextRenderer首先用于消除所报告的视觉缺陷。

请注意,根据MSDN文档,TextRederer.MeasureTextGraphics.DrawString都应用于衡量单行文字。
无论如何,还知道当文本由多行组成时,如果换行将这些行分开,则正确测量文本。
它可以很容易地测试,将源文本与Enviroment.Newline分隔为分隔符,并将行数乘以单行的高度。结果总是一样的。

private Bitmap ASCIIArtBitmap(string text, Font font)
{
    TextFormatFlags flags = TextFormatFlags.Top | TextFormatFlags.Left | 
                            TextFormatFlags.NoPadding | TextFormatFlags.NoClipping;

    Size BitmapSize = TextRenderer.MeasureText(text, font, Size.Empty, flags);

    using (Bitmap bitmap = new Bitmap(BitmapSize.Width, BitmapSize.Height, PixelFormat.Format24bppRgb))
    using (Graphics graphics = Graphics.FromImage(bitmap))
    {
        BitmapSize = TextRenderer.MeasureText(graphics, text, font, new Size(bitmap.Width, bitmap.Height), flags);
        TextRenderer.DrawText(graphics, text, font, Point.Empty, Color.Black, Color.White, flags);
        return (Bitmap)bitmap.Clone();
    }
}

低分辨率渲染,(150 x 55个字符)。没有可见的网格效果

enter image description here

使用Graphics.DrawString()重现报告的行为。

指定

TextRenderingHint.AntiAlias,以减少视觉缺陷。 CompositingQuality.HighSpeed似乎不合适,但在这种情况下,它实际上比HighQuality更好 TextContrast = 1会使得到的图像变暗。默认设置太亮并且丢失了细节(不过我的意见)。

private Bitmap ASCIIArtBitmapGdiPlus(string text, Font font)
{
    using (Bitmap modelbitmap = new Bitmap(10, 10, PixelFormat.Format24bppRgb))
    using (Graphics modelgraphics = Graphics.FromImage(modelbitmap))
    {
        modelgraphics.TextRenderingHint = TextRenderingHint.AntiAlias;
        SizeF BitmapSize = modelgraphics.MeasureString(text, font, Point.Empty, StringFormat.GenericTypographic);

        using (Bitmap bitmap = new Bitmap((int)BitmapSize.Width, (int)BitmapSize.Height, PixelFormat.Format24bppRgb))
        using (Graphics graphics = Graphics.FromImage(bitmap))
        {
            graphics.Clear(Color.White);
            graphics.TextRenderingHint = TextRenderingHint.AntiAlias;
            graphics.CompositingQuality = CompositingQuality.HighSpeed;
            graphics.PixelOffsetMode = PixelOffsetMode.HighSpeed;
            graphics.TextContrast = 1;
            graphics.DrawString(text, font, Brushes.Black, PointF.Empty, StringFormat.GenericTypographic);
            return (Bitmap)bitmap.Clone();
        }
    }
}

中低分辨率(300 x 110个字符),网格效果可见。

enter image description here

            Graphics.DrawString()                    TextRenderer.DrawText()


另一种方法,使用GraphicsPath.AddString()

生成的位图稍微好一些,但无论如何都会出现网格效应 可以真正注意到的是速度的差异。所有其他测试方法GraphicsPath 慢一点

private Bitmap ASCIIArtBitmapGdiPlusPath(string text, Font font)
{
    GraphicsPath GPath = new GraphicsPath(FillMode.Alternate);
    GPath.AddString(text, font.FontFamily, (int)font.Style, 4, Point.Empty, StringFormat.GenericTypographic);

    Rectangle GPathArea = Rectangle.Round(GPath.GetBounds());

    using (Bitmap bitmap = new Bitmap(GPathArea.Width, GPathArea.Height))
    using (Graphics graphics = Graphics.FromImage(bitmap))
    {
        graphics.Clear(Color.White);
        graphics.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;
        graphics.SmoothingMode = SmoothingMode.HighQuality;
        graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;

        graphics.FillPath(Brushes.Black, GPath);
        return (Bitmap)bitmap.Clone();
    }
}


为什么渲染质量在这种情况下是如此不同?

所有这些都取决于GDI +与分辨率无关的网格拟合渲染的性质。

来自WayBack Machine上发现的微软文档的一篇晦涩难懂的文件:

GDI+ Text, Resolution Independence, and Rendering Methods.

  

网格拟合,也称为提示,是调整网格的过程   渲染字形中像素的位置,以便轻松制作字形   在较小的尺寸清晰可辨。技术包括对齐字形词干   整个像素并确保字形的相似特征受到影响   同样。

为了补偿网格拟合,尝试实现文本的最佳外观,印刷跟踪(通常称为letter-spacing)被修改。

  

当GDI +显示一行网格拟合的字形时,短于   它们的设计宽度遵循以下一般规则:

     
      
  1. 允许该行在不改变字形间距的情况下收缩直至em。
  2.   
  3. 通过增加单词之间任何空格的宽度来弥补剩余的收缩,最多加倍。
  4.   
  5. 通过在字形之间引入空白像素来弥补剩余的收缩。
  6.   

这个"努力"似乎被推到了修改kerning对字形的点。

在比例字体中,视觉渲染有一个好处,但是使用固定大小的字体,前面提到的计算会产生一种网格对齐,当相同的符号重复多次时清晰可见。 / p>

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

回答:

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)