Fnt檔案編輯工具

yanghui01發表於2024-04-30

這個僅僅是修改fnt,不負責從fnt生成unity字型檔案。

fnt生成字型檔案看這邊:ugui點陣圖字型使用 - fnt生成fontsettings工具

效果

工具程式碼

public class FntEditTool : EditorWindow
{

    [MenuItem("MyTools/Fnt Edit Tool", false, 100)]
    private static void ShowWindow()
    {
        var wnd = GetWindow<FntEditTool>(false, "FntEditTool");
    }

    private int m_ToolbarIndex = 0;
    private string[] m_ToolbarNames = { "Chars", "Kernings" };
    private Vector2 m_ScrollPos = new Vector2(0, 0);
    private GUIContent m_TempLabelContent = new GUIContent();

    private string m_FntFilePath = "";
    private Fnt m_Fnt;
    private Texture2D m_FntTex;
    private bool m_IsFntTexHeightNoMax = false;
    private bool m_ShowSpriteFrame = false;

    private void OnGUI()
    {
        GUILayout.Space(6);

        EditorGUI.BeginChangeCheck();
        UnityEngine.Object obj = null;
        GUI_ObjectField("Fnt檔案:", ref obj, typeof(TextAsset));
        GUILayout.Space(2);
        if (EditorGUI.EndChangeCheck())
        {
            if (null != obj)
                m_FntFilePath = AssetDatabase.GetAssetPath(obj);
        }

        GUI_TextField("Fnt檔案路徑:", ref m_FntFilePath);
        GUILayout.Space(2);

        if (string.IsNullOrEmpty(m_FntFilePath) || !File.Exists(m_FntFilePath))
        {
            EditorGUILayout.HelpBox($"請先選擇Fnt檔案", MessageType.Warning);
            GUILayout.Space(2);
            GUI.enabled = false;
        }

        EditorGUILayout.BeginHorizontal();
        if (GUILayout.Button("讀取Fnt檔案"))
        {
            m_Fnt = new FntParser().Parse(m_FntFilePath);

            string fntFileFolderPath = Path.GetDirectoryName(m_FntFilePath);
            var fntTexPath = Path.Combine(fntFileFolderPath, m_Fnt.file);
            if (File.Exists(fntTexPath))
                m_FntTex = AssetDatabase.LoadAssetAtPath<Texture2D>(fntTexPath);
            else
                m_FntTex = null;
        }

        if (GUILayout.Button("儲存Fnt"))
        {
            SaveFntFile(m_FntFilePath, m_Fnt);
        }
        EditorGUILayout.EndHorizontal();
        GUI.enabled = true;

        if (null != m_Fnt)
        {
            bool isScrollViewShow = false;
            OnGUI_FntTex(ref isScrollViewShow);

            GUILayout.Space(6);
            if (!isScrollViewShow)
                m_ScrollPos = EditorGUILayout.BeginScrollView(m_ScrollPos);

            EditorGUILayout.BeginHorizontal();
            {
                GUI_IntField("FontSize:", ref m_Fnt.fontSize);
                GUI_IntField("LineHeight:", ref m_Fnt.lineHeight);
                GUI_IntField("BaseLine:", ref m_Fnt.baseLine);
            }
            EditorGUILayout.EndHorizontal();

            EditorGUILayout.LabelField($"texWidth: {m_Fnt.texWidth}, texHeight: {m_Fnt.texHeight}");

            m_ToolbarIndex = GUILayout.Toolbar(m_ToolbarIndex, m_ToolbarNames);
            switch (m_ToolbarIndex)
            {
            case 0:
                OnGUI_Chars();
                break;
            case 1:
                OnGUI_Kernings();
                break;
            }

            EditorGUILayout.EndScrollView();
        }
    }

    private void GUI_ObjectField(string prefix, ref UnityEngine.Object v, Type t)
    {
        float bak = EditorGUIUtility.labelWidth;
        m_TempLabelContent.text = prefix;
        EditorGUIUtility.labelWidth = GUI.skin.label.CalcSize(m_TempLabelContent).x + 2;
        v = EditorGUILayout.ObjectField(m_TempLabelContent, v, t, false);
        EditorGUIUtility.labelWidth = bak;
    }

