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

2024-04-12

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

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

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

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


使用以下命令绘制字符串(ASCII 或 Unicode 编码符号的形式)时Graphics.DrawString() https://learn.microsoft.com/en-us/dotnet/api/system.drawing.graphics.drawstring使用固定大小的字体,生成的图形似乎会生成某种网格,从而降低渲染的视觉质量。

解决方案是用 GDI 方法替换 GDI+ Graphics 方法,使用TextRenderer.MeasureText() https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.textrenderer.measuretext and TextRenderer.DrawText() https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.textrenderer.drawtext.

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

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

用于所有测试的固定大小字体是Lucida Console, 4em Regular.
通常可用的其他候选人是Consolas and 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() and 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 字符)。看不到网格效果.

Using Graphics.DrawString(),重现所报告的行为。

TextRenderingHint.AntiAlias https://learn.microsoft.com/en-us/dotnet/api/system.drawing.text.textrenderinghint被指定为减少视觉缺陷的努力。合成质量.高速 https://learn.microsoft.com/en-us/dotnet/api/system.drawing.drawing2d.compositingquality看起来不合适,但实际上,在这种情况下,它的渲染效果比HighQuality.
文字对比 https://learn.microsoft.com/en-us/dotnet/api/system.drawing.graphics.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() https://learn.microsoft.com/en-us/dotnet/api/system.drawing.drawing2d.graphicspath.addstring

生成的位图稍微好一点,但网格效果仍然存在。
真正可以注意到的是速度的差异。图形路径 https://learn.microsoft.com/en-us/dotnet/api/system.drawing.drawing2d.graphicspath is 慢得多比所有其他测试方法都要好。

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;  // Not used
            g.PixelOffsetMode = PixelOffsetMode.Half;   // <= for the Bitmap
            g.SmoothingMode = SmoothingMode.AntiAlias;  // <= for the GraphicsPath
            g.FillPath(Brushes.Black, path);
            return bitmap;
        }
    }
}

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

一切都取决于 GDI+ 分辨率无关的网格拟合渲染的性质。

来自微软起源的一份不起眼的文档,在 WayBack Machine 上找到:

GDI+ 文本、分辨率独立性和渲染方法。 http://web.archive.org/web/20100116044507/http://windowsclient.net/articles/gdiptext.aspx

网格拟合,也称为提示,是调整网格的过程 渲染字形中像素的位置以使字形更容易 在较小的尺寸下也清晰可见。技术包括将字形词干对齐 整个像素并确保字形的相似特征受到影响 一样。

