FreeTypeSharpを使った文字列描画

# 1.作るに至った動機
・MonoGameの日本語表示方法が気に入らなかったため
・FreeTypeをC#へラップしている「SharpFont」が.NET6.0に対応していなかったため
# 2.内容
### 2.1.開発環境
　Microsoft Visual Studio Community 2022 
　　.NET6.0 C#
　FreeTypeSharp 3.0.0
　　-(https://www.nuget.org/packages/FreeTypeSharp/)
### 2.2.参考資料
　FreeType-2.13.2 API Reference
　　-(https://freetype.org/freetype2/docs/reference/index.html)
　FreeType Tutorial
　　-(https://freetype.org/freetype2/docs/tutorial/step1.html)
　　-(https://freetype.org/freetype2/docs/tutorial/step2.html)
### 2.3.ソース

<details><summary>ソースコード(データクラス)</summary>

~~~C#:CharImageData

    public class CharImageData
    {
        public CharImageData(int width, int height)
        {
            Data = new ImageData(width, height);
        }
        public CharImageData(int advanceX, int bearingX, int bearingY, int width, int height) : this(width, height)
        {
            AdvanceX = advanceX;
            BearingX = bearingX;
            BearingY = bearingY;
        }

        public int AdvanceX { get; set; }
        public int BearingX { get; set; }
        public int BearingY { get; set; }
        public int Width
        {
            get { return Data.Width; }
        }
        public int Height
        {
            get { return Data.Height; }
        }

        public ImageData Data { get; set; }

        public void SetData(int x, int y, byte data)
        {
            Data.SetData(x, y, data);
        }
        public byte GetData(int x, int y)
        {
            return Data.GetData(x, y);
        }
    }
~~~

~~~C#:ImageData
    public class ImageData
    {
        public ImageData(int width, int height)
        {
            Width = width;
            Height = height;

            Datas = new byte[Width * Height];
        }

        public int Width { get; private set; }
        public int Height { get; private set; }
        public byte[] Datas { get; private set; }
        
        public byte GetData(int x, int y) 
        { 
            return Datas[GetIndex(x, y)];
        }
        public void SetData(int x, int y, byte data)
        {
            Datas[GetIndex(x, y)] = data;
        }


        private int GetIndex(int x, int y)
        {
            return x + y * Width;
        }
    }
~~~
</details>

<details><summary>ソースコード(処理クラス)</summary>

~~~C#:ImageFont
    public static unsafe class ImageFont
    {
        private static FreeTypeLibrary _Library;
        private static FreeTypeFaceFacade _FaceFacade;
        private static float _Size;

        static ImageFont()
        {
            _Library = new FreeTypeLibrary();
            _Size = 12;
        }

        public static FreeTypeFaceFacade FaceFacade { get { return _FaceFacade; } }

        public static float Size { get { return _Size; } }

        public static void SetFont(string fontpath)
        {
            FT_FaceRec_* face;

            var error = FT.FT_New_Face(_Library.Native, (byte*)Marshal.StringToHGlobalAnsi(fontpath), IntPtr.Zero, &face);

            if (error != FT_Error.FT_Err_Ok) throw new FreeTypeException(error);

            SetFont(face);
        }
        public static void SetFont(FT_FaceRec_* face)
        {
            _FaceFacade = new FreeTypeFaceFacade(_Library, face);
            SetSize(_Size);
        }

        public static void SetSize(float size)
        {
            _Size = size;
            if (_FaceFacade != null)
            {
                var error = FT.FT_Set_Char_Size(_FaceFacade.FaceRec, IntPtr.Zero, new IntPtr((int)(_Size * 64)), 96, 96);
                
                if (error != FT_Error.FT_Err_Ok) throw new FreeTypeException(error);
            }
        }

        public static StringImageData Render(string text)
        {
            if (string.IsNullOrEmpty(text)) return StringImageData.Empty;

            var charList = text.ToCharArray();
            var resList = new StringImageData(text)
            {
                FontWidth = (int)_FaceFacade.FaceRec->size->metrics.max_advance / 64,
                FontHeight = (int)_FaceFacade.FaceRec->size->metrics.height / 64,
            };

            int index = 0;
            int baseHeight = resList.FontHeight;
            foreach (var c in charList)
            {
                switch (c)
                {
                    case '\r':
                    case '\n':
                        break;

                    case '\t':
                        var tab = new CharImageData(resList.FontWidth * 2, 0, 0, 0, 0);
                        resList.Datas[index] = tab;
                        break;

                    default:
                        var griphIndex = _FaceFacade.GetCharIndex(c);
                        var error = FT.FT_Load_Glyph(_FaceFacade.FaceRec, griphIndex, FT_LOAD.FT_LOAD_DEFAULT);

                        if (error != FT_Error.FT_Err_Ok) throw new FreeTypeException(error);

                        var advanceX = (int)_FaceFacade.GlyphSlot->metrics.horiAdvance / 64;
                        var bealingX = (int)_FaceFacade.GlyphSlot->metrics.horiBearingX / 64;
                        var bearingY = (int)_FaceFacade.GlyphSlot->metrics.horiBearingY / 64;

                        error = FT.FT_Render_Glyph(_FaceFacade.GlyphSlot, FT_Render_Mode_.FT_RENDER_MODE_NORMAL);

                        if (error != FT_Error.FT_Err_Ok) throw new FreeTypeException(error);

                        var bitmap = _FaceFacade.GlyphSlot->bitmap;
                        var cid = new CharImageData(advanceX, bealingX, bearingY, (int)bitmap.width, (int)bitmap.rows + 1);
                        for (int i = 0; i < bitmap.width; i++)
                        {
                            for (int j = 0; j < bitmap.rows; j++)
                            {
                                cid.SetData(i, j, bitmap.buffer[i + j * bitmap.pitch]);
                            }
                        }

                        var tmpHeight = baseHeight - cid.BearingY + cid.Height;
                        if (resList.FontHeight < tmpHeight) resList.FontHeight = tmpHeight;
                        resList.Datas[index] = cid;
                        break;
                }
                index++;
            }

            resList.BaseLine = baseHeight;
            return resList;
        }
    }
~~~

~~~C#:StringImageData
    public class StringImageData
    {
        public static readonly StringImageData Empty = new StringImageData(string.Empty);

        public StringImageData(string value) 
        {
            Value = value;
            Datas = new CharImageData[Value.Length];
        }

        public string Value { get; private set; } 
        public bool IsEmpty
        {
            get { return string.IsNullOrEmpty(Value); }
        }

        public int FontHeight { get; set; }
        public int FontWidth { get; set; }
        public int BaseLine { get; set; }
        public CharImageData[] Datas { get; private set; }
        
        public ImageData ToImageData()
        {
            return ToImageData(0, Value.Length);
        }
        public ImageData ToImageData(int start, int length)
        {
            var lst = CatDatas(start, length);
            var image = new ImageData(GetTotalWidth(lst), FontHeight);

            int startX = 0;
            int startY;
            foreach (var data in lst)
            {
                if (startX != 0 || data.BearingX > 0) startX += data.BearingX;
                startY = BaseLine - data.BearingY;
                for (int i = 0; i < data.Width; i++)
                {
                    for (int j = 0; j < data.Height; j++)
                    {
                        image.SetData(startX + i, startY + j, data.GetData(i, j));
                    }
                }
                startX += data.AdvanceX - data.BearingX;
            }

            return image;
        }

        private CharImageData[] CatDatas(int start, int length)
        {
            if (start == 0 && length == Value.Length) return Datas;

            var res = new CharImageData[length];
            Array.Copy(Datas, start, res, 0, length);
            return res;
        }
        private int GetTotalWidth(CharImageData[] datas)
        {
            int width = 0;
            foreach (var data in datas)
            {
                width += data.AdvanceX;
            }

            return width;
        }
    }
~~~
</details>

<details><summary>ソースコード(呼出クラス)</summary>

~~~C#:Form1
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button2_Click(object sender, EventArgs e)
        {
            ImageFont.SetFont(Path.GetFullPath(@"JF-Dot-MPlusH12.ttf"));
            ImageFont.SetSize(24f);

            var data = ImageFont.Render(textBox1.Text);

            if (data.IsEmpty == false)
            {
                SetCharImageData(data.ToImageData());
            }
            else
            {
                pictureBox1.Image = null;
            }
        }

        private void SetCharImageData(ImageData data)
        {
            var baseColor = Color.DarkGreen;
            var image = new Bitmap(data.Width, data.Height);

            for (int i = 0; i < data.Width; i++)
            {
                for (int j = 0; j < data.Height; j++)
                {
                    var tmp = data.GetData(i, j);
                    if (tmp == 0) continue;
                    image.SetPixel(i, j, Color.FromArgb(tmp, baseColor));
                }
            }

            pictureBox1.Image = image;

        }

    }
~~~
</details>

### 2.4.実行結果
![SimpleFreeFontSharp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3741278/20f0495d-5351-334f-8b5c-0f6ecfea95c1.png)

### 2.5.ロジック
　ImageFontクラスが処理の本体です。
　SetFontメソッドでフォントファイルを選択、SetSizeメソッドでフォントサイズを選択。
　Renderメソッドで描画したい文字列を指示しています。
　FreeTypeでは1文字ごとにしか処理できないため、CharImageDataオブジェクトに1文字分の寸法情報と描画データを記録しています。

　描画データの結合はStringImageDataオブジェクトが行います。
　ToImageDataメソッドでCharImageDataを結合したImageDataオブジェクトを出力しています。
　結合するにあたり、文字の寸法情報を計算しています。


# 3.終わりに
　初投稿なのでいまいち勝手が分からぬ
　やはりソースの公開はGitHubがよいのだろうか

# 4.続き
https://qiita.com/fumifumi0453/items/eb70d106f6db48e5495d