CSharpGL(28)得到高精度可定制字形贴图的极简方法
CSharpGL(28)得到高精度可定制字形贴图的极简方法
回顾
以前我用SharpFont实现了解析TTF文件从而获取字形贴图的功能,并最终实现了用OpenGL渲染文字。


使用SharpFont,美中不足的是:
SharpFont太大了,有上千行代码,且逻辑复杂难懂。
SharpFont画出的字形精度有限,虽然也很高,但是确实有限。用OpenGL渲染出来后会发现边缘不是特别清晰。
SharpFont对加粗、斜体、下划线、删除线如何支持,能否支持?完全不知道。
Graphics+Font
最近我在分析GLGUI(https://github.com/bitzhuwei/GLGUI)的代码时,惊喜地发现它给出了一个极其简单的方案,就是SizeF MeasureString(string text, Font font);和DrawString(string s, Font font, Brush brush, float x, float y);。
Graphics.MeasureString()能够得到任意字符串的Size。
Graphics.DrawString()能把任意字符串写到Bitmap上。
单个字形
首先我们要得到每个字形的Size。
由于MeasureString()返回的字形宽度大于字形实际宽度,所以需要缩减一下。
/// <summary>
/// Get glyph's size by graphics.MeasureString().
/// Then shrink glyph's size.
/// xoffset now means offset in a single glyph's bitmap.
/// </summary>
/// <param name="fontBitmap"></param>
/// <param name="charSet"></param>
/// <param name="singleCharWidth"></param>
/// <param name="singleCharHeight"></param>
private static void PrepareInitialGlyphDict(FontBitmap fontBitmap, string charSet, out int singleCharWidth, out int singleCharHeight)
{
// Get glyph's size by graphics.MeasureString().
{
int maxWidth = , maxHeight = ; float fontSize = fontBitmap.GlyphFont.Size; using (var bitmap = new Bitmap(, , PixelFormat.Format24bppRgb))
{
using (Graphics graphics = Graphics.FromImage(bitmap))
{
foreach (char c in charSet)
{
SizeF size = graphics.MeasureString(c.ToString(), fontBitmap.GlyphFont);
var info = new GlyphInfo(, , (int)size.Width, (int)size.Height);
fontBitmap.GlyphInfoDictionary.Add(c, info);
if (maxWidth < (int)size.Width) { maxWidth = (int)size.Width; }
if (maxHeight < (int)size.Height) { maxHeight = (int)size.Height; }
}
}
}
singleCharWidth = maxWidth;
singleCharHeight = maxHeight;
}
// shrink glyph's size.
// xoffset now means offset in a single glyph's bitmap.
{
using (var bitmap = new Bitmap(singleCharWidth, singleCharHeight))
{
using (var graphics = Graphics.FromImage(bitmap))
{
Color clearColor = Color.FromArgb(, , , );
foreach (var item in fontBitmap.GlyphInfoDictionary)
{
if (item.Key == ' ' || item.Key == '\t' || item.Key == '\r' || item.Key == '\n') { continue; } graphics.Clear(clearColor);
graphics.DrawString(item.Key.ToString(), fontBitmap.GlyphFont, Brushes.White, , );
BitmapData data = bitmap.LockBits(new Rectangle(, , bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
RetargetGlyphRectangleInwards(data, item.Value);
bitmap.UnlockBits(data);
}
}
}
}
}
/// <summary>
/// Returns true if the given pixel is empty (i.e. black)
/// </summary>
/// <param name="bitmapData"></param>
/// <param name="x"></param>
/// <param name="y"></param>
private static unsafe bool IsEmptyPixel(BitmapData bitmapData, int x, int y)
{
var addr = (byte*)(bitmapData.Scan0) + bitmapData.Stride * y + x * ;
return (*addr == && *(addr + ) == && *(addr + ) == );
} /// <summary>
/// shrink glyph's width to fit in exactly.
/// </summary>
/// <param name="bitmapData"></param>
/// <param name="glyph"></param>
private static void RetargetGlyphRectangleInwards(BitmapData bitmapData, GlyphInfo glyph)
{
int startX, endX; {
bool done = false;
for (startX = glyph.xoffset; startX < bitmapData.Width; startX++)
{
for (int j = glyph.yoffset; j < glyph.yoffset + glyph.height; j++)
{
if (!IsEmptyPixel(bitmapData, startX, j))
{
done = true;
break;
}
}
if (done) { break; }
}
}
{
bool done = false;
for (endX = glyph.xoffset + glyph.width - ; endX >= ; endX--)
{
for (int j = glyph.yoffset; j < glyph.yoffset + glyph.height; j++)
{
if (!IsEmptyPixel(bitmapData, endX, j))
{
done = true;
break;
}
}
if (done) { break; }
}
} if (endX < startX)
{
//startX = endX = glyph.xoffset;
glyph.width = ;
}
else
{
glyph.xoffset = startX;
glyph.width = endX - startX + ;
}
}
PrepareInitialGlyphDict
如下图所示,这是经过这一步后得到的字形信息:height、width和xoffset。这里xoffset暂时描述了单个字形的左边距,在最后,xoffset会描述字形左上角在整个贴图中的位置。

最后贴图的Size
由于在创建Bitmap对象时就得指定它的Size,所以这一步要先算出这个Size。
为了能够尽可能使用最小的贴图,我们按下图所示的方式依次排布所有字形。

如上图所示,每个黑框代表一个字形,尽量按正方形来排布,结束后就能得到所需的Size(width和height)
制作贴图
万事俱备,可以创建贴图了。
按照上一步的方式来排布各个字形,并且这次真的把字形贴上去。
/// <summary>
/// Print the final bitmap that contains all glyphs.
/// And also setup glyph's xoffset, yoffset.
/// </summary>
/// <param name="fontBitmap"></param>
/// <param name="singleCharWidth"></param>
/// <param name="singleCharHeight"></param>
/// <param name="width"></param>
/// <param name="height"></param>
private static void PrintBitmap(FontBitmap fontBitmap, int singleCharWidth, int singleCharHeight, int width, int height)
{
var bitmap = new Bitmap(width, height);
using (var graphics = Graphics.FromImage(bitmap))
{
using (var glyphBitmap = new Bitmap(singleCharWidth, singleCharHeight))
{
using (var glyphGraphics = Graphics.FromImage(glyphBitmap))
{
int currentX = leftMargin, currentY = ;
Color clearColor = Color.FromArgb(, , , );
foreach (KeyValuePair<char, GlyphInfo> item in fontBitmap.GlyphInfoDictionary)
{
glyphGraphics.Clear(clearColor);
glyphGraphics.DrawString(item.Key.ToString(), fontBitmap.GlyphFont,
Brushes.White, , );
// move to new line if this line is full.
if (currentX + item.Value.width > width)
{
currentX = leftMargin;
currentY += singleCharHeight;
}
// draw the current glyph.
graphics.DrawImage(glyphBitmap,
new Rectangle(currentX, currentY, item.Value.width, item.Value.height),
item.Value.ToRectangle(),
GraphicsUnit.Pixel);
// move line cursor to next(right) position.
item.Value.xoffset = currentX;
item.Value.yoffset = currentY;
// prepare for next glyph's position.
currentX += item.Value.width + glyphInterval;
}
}
}
} fontBitmap.GlyphBitmap = bitmap;
}
PrintBitmap
结果示意图如下。

Demo
为了便于debug和观看效果,我在CSharpGL.Demos里加了下面这个Demo。你可以指定任意字体,设置是否启用加粗、斜体、下划线、删除线等效果。

用OpenGL渲染文字时,边缘的效果也很令人满意了。


总结
由于使用了.NET自带的Graphics和Font类型,就完全去掉了SharpFont那上千行代码。CSharpGL.dll由此下降了200K。效果增强,体积下降,代码简化,各个方面都获得提升。
CSharpGL(28)得到高精度可定制字形贴图的极简方法的更多相关文章
- CSharpGL(41)改进获取字形贴图的方法
CSharpGL(41)改进获取字形贴图的方法 在(http://www.cnblogs.com/bitzhuwei/p/CSharpGL-28-simplest-way-to-creating-fo ...
- C#+OpenGL+FreeType显示3D文字(1) - 从TTF文件导出字形贴图
C#+OpenGL+FreeType显示3D文字(1) - 从TTF文件导出字形贴图 +BIT祝威+悄悄在此留下版了个权的信息说: 最近需要用OpenGL绘制文字,这是个很费时费力的事.一般的思路就是 ...
- iView定制主题报错问题的解决方法
按照iView官网来是这样的: 1. 在main.js当前目录下新建themes文件夹,里面新建一个叫blue.less的文件 2. 在mian.js里面引入blue.less文件 3. blue.l ...
- HighCharts 根据spline-plot-bands图,定制自己的图(区间里显示多个数据)
公司项目里有这样一个需求,根据数据绘图,但是数据很多,不可能每个点每个点的去画,这样显示的数据太密集非常的难看(更显得技术不专业),如图: 所以我和项目经理商量如何显示这个图形,按照他的意思是,按照范 ...
- BIT祝威博客汇总(Blog Index)
+BIT祝威+悄悄在此留下版了个权的信息说: 关于硬件(Hardware) <穿越计算机的迷雾>笔记 继电器是如何成为CPU的(1) 继电器是如何成为CPU的(2) 关于操作系统(Oper ...
- ASP.NET Core应用的错误处理[3]:ExceptionHandlerMiddleware中间件如何呈现“定制化错误页面”
DeveloperExceptionPageMiddleware中间件利用呈现出来的错误页面实现抛出异常和当前请求的详细信息以辅助开发人员更好地进行纠错诊断工作,而ExceptionHandlerMi ...
- CSharpGL(26)在opengl中实现控件布局/渲染文字
CSharpGL(26)在opengl中实现控件布局/渲染文字 效果图 如图所示,可以将文字.坐标轴固定在窗口的一角. 下载 CSharpGL已在GitHub开源,欢迎对OpenGL有兴趣的同学加入( ...
- CSharpGL(0)一个易学易用的C#版OpenGL
+BIT祝威+悄悄在此留下版了个权的信说: CSharpGL(0)一个易学易用的C#版OpenGL CSharpGL是我受到SharpGL的启发,在整理了SharpGL,GLM,SharpFont等开 ...
- ExceptionHandlerMiddleware中间件如何呈现“定制化错误页面”
ExceptionHandlerMiddleware中间件如何呈现“定制化错误页面” DeveloperExceptionPageMiddleware中间件利用呈现出来的错误页面实现抛出异常和当前请求 ...
随机推荐
- HTML 事件(一) 事件的介绍
本篇主要介绍HTML中的事件知识:事件相关术语.DOM事件规范.事件对象. 其他事件文章 1. HTML 事件(一) 事件的介绍 2. HTML 事件(二) 事件的注册与注销 3. HTML 事件(三 ...
- clr 元数据
clr相关编译器编译生成的托管模块由四部分组成:PE32或32+头.clr头.元数据.IL代码. 元数据和IL代码完全对应,保持一致(:>)性. 元数据有很多用途: VS的智能感知,自动补全: ...
- ASP.NET Core应用针对静态文件请求的处理[4]: DirectoryBrowserMiddleware中间件如何呈现目录结构
和StaticFileMiddleware中间件一样,DirectoryBrowserMiddleware中间本质上还是定义了一个请求地址与某个物理目录之间的映射关系,而目标目录体现为一个FilePr ...
- ASP.NET Core: You must add a reference to assembly mscorlib, version=4.0.0.0
ASP.NET Core 引用外部程序包的时候,有时会出现下面的错误: The type 'Object' is defined in an assembly that is not referenc ...
- 深入学习jQuery自定义插件
原文地址:jQuery自定义插件学习 1.定义插件的方法 对象级别的插件扩展,即为jQuery类的实例增加方法, 调用:$(选择器).函数名(参数); $(‘#id’).myPlugin(o ...
- 游戏AI系列内容 咋样才能做个有意思的AI呢
游戏AI系列内容 咋样才能做个有意思的AI呢 写在前面的话 怪物AI怎么才能做的比较有意思.其实这个命题有点大,我作为一个仅仅进入游戏行业两年接触怪物AI还不到一年的程序员来说,来谈这个话题,我想我是 ...
- C#反序列化XML异常:在 XML文档(0, 0)中有一个错误“缺少根元素”
Q: 在反序列化 Xml 字符串为 Xml 对象时,抛出如下异常. 即在 XML文档(0, 0)中有一个错误:缺少根元素. A: 首先看下代码: StringBuilder sb = new Stri ...
- 纯javaScript、jQuery实现个性化图片轮播
纯javaScript实现个性化图片轮播 轮播原理说明<如上图所示>: 1. 画布部分(可视区域)属性说明:overflow:hidden使得超出画布部分隐藏或说不可见.position: ...
- 找到第k个最小元----快速选择
此算法借用快速排序算法. 这个快速选择算法主要利用递归调用,数组存储方式.包含3个文件,头文件QuickSelect.h,库函数QuickSelect.c,测试文件TestQuickSelect. 其 ...
- 论C#之多继承
C#多继承的讨论似乎是个古老的问题了,但今天本文要向大家展示的C#多继承可能是大家闻所未闻见所未见的,甚至是发明C#语言的人也不曾想到我会这样去写代码,并且自得其乐. 说起多继承,首先大家可以想想这个 ...