为了补偿网格拟合,尝试实现文本的最佳外观,印刷跟踪(通常称为字母间距 https://en.wikipedia.org/wiki/Letter-spacing),已修改。

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

  1. 该线路允许收缩最多em字形间距没有任何变化。
  2. 剩余的收缩是通过将单词之间的任何空格的宽度增加到最大两倍来弥补的。
  3. 剩余的收缩是通过在字形之间引入空白像素来弥补的。

这种“努力”似乎已经到了修改kerning https://en.wikipedia.org/wiki/Kerning字形对。

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

TextRenderer GDI 方法,基于清晰类型渲染 - 旨在屏幕上文本的视觉呈现- 使用字形的子像素表示。字母间距的计算完全不同。

Microsoft ClearType 概述。 https://learn.microsoft.com/en-us/typography/cleartype/

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

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

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

开发的艺术 - 文本渲染方法比较或 GDI 与 GDI+ https://theartofdev.com/2014/04/21/text-rendering-methods-comparison-or-gdi-vs-gdi-revised/

为什么我的文本在 GDI+ 和 GDI 中看起来不同? https://vinsontech.wordpress.com/2012/02/06/why-does-my-text-look-different-in-gdi-and-in-gdi/

GDI 与 GDI+ 文本渲染性能 https://blogs.msdn.microsoft.com/cjacks/2006/05/19/gdi-vs-gdi-text-rendering-performance/

堆栈溢出答案:

为什么 Graphics.MeasureString() 返回的数字高于预期数字? https://stackoverflow.com/questions/1203087/why-is-graphics-measurestring-returning-a-higher-than-expected-number

修改 System.Drawing.Graphics.DrawString() 中的字距调整 https://stackoverflow.com/questions/3235103/modifying-the-kerning-in-system-drawing-graphics-drawstring

用于生成 ASCII 艺术文本的应用程序:
SourceForge 上的 ASCII Generator 2(免费软件) https://ascgendotnet.jmsoftware.co.uk/

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

在位图上绘制长字符串会导致绘图问题 的相关文章

  • 信号与信号2

    我的应用程序可能会受益于使用 boost 的信号库之一而不是本土解决方案 该应用程序是多线程的 但执行信号处理的部分是单线程的 如果多线程不是问题 是否有任何理由更喜欢 Boost Signals2 而不是 Boost Signal Boo
  • Winform DatagridView 数字列排序

    我只使用一个简单的 DataGridView 来保存一堆数据 有趣的是 我在特定列中有小数 但是当按小数列排序时 它的排序是错误的 例如 起始顺序可能是 0 56 3 45 500 89 20078 90 1 56 100 29 2 39
  • 预编译头和 Visual Studio

    有没有办法设置 Visual Studio 解决方案参数 以便它只创建预编译头而不构建整个解决方案 具体来说 它是一个巨大的 C 解决方案 本身有许多项目 谢谢 仅选择 pch 创建者源文件 通常是 stdafx cpp 然后编译该文件 C
  • 无法将参数从 `const char *` 转换为 `char *`

    鉴于此代码 void group build int size std string ips Build the LL after receiving the member list from bootstrap head new memb
  • 选择initializer_list迭代器定义

    Why std initializer list
  • 为什么像 BindingList 或 ObservableCollection 这样的类不是线程安全的?

    我一次又一次发现自己必须编写 BindingList 和 ObservableCollection 的线程安全版本 因为当绑定到 UI 时 这些控件无法从多个线程更改 我想理解的是why情况就是这样 这是设计错误还是故意的 问题是设计一个线
  • 在 .NET Core 中从 HttpResponseMessage 转换为 IActionResult

    我正在将之前在 NET Framework 中编写的一些代码移植到 NET Core 我有这样的事情 HttpResponseMessage result await client SendAync request if result St
  • C# 中的抽象类和接口类有什么不同?

    C 中的抽象类和接口类有什么不同 An 接口不是类 它只是一个contract定义了public一个类的成员must实施 抽象类只是一个类 您从中可以cannot创建一个实例 通常您会使用它来定义一个基类 该基类定义了一些virtual方法
  • 如何调试.NET Windows Service OnStart方法?

    我用 NET 编写的代码仅在作为 Windows 服务安装时才会失败 该故障甚至不允许服务启动 我不知道如何进入 OnStart 方法 如何 调试 Windows 服务应用程序 http msdn microsoft com en us l
  • 原子存储抛出错误

    我最近升级到了 C 11 兼容编译器 并且尝试将一些代码从 boost 更新到 c 11 标准 我在使用atomic store转换一些代码时遇到了问题 这是一些简单的测试代码 似乎会引发编译器错误 int main std shared
  • 如何在 C++ 和 QML 应用程序中使用 qrc?

    我在 Windows7 上用 c qnd Qt Creator QML 编写了 Qt Quick Desktop 应用程序 现在 我必须部署它 并且我需要隐藏 qml 文件和图像 意味着 将它们放入资源等中 我读到有一个很好的方法可以使用
  • ASP.NET MVC 动作过滤器

    有谁知道即使在 CATCH 块中 ActionFilterAttribute 类的 OnResultExecuted 方法是否也会执行 ie CookiesActions public ActionResult Login Usuarios
  • 本地时间的内存需要释放吗?

    void log time t current time 0 tm ptm localtime current stuf 只是想确定 我是否需要在方法结束时释放 tm 指针分配的内存 不 你不应该释放它 该结构是静态分配的 检查文档 htt
  • 冒号在c中起什么作用?

    我在课堂上得到了这个例子 但我不确定它的作用 我知道冒号添加了一个位字段 但我仍然不确定这个问题 a b gt 0 3 1 运算符称为条件运算符 If b值为 gt 0 价值3被分配给a否则值1被分配给a 以 Kernighan Ritch
  • 如果finally 块包含await,为什么*有时*不会在ThreadAbortException 上执行?

    UPDATE 我不认为这个问题是重复的ThreadAbortException最后可以跳过吗 https stackoverflow com questions 18002668 can threadabortexception skip
  • 在特定线程上运行工作

    我想要一个特定的线程 任务队列并在该单独的线程中处理任务 应用程序将根据用户的使用情况创建任务并将其排队到任务队列中 然后单独的线程处理任务 即使队列为空 保持线程活动并使用它来处理排队任务也至关重要 我尝试过几种实现TaskSchedul
  • 如何重用具有稍微不同的 ProcessStartInfo 实例的 Process 实例?

    我有以下开始的代码robocopy https technet microsoft com en us library cc733145 aspx as a Process 我还需要进行数据库查询以确定每次需要复制哪些目录robocopy被
  • 基础设施 - 同步和异步接口和实现? [关闭]

    Closed 这个问题是基于意见的 help closed questions 目前不接受答案 在实现库 基础设施时 并且该 API 的用户希望同步和异步使用代码 我读到混合同步和异步并不是一个好主意 例如 同步实现包括等待异步实现 显然
  • 为什么 C++ 标准没有将 sizeof(bool) 定义为 1?

    Size of char signed char and unsigned char由 C 标准本身定义为 1 个字节 我想知道为什么它没有定义sizeof bool also C 03 标准 5 3 3 1 说 sizeof char s
  • 使用任务的经典永无止境的线程循环?

    给出了一个非常常见的线程场景 宣言 private Thread thread private bool isRunning false Start thread new Thread gt NeverEndingProc thread S

随机推荐

  • 使用不同的行终止符在Python中读取csv文件

    我有一个 CSV 格式的文件 其中分隔符是 ASCII 单位分隔符 行终止符是 ASCII 记录分隔符 显然 由于这些是非打印字符 我只是使用了此处编写它们的标准方法之一 我已经编写了大量读取和写入 CSV 文件的代码 因此我的问题不在于
  • Node.js ws 包上的正确错误处理

    我正在努力将基于 REST 的数据管道替换为基于 Websocket 的数据管道 但我无法找到所有可能出错的地方 该系统是生产系统 因此如果出现故障并且无法恢复 将会发生非常糟糕的情况 这是我到目前为止所得到的 客户端 let server
  • 路径组件应该是“/”

    我正在尝试创建一个FileSystem保存 ext2 文件系统的对象 我的URI似乎无效 给我一个路径组件应该是 运行时错误 我使用的是 Windows 我的项目位于 Eclipse 中 有一个名为 fs 的子目录 用于保存文件系统映像 我
  • 如何将 Zend Framework 2 集成到 Netbeans 7.2 IDE [关闭]

    就目前情况而言 这个问题不太适合我们的问答形式 我们希望答案得到事实 参考资料或专业知识的支持 但这个问题可能会引发辩论 争论 民意调查或扩展讨论 如果您觉得这个问题可以改进并可能重新开放 访问帮助中心 help reopen questi
  • Actor 中的 WebSocket.acceptWithActor 和 @Inject()(播放 2.5)

    WebSocket acceptWithActor不使用 Guice 实例化一个新的 Akka actor 在 Play 2 4 中 仍然可以通过导入来为我的 actor 使用注入器play api Play current 片段来自Rea
  • Fortran 中指数函数的 DEXP 或 EXP?

    我有两个非常简短的问题 1 我刚刚读到DEXP is the archaic的形式EXP 这是否意味着不应再使用它 我一直以为DEXP 双精度等于EXP 2 指数函数的范围是多少 它依赖于编译器吗 问题 1 在现代 Fortran 中 最好
  • 如何在magento中添加自定义模块

    您好 我想为页脚创建一个自定义模块 用于显示新闻标题 还没有使用自定义模块 我该怎么做 谁能告诉我创建自定义模块的简单步骤 Thanks 您会发现的最佳资源是 模块创建器扩展 http www magentocommerce com ext
  • 如何在android xml布局中将9个按钮放在3行中?

    我正在尝试制作一个井字游戏 android 版本 我想让所有 9 个按钮根据设备的宽度和高度自动调整大小 并将它们均匀地放在 3 3 网格中 但我现在只能设置它们的尺寸的数字 谁能告诉我如何让他们使用父母的高度和宽度并计算他们的尺寸 另外
  • 静态库 (.a) 和共享库 (.so) 之间的文件格式差异?

    我知道关于共享库和静态库的用例有很多问题 这个问题与此无关 我问的是磁盘上存储的文件格式的差异 为什么问题是 两者之间有什么区别 或者它们完全相同 只是用途不同 我相信它们是不一样的 因为在共享库上运行 nm 需要 D 标志 显然它需要做一
  • 尝试检索 Google 日历活动时 WEB_HOOK 通道不可用

    我正在尝试使用Google 日历的推送通知 https developers google com calendar v3 push 回调端点托管在 Heroku 上 应用程序名称 herokuapp com已在 Search Consol
  • 如何过滤上传对话框中可以看到哪些文件?

    在没有 ActiveXes Flash 或 Java Applet JavaScript 也可以 等客户端对象的 ASP NET MVC 应用程序中 是否可以想象当弹出上传文件对话框时 它只会显示我指定的文件 例如 在用于选择要上传的文件的
  • 如何在 django 中创建新的数据库连接

    我需要创建一个新的数据库连接 会话 以避免 django 事务中的 MySql 过程意外提交 如何在django中设置它 我尝试在设置文件中复制数据库配置 它对我有用 但似乎不是一个好的解决方案 有关更多详细信息 请参阅我的代码 class
  • javascript - 捕获语法错误并运行备用函数

    我正在尝试在 javascript 上构建一些东西 我可以有一个可以是一切的输入 像字符串 xml javascript 和 不带引号的非 JavaScript 字符串 如下 strings eval hello I am a string
  • 如何使用 LINQ 从没有主键的表中插入/更新/删除记录

    我正在使用 LINQ 连接到第三者数据库 现在我必须将一些记录插入到没有设置主键的表中 并且出现以下异常 System InvalidOperationException 无法对 表 the table 执行创建 更新或删除操作 因为它没有
  • 如何在不使用 Next() 的情况下获取 sql.Rows 的计数?

    我需要得到的长度 sql Rows before我开始 Next 循环来获取值 一种方法是通过循环 Next 两次来创建行切片 获取计数 然后循环该新切片以提取值 但这似乎效率很低 所以我希望有一种更好的方法做这个 查看文档 我没有看到任何
  • mclapply 遇到取决于核心 id 的错误?

    我有一组基因 我需要并行计算一些系数 系数在里面计算GeneTo GeneCoeffs filtered它将基因名称作为输入并返回 2 个数据框的列表 长度为 100gene array我使用不同数量的核心运行此命令 5 6 和 7 Coe
  • 从字符串中获取“$#”的所有正则表达式匹配项

    我有一个字符串 其中包含多个美元符号实例 后跟一个正数 我需要使用正则表达式获取每个实例 这是一个字符串的示例 This that 1 who 2 到目前为止 我使用 vb net 得到的结果如下 Dim wordSplitMatches
  • npm ci 命令失败并显示“无法读取未定义的属性‘@angular/animations’”

    在为我的 Angular 项目执行 docker build 时 在npm ci步骤 我收到以下错误 Cannot read property angular animations of undefined 由于没有正确的错误 我们无法找到
  • 重复AlarmManager如何启动AsyncTask?

    我通常编写这段代码来启动服务AlarmManager intent new Intent getActivity someservice class pendingNotificationIntent PendingIntent getSe
  • 在位图上绘制长字符串会导致绘图问题

    我正在将一个长字符串绘制到位图 超过一百万个字符 包括多行字符 r n 由一个写StringBuilder 我的文本转位图代码如下 public static Bitmap GetBitmap string input Font input