代码之家  ›  专栏  ›  技术社区  ›  Ally

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

  •  4
  • Ally  · 技术社区  · 6 年前

    我正在为位图绘制一个长字符串(超过一百万个字符),包括多行字符 \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(请参见 full Image ):

    Top left corner of image

    但是,我可以在记事本++中仅使用字符串的输出呈现相同的文本,这与预期基本相同:

    Top left corner of text

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

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

    1 回复  |  直到 4 年前
        1
  •  7
  •   Jimi    3 年前

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

    解决方案是用GDI方法替换GDI+图形方法,使用 TextRenderer.MeasureText() 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 首先用于消除报告的视觉缺陷。

    值得注意的是 TextRederer.MeasureText() Graphics.DrawString() ,根据MSDN文档,应用于测量单行文本。
    无论如何,如果换行符将多行分隔开,那么当文本由多行组成时,也可以正确测量文本。
    它可以很容易地进行测试,将源文本拆分为 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个字符)。无可见栅格效果

    enter image description here

    使用 图样抽绳() ,以复制报告的行为。

    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字符) ,栅格效果可见。

    enter image description here

               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机器上发现的一份来源于微软的模糊文档中:

    GDI+ Text, Resolution Independence, and Rendering Methods.

    网格拟合,也称为提示,是调整 渲染图示符中像素的位置,以轻松创建图示符 尺寸较小时清晰可见。技术包括在屏幕上对齐字形柄 整个像素并确保图示符的类似特征受到影响 同样地。

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

    当GDI+显示一行短于 其设计宽度遵循以下一般规则:

    1. 该线路最多可通过 em 不更改图示符间距。
    2. 剩余的收缩是通过增加单词之间任何空格的宽度来弥补的,最大值为两倍。
    3. 剩余的收缩通过在轮廓之间引入空白像素来弥补。

    这种“努力”似乎被推到了修改 kerning 图示符对。

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

    TextRenderer GDI方法,基于清除类型渲染-针对 屏幕上文本的视觉呈现 -使用图示符的亚像素表示。字母间距的计算完全不同。

    Microsoft ClearType overview.

    ClearType通过访问单个垂直色条工作 LCD屏幕每个像素中的元素。在ClearType之前 计算机能够显示的最小细节级别是单个 像素,但随着ClearType在LCD显示器上运行,我们现在可以 显示宽度仅为一个像素分数的文本特征。
    额外的分辨率提高了图像中细微细节的清晰度 文本显示,使长时间阅读更加容易。

    缺点是,这种计算字母间距的方法不适合从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)