    private void GUI_TextField(string prefix, ref string v, int editBoxMaxWidth = -1)
    {
        float bak = EditorGUIUtility.labelWidth;
        m_TempLabelContent.text = prefix;
        EditorGUIUtility.labelWidth = GUI.skin.label.CalcSize(m_TempLabelContent).x + 2;
        if (editBoxMaxWidth > 0)
            v = EditorGUILayout.TextField(m_TempLabelContent, v, GUILayout.MaxWidth(EditorGUIUtility.labelWidth + editBoxMaxWidth));
        else
            v = EditorGUILayout.TextField(m_TempLabelContent, v);
        EditorGUIUtility.labelWidth = bak;
    }

    private void GUI_IntField(string prefix, ref int v, int editBoxMaxWidth = -1)
    {
        float bak = EditorGUIUtility.labelWidth;
        m_TempLabelContent.text = prefix;
        EditorGUIUtility.labelWidth = GUI.skin.label.CalcSize(m_TempLabelContent).x + 2;
        if (editBoxMaxWidth > 0)
            v = EditorGUILayout.IntField(m_TempLabelContent, v, GUILayout.MaxWidth(EditorGUIUtility.labelWidth + editBoxMaxWidth));
        else
            v = EditorGUILayout.IntField(m_TempLabelContent, v);
        EditorGUIUtility.labelWidth = bak;
    }

    private void OnGUI_FntTex(ref bool isScrollViewShow)
    {
        if (null != m_FntTex)
        {
            m_IsFntTexHeightNoMax = EditorGUILayout.ToggleLeft("貼圖預覽無高度限制", m_IsFntTexHeightNoMax);
            float fntTexMaxHeight = 0;
            if (m_IsFntTexHeightNoMax)
            {
                m_ScrollPos = EditorGUILayout.BeginScrollView(m_ScrollPos);
                isScrollViewShow = true;
                GUILayout.Label(m_FntTex);
            }
            else
            {
                fntTexMaxHeight = 150;
                GUILayout.Label(m_FntTex, GUILayout.MaxHeight(fntTexMaxHeight));
            }
            var lastRect = GUILayoutUtility.GetLastRect();
            float scale = 1;
            if (fntTexMaxHeight > 0)
            {
                scale = fntTexMaxHeight / m_FntTex.height;
                if (scale > 1)
                    scale = 1;
            }

            m_ShowSpriteFrame = EditorGUILayout.ToggleLeft("字元Sprite外框", m_ShowSpriteFrame);
            string focusCtrlName = GUI.GetNameOfFocusedControl();
            if (string.IsNullOrEmpty(focusCtrlName))
            {
                foreach (var ch in m_Fnt.charList)
                {
                    DrawCharFrame(scale, lastRect, ch);
                }
            }
            else
            {
                if (int.TryParse(focusCtrlName, out var num))
                {
                    int charIndex = num % 100;
                    if (charIndex < m_Fnt.charList.Count)
                    {
                        DrawCharFrame(scale, lastRect, m_Fnt.charList[charIndex]);
                    }
                }
            }
        }
    }

    private void DrawCharFrame(float scale, Rect lastRect, FntChar ch)
    {
        if (ch.w > 0 && ch.h > 0)
        {
            Vector2 leftTop = lastRect.min;
            if (m_ShowSpriteFrame)
            {
                //字元Sprite框
                Vector2 leftTop1 = new Vector2(leftTop.x + ch.x * scale, leftTop.y + ch.y * scale);
                Vector2 rightBottom1 = new Vector2(leftTop1.x + ch.w * scale, leftTop1.y + ch.h * scale);
                DrawRect(leftTop1, rightBottom1);
            }

            //排版框
            Handles.color = Color.red;
            Vector2 leftTop2 = new Vector2(leftTop.x + (ch.x - ch.xoff) * scale, leftTop.y + (ch.y - ch.yoff) * scale);
            Vector2 rightBottom2 = new Vector2(leftTop2.x + ch.xadv * scale, leftTop2.y + m_Fnt.lineHeight * scale);
            DrawRect(leftTop2, rightBottom2);
            Handles.color = Color.white;
        }
    }

