文章出處

CSharpGL(41)改進獲取字形貼圖的方法

在(http://www.cnblogs.com/bitzhuwei/p/CSharpGL-28-simplest-way-to-creating-font-bitmap.html)中我實現了純C#獲取字形貼圖的方法。

最近發現這個方法有些缺陷:

  1. 單純地將每個字形左右兩側的部分剔除,這可能會損失某些信息。例如"新宋體"的小數點和數字寬度幾乎是相同的,但是這個方法將小數點的寬度大大減少了。看下圖就會發現區別。
  2. 整個方法過程比較長,而我的代碼邏輯也沒有穩妥地分步進行,顯得混亂難看。

于是我重新設計實現了這個方法。

試驗

為了完美地得到最終結果,先得確認一些基本的前提條件。

同高?

在之前的實現版本里,用 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         }
View Code

這個試驗的代碼會把所有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的前面若干字形。


文章列表


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

    IT工程師數位筆記本

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