LoginSignup
0
0

FreeTypeSharpを使った文字列描画

Posted at

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.ソース

ソースコード(データクラス)
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);
        }
    }
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;
        }
    }
ソースコード(処理クラス)
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;
        }
    }
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;
        }
    }
ソースコード(呼出クラス)
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;

        }

    }

2.4.実行結果

SimpleFreeFontSharp.png

2.5.ロジック

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

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

3.終わりに

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0