文章出處

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 == '\t' || item.Key == '\r' || item.Key == '\n') { 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。效果增強,體積下降,代碼簡化,各個方面都獲得提升。

 


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()