在位图上绘制长字符串会导致绘制问题

Ale*_* M. 4 c# text bitmap

我正在将一个长字符串绘制到位图(通话超过一百万个字符),包括\r\n由a编写的多行字符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;
    }
Run Code Online (Sandbox Code Playgroud)

通常,它可以按预期工作-但仅用于单个字符时。但是,问题出在使用多个字符时。简而言之,当画在图像上时,多余的线条在水平和垂直方向上都会出现。

此处演示了此效果,放大了1:1(请参阅https://i.imgur.com/ITGA9WN.png):

图片的左上角

但是,我可以在Notepad ++中仅使用字符串的输出来呈现相同的文本,并且基本上是如预期的那样:

文字的左上角

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

那么程序如何以及为什么用那些“额外”的行渲染位图呢?

Jim*_*imi 5

当使用具有固定大小的Font的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();
Run Code Online (Sandbox Code Playgroud)

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

作为一个说明,无论是TextRederer.MeasureTextGraphics.DrawString每MSDN文档,应该被用来测量单个文本行。
无论如何,如果由多行组成的文本由换行符分隔,则还可以正确测量文本。
它很容易测试,使用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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

在此处输入图片说明

使用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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

在此处输入图片说明

            Graphics.DrawString()                    TextRenderer.DrawText()
Run Code Online (Sandbox Code Playgroud)


另一种方法,使用 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();
    }
}
Run Code Online (Sandbox Code Playgroud)


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

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

从WayBack Machine上发现的,来自Microsoft的晦涩文档中:

GDI +文本,分辨率独立性和渲染方法。

网格拟合,也称为提示,是调整渲染字形中像素位置以使字形在较小尺寸下易于辨认的过程。技术包括在整个像素上对齐字形茎,并确保字形的相似特征受到同等影响。

为了弥补网格拟合的不足,试图使文本达到最佳外观,对印刷跟踪(通常称为字母间距)进行了修改。

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

  1. 允许该行最多收缩一个em,而字形间距不变。
  2. 剩余的收缩是通过增加单词之间的任何空格的宽度(最大为两倍)来弥补的。
  3. 剩余的收缩是通过在字形之间引入空白像素来弥补的。

这种“努力”似乎已被推到修改字形紧缩对的地步。

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

基于透明类型呈现的TextRenderer GDI方法(旨在在屏幕上视觉呈现文本)使用字形的子像素表示。字母间距的计算完全不同。

Microsoft ClearType概述。

ClearType通过访问LCD屏幕的每个像素中的各个垂直色带元素来工作。在使用ClearType之前,计算机可以显示的最小细节级别为单个像素,但是在LCD监视器上运行ClearType的情况下,我们现在可以显示宽度仅为像素的一小部分的文本。
额外的分辨率提高了文本显示中微小细节的清晰度,使长时间阅读变得更加容易。

缺点是这种计算字母间距的方法使其不适合从WinForms打印。MSDN文档反复说明了这一点。

关于此主题的其他有趣资源:

开发的艺术-文本呈现方法比较或GDI与GDI +

为什么我的文字在GDI +和GDI中看起来不同?

GDI与GDI +文本渲染性能

所以答案:

为什么Graphics.MeasureString()返回的数字比预期的高?

在System.Drawing.Graphics.DrawString()中修改字距调整

用于生成ASCII文字的应用程序:

SourceForge上的ASCII生成器2(免费软件)