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 }
如下圖所示,這是經過這一步后得到的字形信息: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 }
結果示意圖如下。
Demo
為了便于debug和觀看效果,我在CSharpGL.Demos里加了下面這個Demo。你可以指定任意字體,設置是否啟用加粗、斜體、下劃線、刪除線等效果。
用OpenGL渲染文字時,邊緣的效果也很令人滿意了。
總結
由于使用了.NET自帶的Graphics和Font類型,就完全去掉了SharpFont那上千行代碼。CSharpGL.dll由此下降了200K。效果增強,體積下降,代碼簡化,各個方面都獲得提升。
文章列表