192 lines
4.9 KiB
C#
192 lines
4.9 KiB
C#
using System.Numerics;
|
|
|
|
using FreeTypeSharp;
|
|
using static FreeTypeSharp.FT;
|
|
using static FreeTypeSharp.FT_LOAD;
|
|
|
|
namespace Voile;
|
|
|
|
public struct Glyph
|
|
{
|
|
public int TextureId { get; set; } = -1;
|
|
public float Width { get; set; }
|
|
public float Height { get; set; }
|
|
public Vector2 Bearing { get; set; }
|
|
public int Advance { get; set; }
|
|
|
|
public Glyph() { }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents font data.
|
|
/// </summary>
|
|
public class Font : Resource, IUpdatableResource, IDisposable
|
|
{
|
|
/// <summary>
|
|
/// Internal handle for the font. If it got successfully loaded into the GPU, the value will be other than -1.
|
|
/// </summary>
|
|
internal int Handle { get; set; } = -1;
|
|
public int Size { get; set; } = 16;
|
|
|
|
public byte[]? Buffer { get; private set; }
|
|
public long BufferSize { get; set; }
|
|
|
|
public bool Dirty => _dirty;
|
|
|
|
internal float UnitsPerEm;
|
|
|
|
internal nint FacePtr;
|
|
internal nint LibraryPtr;
|
|
|
|
internal List<int> Codepoints => _glyphs.Keys.ToList();
|
|
|
|
public Font(string path, byte[] buffer) : base(path)
|
|
{
|
|
Buffer = buffer;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
unsafe
|
|
{
|
|
if (FacePtr != IntPtr.Zero)
|
|
FT_Done_Face((FT_FaceRec_*)FacePtr);
|
|
|
|
if (LibraryPtr != IntPtr.Zero)
|
|
FT_Done_FreeType((FT_LibraryRec_*)LibraryPtr);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads a basic ASCII charset for this font.
|
|
/// </summary>
|
|
public void LoadAsciiData()
|
|
{
|
|
for (char c = ' '; c < 127; c++)
|
|
{
|
|
GetGlyph(c);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Measures a given string using the font metrics.
|
|
/// </summary>
|
|
/// <param name="text">Text to measure.</param>
|
|
/// <returns>A <see cref="Rect"/> with the sizes of a given text using this font.</returns>
|
|
public Rect Measure(string text)
|
|
{
|
|
if (string.IsNullOrEmpty(text))
|
|
return Rect.Zero;
|
|
|
|
float totalWidth = 0;
|
|
float maxAscent = 0;
|
|
float maxDescent = 0;
|
|
|
|
for (int i = 0; i < text.Length; i++)
|
|
{
|
|
char c = text[i];
|
|
Glyph glyph = GetGlyph(c);
|
|
|
|
totalWidth += glyph.Advance;
|
|
|
|
float ascent = glyph.Bearing.Y;
|
|
float descent = glyph.Height - glyph.Bearing.Y;
|
|
|
|
if (ascent > maxAscent)
|
|
maxAscent = ascent;
|
|
if (descent > maxDescent)
|
|
maxDescent = descent;
|
|
|
|
if (i > 0)
|
|
{
|
|
char prevChar = text[i - 1];
|
|
totalWidth += GetKerning(prevChar, c);
|
|
}
|
|
}
|
|
|
|
float totalHeight = maxAscent + maxDescent;
|
|
return new Rect(totalWidth, totalHeight);
|
|
}
|
|
|
|
public int GetKerning(char left, char right)
|
|
{
|
|
unsafe
|
|
{
|
|
if (FacePtr == IntPtr.Zero)
|
|
return 0;
|
|
|
|
FT_FaceRec_* face = (FT_FaceRec_*)FacePtr;
|
|
|
|
uint leftIndex = FT_Get_Char_Index(face, left);
|
|
uint rightIndex = FT_Get_Char_Index(face, right);
|
|
|
|
if (leftIndex == 0 || rightIndex == 0)
|
|
return 0;
|
|
|
|
FT_Vector_ kerning;
|
|
if (FT_Get_Kerning(face, leftIndex, rightIndex, FT_Kerning_Mode_.FT_KERNING_DEFAULT, &kerning) != 0)
|
|
return 0;
|
|
|
|
return (int)kerning.x;
|
|
}
|
|
}
|
|
|
|
public Glyph GetGlyph(char character)
|
|
{
|
|
if (!HasGlyph(character))
|
|
{
|
|
_glyphs.TryGetValue('?', out var defGlyph);
|
|
return defGlyph;
|
|
}
|
|
if (_glyphs.TryGetValue(character, out var glyph))
|
|
return glyph;
|
|
|
|
var loaded = LoadGlyph(character);
|
|
_glyphs[character] = loaded;
|
|
|
|
_dirty = true;
|
|
|
|
return loaded;
|
|
}
|
|
|
|
public bool HasGlyph(char character)
|
|
{
|
|
unsafe
|
|
{
|
|
var face = (FT_FaceRec_*)FacePtr;
|
|
return FT_Get_Char_Index(face, character) != 0;
|
|
}
|
|
}
|
|
|
|
private unsafe Glyph LoadGlyph(char character)
|
|
{
|
|
var face = (FT_FaceRec_*)FacePtr;
|
|
|
|
// TODO: for now, we're loading glyphs for metrics, but when implementing WebGPU rendering, we want to somehow render them to SDF or bitmap.
|
|
FT_Error error = FT_Set_Pixel_Sizes(face, 0, (uint)Size);
|
|
error = FT_Load_Char(face, character, FT_LOAD_NO_BITMAP);
|
|
|
|
if (error != 0)
|
|
throw new Exception($"Failed to load glyph for '{character}'");
|
|
|
|
var glyph = face->glyph;
|
|
var bitmap = glyph->bitmap;
|
|
var metrics = glyph->metrics;
|
|
|
|
return new Glyph
|
|
{
|
|
Width = metrics.width >> 6,
|
|
Height = metrics.height >> 6,
|
|
Bearing = new Vector2(metrics.horiBearingX >> 6, metrics.horiBearingY >> 6),
|
|
Advance = (int)metrics.horiAdvance >> 6,
|
|
};
|
|
}
|
|
|
|
public void MarkUpdated()
|
|
{
|
|
_dirty = false;
|
|
}
|
|
|
|
private bool _dirty;
|
|
private Dictionary<int, Glyph> _glyphs = new();
|
|
} |