    private void DrawRect(Vector2 leftTop, Vector2 rightBottom)
    {
        Vector2 leftBottom = new Vector2(leftTop.x, rightBottom.y);
        Vector2 rightTop = new Vector2(rightBottom.x, leftTop.y);
        Handles.DrawAAPolyLine(2f, leftTop, rightTop, rightBottom, leftBottom, leftTop);
    }

    private void OnGUI_Chars()
    {
        GUILayout.Space(6);
        //EditorGUILayout.LabelField($"Chars: {GUI.GetNameOfFocusedControl()}, {EditorGUIUtility.hotControl}");
        for (int i = 0; i < m_Fnt.charList.Count; ++i)
        {
            int controlType = 100;
            var fntChar = m_Fnt.charList[i];
            EditorGUILayout.BeginVertical("box");
            {
                EditorGUILayout.BeginHorizontal();
                GUI.SetNextControlName($"{controlType + i}");
                controlType += 100;
                fntChar.ch = EditorGUILayout.TextField("char:", fntChar.ch, GUILayout.Width(120));
                if (string.IsNullOrEmpty(fntChar.ch))
                {
                    fntChar.id = 0;
                }
                else
                {
                    GUILayout.Space(10);
                    int charCode2 = char.ConvertToUtf32(fntChar.ch, 0);
                    fntChar.id = charCode2;
                    EditorGUILayout.LabelField($"CharCode: {charCode2.ToString()}");
                }
                EditorGUILayout.EndHorizontal();

                EditorGUILayout.LabelField($"精靈在貼圖上的資訊:");
                EditorGUILayout.LabelField($"xy: ({fntChar.x}, {fntChar.y}) size: ({fntChar.w}, {fntChar.h})");

                EditorGUILayout.LabelField("排版資訊:");
                EditorGUILayout.BeginHorizontal();
                GUI.SetNextControlName($"{controlType + i}");
                controlType += 100;
                GUI_IntField("xoff:", ref fntChar.xoff);

                GUI.SetNextControlName($"{controlType + i}");
                controlType += 100;
                GUI_IntField("yoff:", ref fntChar.yoff);

                GUI.SetNextControlName($"{controlType + i}");
                controlType += 100;
                GUI_IntField("xadv:", ref fntChar.xadv);
                EditorGUILayout.EndHorizontal();

                GUILayout.Space(5);
                if (GUILayout.Button("X", GUILayout.Width(30)))
                {
                    m_Fnt.charList.RemoveAt(i);
                    i--;
                }
            }
            EditorGUILayout.EndVertical();
            GUILayout.Space(10); //條目間隔
        }
    }

    private void OnGUI_Kernings()
    {
        GUILayout.Space(6);
        if (GUILayout.Button("+"))
        {
            var kern = new FntKerning();
            m_Fnt.kerningList.Add(kern);
        }

        for (int i = 0; i < m_Fnt.kerningList.Count; ++i)
        {
            var kern = m_Fnt.kerningList[i];
            EditorGUILayout.BeginVertical("box");
            {
                EditorGUILayout.BeginHorizontal();
                GUI_TextField("字元1:", ref kern.ch1, 200);
                if (string.IsNullOrEmpty(kern.ch1))
                {
                    kern.first = 0;
                }
                else
                {
                    GUILayout.Space(10);
                    int charCode2 = char.ConvertToUtf32(kern.ch1, 0);
                    kern.first = charCode2;
                    EditorGUILayout.LabelField($"CharCode: {charCode2.ToString()}");
                }
                EditorGUILayout.EndHorizontal();

                EditorGUILayout.BeginHorizontal();
                GUI_TextField("字元2:", ref kern.ch2, 200);
                if (string.IsNullOrEmpty(kern.ch2))
                {
                    kern.second = 0;
                }
                else
                {
                    GUILayout.Space(10);
                    int charCode2 = char.ConvertToUtf32(kern.ch2, 0);
                    kern.second = charCode2;
                    EditorGUILayout.LabelField($"CharCode: {charCode2.ToString()}");
                }
                GUILayout.FlexibleSpace();
                EditorGUILayout.EndHorizontal();

                GUI_IntField("間距修正:", ref kern.amount, 200);
            }

            GUILayout.Space(5);
            if (GUILayout.Button("X", GUILayout.Width(30)))
            {
                m_Fnt.kerningList.RemoveAt(i);
                i--;
            }
        }
        EditorGUILayout.EndVertical();
        GUILayout.Space(10); //條目間隔
    }

