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.ソース
ソースコード(データクラス)
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);
}
}
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;
}
}
ソースコード(処理クラス)
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;
}
}
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;
}
}
ソースコード(呼出クラス)
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.実行結果
2.5.ロジック
ImageFontクラスが処理の本体です。
SetFontメソッドでフォントファイルを選択、SetSizeメソッドでフォントサイズを選択。
Renderメソッドで描画したい文字列を指示しています。
FreeTypeでは1文字ごとにしか処理できないため、CharImageDataオブジェクトに1文字分の寸法情報と描画データを記録しています。
描画データの結合はStringImageDataオブジェクトが行います。
ToImageDataメソッドでCharImageDataを結合したImageDataオブジェクトを出力しています。
結合するにあたり、文字の寸法情報を計算しています。
3.終わりに
初投稿なのでいまいち勝手が分からぬ
やはりソースの公開はGitHubがよいのだろうか
4.続き