CSharpGL(41)改進獲取字形貼圖的方法
在(http://www.cnblogs.com/bitzhuwei/p/CSharpGL-28-simplest-way-to-creating-font-bitmap.html)中我實現了純C#獲取字形貼圖的方法。
最近發現這個方法有些缺陷:
-
單純地將每個字形左右兩側的部分剔除,這可能會損失某些信息。例如"新宋體"的小數點和數字寬度幾乎是相同的,但是這個方法將小數點的寬度大大減少了。看下圖就會發現區別。
-
整個方法過程比較長,而我的代碼邏輯也沒有穩妥地分步進行,顯得混亂難看。
于是我重新設計實現了這個方法。
試驗
為了完美地得到最終結果,先得確認一些基本的前提條件。
同高?
在之前的實現版本里,用 Graphics.MeasureString() 能夠得到任意字符串的Size,但是返回的字形寬度大于字形實際寬度,所以需要縮減一下。這個縮減也是導致缺陷1的原因。
因此我想了這樣一個獲取字符實際寬度的方法。舉例來說,想知道在某Font下字符x的寬度,可以先用 SizeF oneSize = graphics.MeasureString("x", font) 獲取這個單字符的寬度(左右有白邊),再用 SizeF doubleSize = graphics.MeasureString("xx", font) 獲取雙字符的寬度(左右有白邊),然后 doubleSize.Width - oneSize.Width 就是字符x的實際寬度了。最后用oneSize.Width左右分別去掉相同的空白量,就可以得到緊湊且無損的x的寬度了。
然而這個方法得到的x的高度取誰?oneSize.Height還是doubleSize.Height?如果兩者相等(直覺上是),那么沒問題了;如果兩者不等,該腫么辦?
這里第一個試驗,就是要試試會不會有不等高的奇葩字符。
1 /// <summary> 2 /// result: only (char)10 and (char)132 triggers ("!!!!!!!!!!!!!!"); 3 /// </summary> 4 static void TestIfDoubleCharChangesHeight() 5 { 6 var font = new Font("Arial", 32); 7 using (var bmp = new Bitmap(1, 1)) 8 { 9 using (var graphics = Graphics.FromImage(bmp)) 10 { 11 for (int i = 0; i <= char.MaxValue; i++) 12 { 13 SizeF oneSize = graphics.MeasureString(string.Format("{0}", (char)i), font); 14 SizeF doubleSize = graphics.MeasureString(string.Format("{0}{0}", (char)i), font); 15 if (oneSize.Height != doubleSize.Height) 16 { 17 Console.WriteLine("!!!!!!!!!!!!!!"); 18 } 19 } 20 } 21 } 22 }
試驗結果證明只有(char)10 和 (char)132這兩個特殊字符是不符合直覺的。所幸這2個字符是特殊字符,不可見的。所以以后直接忽略(跳過)即可。
同高?
仍然是個同高的問題,即:同一Font下的所有字形,通過 Graphics.MeasureString() 獲得的高度都相同嗎?直覺上是相同的,還是試驗讓你眼見為實。
1 /// <summary> 2 /// 有2種高度 3 /// </summary> 4 private static void TestIfAllHeightSame() 5 { 6 var font = new Font("Arial", 32); 7 var heightDict = new Dictionary<float, List<char>>(); 8 9 using (var bmp = new Bitmap(1, 1)) 10 { 11 using (var graphics = Graphics.FromImage(bmp)) 12 { 13 for (int i = 0; i <= char.MaxValue; i++) 14 { 15 SizeF oneSize = graphics.MeasureString(string.Format("{0}", (char)i), font); 16 SizeF doubleSize = graphics.MeasureString(string.Format("{0}{0}", (char)i), font); 17 if (oneSize.Height != doubleSize.Height) { continue; } 18 19 if (heightDict.ContainsKey(oneSize.Height)) 20 { 21 heightDict[oneSize.Height].Add((char)i); 22 } 23 else 24 { 25 heightDict.Add(oneSize.Height, new List<char>((char)i)); 26 } 27 } 28 } 29 } 30 31 Console.WriteLine("{0} heights", heightDict.Count); 32 }
試驗以Arial字體為例,結果出現了2種高度的字形。這說明,一般普遍的,同一Font下的所有字形,通過 Graphics.MeasureString() 獲得的高度是不同的。(不過相差不會大)
左右空白相等?
在第一個"同高"試驗里,我說"最后用oneSize.Width左右分別去掉相同的空白量,就可以得到緊湊且無損的x的寬度了。"。這里包含一個假設,就是任意字符,其左右兩側的空白都是相等的。那么果真這么美好嗎?試驗讓你眼見為實。

1 private static void PrintAllUnicodeChars() 2 { 3 var font = new Font("Arial", 32); 4 using (var bmp = new Bitmap(1, 1)) 5 { 6 using (var graphics = Graphics.FromImage(bmp)) 7 { 8 for (int i = 0; i <= char.MaxValue; i++) 9 { 10 Console.WriteLine("Processing {0}/{1}", i, char.MaxValue); 11 Size oneSize = graphics.MeasureString(string.Format("{0}", (char)i), font).ToSize(); 12 Size doubleSize = graphics.MeasureString(string.Format("{0}{0}", (char)i), font).ToSize(); 13 14 if (oneSize.Height != doubleSize.Height) { continue; } 15 if (oneSize.Width >= doubleSize.Width) { continue; } 16 17 Size charSize = new Size(doubleSize.Width - oneSize.Width, oneSize.Height); 18 string dirName = string.Format("{0}x{1}", charSize.Width, charSize.Height); 19 if (!Directory.Exists(dirName)) { Directory.CreateDirectory(dirName); } 20 21 using (var oneBitmap = new Bitmap(oneSize.Width, oneSize.Height)) 22 { 23 using (var g = Graphics.FromImage(oneBitmap)) 24 { g.DrawString(string.Format("{0}", (char)i), font, Brushes.Red, 0, 0); } 25 26 using (var charBitmap = new Bitmap(charSize.Width, charSize.Height)) 27 { 28 using (var g = Graphics.FromImage(charBitmap)) 29 { 30 g.DrawImage(oneBitmap, -(oneSize.Width - charSize.Width) / 2, 0); 31 } 32 33 charBitmap.Save(string.Format(@"{0}x{1}\{2}.png", charSize.Width, charSize.Height, i)); 34 } 35 } 36 } 37 } 38 } 39 }
這個試驗的代碼會把所有Unicode字符都保存為一個單獨的png圖片,且相同大小的字符保存到同一目錄下。
還是以Arial字體為例,高度只有52、54兩種,寬度出現了128種。逐個打開這些文件夾查看,我是沒有發現被截肢的字形。(其實我就挑著看了幾個,而且很多國家的文字我不認識)
開工
上面的試驗說明我已經可以用oneSize/doubleSize的方法獲取一個緊湊無損的字形。那么剩下的就是好好梳理整個流程了。
1 /// <summary> 2 /// Gets a <see cref="FontBitmap"/>'s intance. 3 /// </summary> 4 /// <param name="font">建議最大字體不超過32像素高度,否則可能無法承載所有Unicode字符。</param> 5 /// <param name="charSet"></param> 6 /// <param name="drawBoundary"></param> 7 /// <returns></returns> 8 public static FontBitmap GetFontBitmap(this Font font, string charSet, bool drawBoundary = false) 9 { 10 var fontBitmap = new FontBitmap();// font, glyph dict, bitmap 11 fontBitmap.GlyphFont = font; 12 // 先獲取各個glyph的width和height 13 fontBitmap.GlyphInfoDictionary = GetGlyphDict(font, charSet); 14 // 獲取所有glyph的面積之和,開方得到最終貼圖的寬度textureWidth 15 int textureWidth = GetTextureWidth(fontBitmap.GlyphInfoDictionary); 16 // 以所有glyph中height最大的為標準高度 17 fontBitmap.GlyphHeight = GetGlyphHeight(fontBitmap.GlyphInfoDictionary, textureWidth); 18 // 擺放glyph,得到x偏移和y偏移量,同時順便得到最終貼圖的高度textureHeight 19 int textureHeight = LayoutGlyphs(fontBitmap.GlyphInfoDictionary, textureWidth, fontBitmap.GlyphHeight); 20 // 根據glyph的擺放位置,生成最終的貼圖 21 fontBitmap.GlyphBitmap = PaintTexture(textureWidth, textureHeight, fontBitmap.GlyphInfoDictionary, font); 22 23 return fontBitmap; 24 }
對于"新宋體"的ASCII碼,會得到這樣的貼圖:
如果想觀察各個glyph的偏移量和寬高,就是這樣的:
你可以注意到"小數點"終于和數字"0"到"9"是一樣的寬度了。真正的緊湊且無損。
下載
CSharpGL已在GitHub開源,歡迎對OpenGL有興趣的同學加入(https://github.com/bitzhuwei/CSharpGL)
總結
下面是"新宋體"Unicode的前面若干字形。
文章列表