    private static void SaveFntFile(string fntFilePath, Fnt fnt)
    {
        if (!EditorUtility.DisplayDialog("提示", $"是否覆蓋{fntFilePath}?", "Yes", "No"))
            return;

        using (StreamWriter sw = new StreamWriter(fntFilePath, false, Encoding.UTF8))
        {
            sw.WriteLine($"info face=\"Art\" size={fnt.fontSize}");
            sw.WriteLine($"common lineHeight={fnt.lineHeight} base={fnt.baseLine} scaleW={fnt.texWidth} scaleH={fnt.texHeight} pages=1");
            sw.WriteLine($"page id=0 file=\"{fnt.file}\"");
            sw.WriteLine($"chars count={fnt.charList.Count}");
            foreach (var c in fnt.charList)
            {
                sw.WriteLine($"char id={c.id} x={c.x} y={c.y} width={c.w} height={c.h} xoffset={c.xoff} yoffset={c.yoff} xadvance={c.xadv} page=0");
            }

            int kernCnt = fnt.kerningList.Count;
            if (kernCnt > 0)
            {
                sw.WriteLine($"kernings count={kernCnt}");
                foreach (var k in fnt.kerningList)
                {
                    sw.WriteLine($"kerning first={k.first} second={k.second} amount={k.amount}");
                }
            }
        }
        AssetDatabase.Refresh();
    }

}

fnt檔案資訊

public class Fnt
{
    public int fontSize = 22; //字型大小
    public int lineHeight = 22; //行高
    public int baseLine = 20; //基線位置
    public int texWidth = 512; //貼圖寬度
    public int texHeight = 256; //貼圖高度
    public string file = ""; //貼圖名字

    public List<FntChar> charList = new List<FntChar>();
    public List<FntKerning> kerningList = new List<FntKerning>();
}

public class FntChar
{
    public string ch = ""; //對應字元
    //unicode碼
    public int id;

    //在貼圖上的座標(左上角), 相對貼圖左上角
    public int x;
    public int y;
    //在貼圖上的寬高
    public int w;
    public int h;

    //排版修正
    public int xoff;
    public int yoff;
    //排版寬度
    public int xadv;
}

public class FntKerning
{
    public string ch1; //字元1
    public int first; //字元1的unicode碼

    public string ch2; //字元2
    public int second; //字元2的unicode碼

    public int amount; //x方向修正
}

fnt檔案解析

public class FntParser
{
    private Fnt m_Fnt;
    private Regex m_KVRegexPattern;

    public Fnt Parse(string filePath)
    {
        if (null == m_KVRegexPattern)
            m_KVRegexPattern = new Regex(@"(\S+)=""?([\w-.]+)""?");

        m_Fnt = new Fnt();
        using (StreamReader sr = new StreamReader(filePath, Encoding.UTF8))
        {
            string lineStr = "";
            int lineNum = 0;
            while (!string.IsNullOrEmpty(lineStr = sr.ReadLine()))
            {
                lineNum++;
                if (lineStr.StartsWith("info "))
                {
                    ParseInfoLine(lineStr);
                }
                else if (lineStr.StartsWith("common "))
                {
                    ParseCommonLine(lineStr);
                }
                else if (lineStr.StartsWith("page "))
                {
                    ParsePageLine(lineStr);
                }
                else if (lineStr.StartsWith("chars "))
                {

                }
                else if (lineStr.StartsWith("char "))
                {
                    ParseCharLine(lineStr);
                }
                else if (lineStr.StartsWith("kernings "))
                {

                }
                else if (lineStr.StartsWith("kerning "))
                {
                    ParseKerningLine(lineStr);
                }
            }
        }
        return m_Fnt;
    }

    private void ParseInfoLine(string infoLine)
    {
        MatchCollection result = m_KVRegexPattern.Matches(infoLine);
        for (var i = 0; i < result.Count; ++i)
        {
            Match item = result[i];
            if (3 == item.Groups.Count)
            {
                var key = item.Groups[1].Value;
                var value = item.Groups[2].Value;
                switch (key)
                {
                case "size": m_Fnt.fontSize = int.Parse(value); break;
                }
            }
        }
    }

