• 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()返回的字形宽度大于字形实际宽度,所以需要缩减一下。

      1         /// <summary>
      2         /// Get glyph's size by graphics.MeasureString().
      3         /// Then shrink glyph's size.
      4         /// xoffset now means offset in a single glyph's bitmap.
      5         /// </summary>
      6         /// <param name="fontBitmap"></param>
      7         /// <param name="charSet"></param>
      8         /// <param name="singleCharWidth"></param>
      9         /// <param name="singleCharHeight"></param>
     10         private static void PrepareInitialGlyphDict(FontBitmap fontBitmap, string charSet, out int singleCharWidth, out int singleCharHeight)
     11         {
     12             // Get glyph's size by graphics.MeasureString().
     13             {
     14                 int maxWidth = 0, maxHeight = 0;
     15 
     16                 float fontSize = fontBitmap.GlyphFont.Size;
     17 
     18                 using (var bitmap = new Bitmap(1, 1, PixelFormat.Format24bppRgb))
     19                 {
     20                     using (Graphics graphics = Graphics.FromImage(bitmap))
     21                     {
     22                         foreach (char c in charSet)
     23                         {
     24                             SizeF size = graphics.MeasureString(c.ToString(), fontBitmap.GlyphFont);
     25                             var info = new GlyphInfo(0, 0, (int)size.Width, (int)size.Height);
     26                             fontBitmap.GlyphInfoDictionary.Add(c, info);
     27                             if (maxWidth < (int)size.Width) { maxWidth = (int)size.Width; }
     28                             if (maxHeight < (int)size.Height) { maxHeight = (int)size.Height; }
     29                         }
     30                     }
     31                 }
     32                 singleCharWidth = maxWidth;
     33                 singleCharHeight = maxHeight;
     34             }
     35             // shrink glyph's size.
     36             // xoffset now means offset in a single glyph's bitmap.
     37             {
     38                 using (var bitmap = new Bitmap(singleCharWidth, singleCharHeight))
     39                 {
     40                     using (var graphics = Graphics.FromImage(bitmap))
     41                     {
     42                         Color clearColor = Color.FromArgb(0, 0, 0, 0);
     43                         foreach (var item in fontBitmap.GlyphInfoDictionary)
     44                         {
     45                             if (item.Key == ' ' || item.Key == '	' || item.Key == '
    ' || item.Key == '
    ') { continue; }
     46 
     47                             graphics.Clear(clearColor);
     48                             graphics.DrawString(item.Key.ToString(), fontBitmap.GlyphFont, Brushes.White, 0, 0);
     49                             BitmapData data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
     50                             RetargetGlyphRectangleInwards(data, item.Value);
     51                             bitmap.UnlockBits(data);
     52                         }
     53                     }
     54                 }
     55             }
     56         }
     57         /// <summary>
     58         /// Returns true if the given pixel is empty (i.e. black)
     59         /// </summary>
     60         /// <param name="bitmapData"></param>
     61         /// <param name="x"></param>
     62         /// <param name="y"></param>
     63         private static unsafe bool IsEmptyPixel(BitmapData bitmapData, int x, int y)
     64         {
     65             var addr = (byte*)(bitmapData.Scan0) + bitmapData.Stride * y + x * 3;
     66             return (*addr == 0 && *(addr + 1) == 0 && *(addr + 2) == 0);
     67         }
     68 
     69         /// <summary>
     70         /// shrink glyph's width to fit in exactly.
     71         /// </summary>
     72         /// <param name="bitmapData"></param>
     73         /// <param name="glyph"></param>
     74         private static void RetargetGlyphRectangleInwards(BitmapData bitmapData, GlyphInfo glyph)
     75         {
     76             int startX, endX;
     77 
     78             {
     79                 bool done = false;
     80                 for (startX = glyph.xoffset; startX < bitmapData.Width; startX++)
     81                 {
     82                     for (int j = glyph.yoffset; j < glyph.yoffset + glyph.height; j++)
     83                     {
     84                         if (!IsEmptyPixel(bitmapData, startX, j))
     85                         {
     86                             done = true;
     87                             break;
     88                         }
     89                     }
     90                     if (done) { break; }
     91                 }
     92             }
     93             {
     94                 bool done = false;
     95                 for (endX = glyph.xoffset + glyph.width - 1; endX >= 0; endX--)
     96                 {
     97                     for (int j = glyph.yoffset; j < glyph.yoffset + glyph.height; j++)
     98                     {
     99                         if (!IsEmptyPixel(bitmapData, endX, j))
    100                         {
    101                             done = true;
    102                             break;
    103                         }
    104                     }
    105                     if (done) { break; }
    106                 }
    107             }
    108 
    109             if (endX < startX)
    110             {
    111                 //startX = endX = glyph.xoffset;
    112                 glyph.width = 0;
    113             }
    114             else
    115             {
    116                 glyph.xoffset = startX;
    117                 glyph.width = endX - startX + 1;
    118             }
    119         }
    PrepareInitialGlyphDict

    如下图所示,这是经过这一步后得到的字形信息:height、width和xoffset。这里xoffset暂时描述了单个字形的左边距,在最后,xoffset会描述字形左上角在整个贴图中的位置。

    最后贴图的Size

    由于在创建Bitmap对象时就得指定它的Size,所以这一步要先算出这个Size。

    为了能够尽可能使用最小的贴图,我们按下图所示的方式依次排布所有字形。

    如上图所示,每个黑框代表一个字形,尽量按正方形来排布,结束后就能得到所需的Size(width和height)

    制作贴图

    万事俱备,可以创建贴图了。

    按照上一步的方式来排布各个字形,并且这次真的把字形贴上去。

     1         /// <summary>
     2         /// Print the final bitmap that contains all glyphs.
     3         /// And also setup glyph's xoffset, yoffset.
     4         /// </summary>
     5         /// <param name="fontBitmap"></param>
     6         /// <param name="singleCharWidth"></param>
     7         /// <param name="singleCharHeight"></param>
     8         /// <param name="width"></param>
     9         /// <param name="height"></param>
    10         private static void PrintBitmap(FontBitmap fontBitmap, int singleCharWidth, int singleCharHeight, int width, int height)
    11         {
    12             var bitmap = new Bitmap(width, height);
    13             using (var graphics = Graphics.FromImage(bitmap))
    14             {
    15                 using (var glyphBitmap = new Bitmap(singleCharWidth, singleCharHeight))
    16                 {
    17                     using (var glyphGraphics = Graphics.FromImage(glyphBitmap))
    18                     {
    19                         int currentX = leftMargin, currentY = 0;
    20                         Color clearColor = Color.FromArgb(0, 0, 0, 0);
    21                         foreach (KeyValuePair<char, GlyphInfo> item in fontBitmap.GlyphInfoDictionary)
    22                         {
    23                             glyphGraphics.Clear(clearColor);
    24                             glyphGraphics.DrawString(item.Key.ToString(), fontBitmap.GlyphFont,
    25                                 Brushes.White, 0, 0);
    26                             // move to new line if this line is full.
    27                             if (currentX + item.Value.width > width)
    28                             {
    29                                 currentX = leftMargin;
    30                                 currentY += singleCharHeight;
    31                             }
    32                             // draw the current glyph.
    33                             graphics.DrawImage(glyphBitmap,
    34                                 new Rectangle(currentX, currentY, item.Value.width, item.Value.height),
    35                                 item.Value.ToRectangle(),
    36                                 GraphicsUnit.Pixel);
    37                             // move line cursor to next(right) position.
    38                             item.Value.xoffset = currentX;
    39                             item.Value.yoffset = currentY;
    40                             // prepare for next glyph's position.
    41                             currentX += item.Value.width + glyphInterval;
    42                         }
    43                     }
    44                 }
    45             }
    46 
    47             fontBitmap.GlyphBitmap = bitmap;
    48         }
    PrintBitmap

    结果示意图如下。

    Demo

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

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

     

    总结

    由于使用了.NET自带的Graphics和Font类型,就完全去掉了SharpFont那上千行代码。CSharpGL.dll由此下降了200K。效果增强,体积下降,代码简化,各个方面都获得提升。

  • 相关阅读:
    About Me
    洛谷 P2633 Count on a tree
    【题解】牛客编程巅峰赛S1赛季第1场
    洛谷 P4132 [BJOI2012]算不出的等式
    洛谷 P5970 [POI2016]Nim z utrudnieniem
    DP没入门就入土
    洛谷 P4042 [AHOI2014/JSOI2014]骑士游戏
    洛谷 P3592 [POI2015]MYJ
    51Nod 1683 最短路
    51Nod 1327 棋盘游戏
  • 原文地址:https://www.cnblogs.com/bitzhuwei/p/CSharpGL-28-simplest-way-to-creating-font-bitmap.html
Copyright © 2020-2023  润新知