    private void ParseCommonLine(string commonLine)
    {
        MatchCollection result = m_KVRegexPattern.Matches(commonLine);
        for (var i = 0; i < result.Count; ++i)
        {
            Match item = result[i];
            if (3 == item.Groups.Count)
            {
                var key = item.Groups[1].Value;
                var value = item.Groups[2].Value;
                switch (key)
                {
                case "lineHeight": m_Fnt.lineHeight = int.Parse(value); break;
                case "base": m_Fnt.baseLine = int.Parse(value); break;
                case "scaleW": m_Fnt.texWidth = int.Parse(value); break;
                case "scaleH": m_Fnt.texHeight = int.Parse(value); break;
                }
            }
        }
    }

    private void ParsePageLine(string pageLine)
    {
        int id = -1;
        string file = "";

        MatchCollection result = m_KVRegexPattern.Matches(pageLine);
        for (var i = 0; i < result.Count; ++i)
        {
            Match item = result[i];
            if (3 == item.Groups.Count)
            {
                var key = item.Groups[1].Value;
                var value = item.Groups[2].Value;
                //Debug.Log($"page: key='{key}', value='{value}'");
                switch (key)
                {
                case "id": id = int.Parse(value); break;
                case "file": file = value; break;
                }
            }
        }

        if (0 == id)
            m_Fnt.file = file;
        else
            Debug.LogWarning ($"page != 0, ignore: {pageLine}");
    }

    private int ParseCharsOrKerningCount(string line)
    {
        MatchCollection result = m_KVRegexPattern.Matches(line);
        for (var i = 0; i < result.Count; ++i)
        {
            Match item = result[i];
            if (3 == item.Groups.Count)
            {
                var key = item.Groups[1].Value;
                var value = item.Groups[2].Value;
                switch (key)
                {
                case "count":
                    return int.Parse(value);
                }
            }
        }
        return 0;
    }

    private void ParseCharLine(string charLine)
    {
        FntChar fntChar = new FntChar();

        MatchCollection result = m_KVRegexPattern.Matches(charLine);
        int page = 0;
        for (var i = 0; i < result.Count; ++i)
        {
            Match item = result[i];
            if (3 == item.Groups.Count)
            {
                var key = item.Groups[1].Value;
                var value = item.Groups[2].Value;
                //Debug.Log($"char: key:'{key}', value:'{value}'");
                switch (key)
                {
                case "id":
                    fntChar.id = int.Parse(value);
                    fntChar.ch = char.ConvertFromUtf32(fntChar.id);
                    break;
                case "x": fntChar.x = int.Parse(value); break;
                case "y": fntChar.y = int.Parse(value); break;
                case "width": fntChar.w = int.Parse(value); break;
                case "height": fntChar.h = int.Parse(value); break;
                case "xoffset": fntChar.xoff = int.Parse(value); break;
                case "yoffset": fntChar.yoff = int.Parse(value); break;
                case "xadvance": fntChar.xadv = int.Parse(value); break;
                case "page": page = int.Parse(value); break;
                }
            }
        }

        if (0 == page)
            m_Fnt.charList.Add(fntChar);
        else
            Debug.LogWarning($"page != 0, ignore:{charLine}");
    }

    private void ParseKerningLine(string kerningLine)
    {
        MatchCollection result = m_KVRegexPattern.Matches(kerningLine);
        var fntKern = new FntKerning();
        for (var i = 0; i < result.Count; ++i)
        {
            Match item = result[i];
            if (3 == item.Groups.Count)
            {
                var key = item.Groups[1].Value;
                var value = item.Groups[2].Value;
                //Debug.Log($"kerning: key:'{key}', value:'{value}'");
                switch (key)
                {
                case "first":
                    fntKern.first = int.Parse(value);
                    fntKern.ch1 = char.ConvertFromUtf32(fntKern.first);
                    break;
                case "second":
                    fntKern.second = int.Parse(value);
                    fntKern.ch2 = char.ConvertFromUtf32(fntKern.second);
                    break;
                case "amount": fntKern.amount = int.Parse(value); break;
                }
            }
        }
        m_Fnt.kerningList.Add(fntKern);
    }

}

相關文章