diff --git a/FontManagement/FontManager.cs b/FontManagement/FontManager.cs
index a58f737..2d7b066 100644
--- a/FontManagement/FontManager.cs
+++ b/FontManagement/FontManager.cs
@@ -3,6 +3,7 @@
using System.Globalization;
using System.IO;
using FontStashSharp;
+using FontStashSharp.Interfaces;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
@@ -61,7 +62,7 @@ public static void Initialize()
///
/// Creates a new FontSystem with current text shaping settings.
///
- private static FontSystem CreateFontSystem()
+ private static FontSystem CreateFontSystem(IFontLoader fontLoader = null)
{
var settings = new FontSystemSettings
{
@@ -71,7 +72,8 @@ private static FontSystem CreateFontSystem()
TextureWidth = fontRenderingSettings.TextureWidth,
TextureHeight = fontRenderingSettings.TextureHeight,
GlyphRenderResult = fontRenderingSettings.GlyphRenderResult,
- UseEmToPixelsScale = true
+ UseEmToPixelsScale = true,
+ FontLoader = fontLoader,
};
if (textShapingSettings.Enabled)
@@ -84,7 +86,7 @@ private static FontSystem CreateFontSystem()
settings.ShapedTextCacheSize = textShapingSettings.CacheSize;
}
- return new FontSystem(settings);
+ return new FontSystem(settings) { DefaultCharacter = '?' };
}
public static Vector2 MeasureString(string text, int fontIndex)
@@ -265,12 +267,16 @@ private static void CreateFontIndexesFromIni(IniFile iniFile, ContentManager con
string fontPath = iniFile.GetStringValue(section, "Path", "");
int size = iniFile.GetIntValue(section, "Size", 16);
string fontTypeStr = iniFile.GetStringValue(section, "Type", nameof(FontType.SpriteFont));
+ string fontLoaderStr = iniFile.GetStringValue(section, "Loader", nameof(TTFFontLoader.StbTrueType));
int fallback = iniFile.GetIntValue(section, "Fallback", -1);
if (!Enum.TryParse(fontTypeStr, true, out var fontType))
throw new Exception($"Invalid font type for {section}: {fontTypeStr}");
- fontConfigs.Add(new FontConfig(fontPath, size, fontType, fallback));
+ if (!Enum.TryParse(fontLoaderStr, true, out var fontLoader))
+ throw new Exception($"Invalid font loader for {section}: {fontLoaderStr}");
+
+ fontConfigs.Add(new FontConfig(fontPath, size, fontType, fontLoader, fallback));
}
for (int i = 0; i < fontCount; i++)
@@ -299,7 +305,17 @@ private static void CreateFontIndexesFromIni(IniFile iniFile, ContentManager con
///
private static void CreateTrueTypeFontIndex(int fontIndex, FontConfig config, List allConfigs, string searchPath)
{
- FontSystem fontSystem = CreateFontSystem();
+ IFontLoader fontLoader = config.FontLoader switch
+ {
+ // Note: FontStashSharp automatically uses StbTrueType with parameters specified in FontSystemSettings, so we return null here to use the default loader.
+ TTFFontLoader.StbTrueType => null,
+ TTFFontLoader.FreeType => new FreeTypeFontLoader(FreeTypeRenderMode.Normal),
+ TTFFontLoader.FreeTypeMono => new FreeTypeFontLoader(FreeTypeRenderMode.Mono),
+ TTFFontLoader.Forme => throw new NotImplementedException("Forme font loader is not implemented yet"),
+ _ => throw new Exception($"Unsupported font loader for Font{fontIndex}: {config.FontLoader}")
+ };
+
+ FontSystem fontSystem = CreateFontSystem(fontLoader);
fontSystem.DefaultCharacter = '?';
fontSystems.Add(fontSystem);
@@ -395,13 +411,15 @@ private readonly struct FontConfig
public string Path { get; }
public int Size { get; }
public FontType FontType { get; }
+ public TTFFontLoader FontLoader { get; }
public int Fallback { get; }
- public FontConfig(string path, int size, FontType fontType, int fallback)
+ public FontConfig(string path, int size, FontType fontType, TTFFontLoader fontLoader, int fallback)
{
Path = path;
Size = size;
FontType = fontType;
+ FontLoader = fontLoader;
Fallback = fallback;
}
}
diff --git a/FontManagement/FreeTypeFontLoader.cs b/FontManagement/FreeTypeFontLoader.cs
new file mode 100644
index 0000000..23e5a18
--- /dev/null
+++ b/FontManagement/FreeTypeFontLoader.cs
@@ -0,0 +1,22 @@
+using FontStashSharp.Interfaces;
+
+namespace Rampastring.XNAUI.FontManagement;
+
+///
+/// FreeType-based font loader that correctly handles embedded bitmap fonts in TTF files.
+/// Replaces FontStashSharp.Rasterizers.FreeType which has broken struct layouts on Windows x64
+/// (FreeTypeSharp maps C 'long' to IntPtr instead of int, causing struct field misalignment).
+///
+public sealed class FreeTypeFontLoader : IFontLoader
+{
+ public FreeTypeRenderMode RenderMode { get; } = FreeTypeRenderMode.Normal;
+
+ public FreeTypeFontLoader() { }
+
+ public FreeTypeFontLoader(FreeTypeRenderMode renderMode)
+ {
+ RenderMode = renderMode;
+ }
+
+ public IFontSource Load(byte[] data) => new FreeTypeFontSource(data) { RenderMode = RenderMode };
+}
diff --git a/FontManagement/FreeTypeFontSource.cs b/FontManagement/FreeTypeFontSource.cs
new file mode 100644
index 0000000..0ff7c76
--- /dev/null
+++ b/FontManagement/FreeTypeFontSource.cs
@@ -0,0 +1,378 @@
+using FontStashSharp.Interfaces;
+using System;
+using System.Runtime.InteropServices;
+
+namespace Rampastring.XNAUI.FontManagement;
+
+///
+/// IFontSource implementation backed by FreeType with correct Windows P/Invoke struct layouts.
+/// Unlike FreeTypeSharp, this correctly maps FT_Pos/FT_Fixed (C 'long') to 'int' on Windows,
+/// where C 'long' is always 4 bytes regardless of 32/64-bit.
+///
+public sealed class FreeTypeFontSource : IFontSource
+{
+ private static IntPtr _library;
+ private GCHandle _memoryHandle;
+ private IntPtr _face;
+
+ public FreeTypeRenderMode RenderMode { get; set; } = FreeTypeRenderMode.Normal;
+
+ public FreeTypeFontSource(byte[] data)
+ {
+ if (_library == IntPtr.Zero)
+ {
+ int err = FT_Init_FreeType(out _library);
+ if (err != 0)
+ throw new InvalidOperationException($"FT_Init_FreeType failed with error {err}");
+ }
+
+ // Pin the font data in memory so FreeType can read from it.
+ _memoryHandle = GCHandle.Alloc(data, GCHandleType.Pinned);
+
+ int error = FT_New_Memory_Face(
+ _library,
+ _memoryHandle.AddrOfPinnedObject(),
+ data.Length,
+ 0,
+ out _face);
+
+ if (error != 0)
+ throw new InvalidOperationException($"FT_New_Memory_Face failed with error {error}");
+ }
+
+ ~FreeTypeFontSource()
+ {
+ Dispose(false);
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (_face != IntPtr.Zero)
+ {
+ FT_Done_Face(_face);
+ _face = IntPtr.Zero;
+ }
+
+ if (_memoryHandle.IsAllocated)
+ _memoryHandle.Free();
+ }
+
+ public int? GetGlyphId(int codepoint)
+ {
+ uint index = FT_Get_Char_Index(_face, (uint)codepoint);
+ return index == 0 ? null : (int?)index;
+ }
+
+ public void GetMetricsForSize(float fontSize, out int ascent, out int descent, out int lineHeight)
+ {
+ SetPixelSizes(0, fontSize);
+
+ IntPtr facePtr = _face;
+ IntPtr sizePtr = ReadFaceSize(facePtr);
+ FT_Size_Metrics metrics = ReadSizeMetrics(sizePtr);
+
+ ascent = metrics.ascender >> 6;
+ descent = metrics.descender >> 6;
+ lineHeight = metrics.height >> 6;
+ }
+
+ public void GetGlyphMetrics(int glyphId, float fontSize, out int advance, out int x0, out int y0, out int x1, out int y1)
+ {
+ SetPixelSizes(0, fontSize);
+ LoadGlyph(glyphId);
+
+ IntPtr slotPtr = ReadFaceGlyphSlot(_face);
+ FT_Glyph_Metrics glyphMetrics = ReadGlyphSlotMetrics(slotPtr);
+
+ advance = glyphMetrics.horiAdvance >> 6;
+ x0 = glyphMetrics.horiBearingX >> 6;
+ y0 = -(glyphMetrics.horiBearingY >> 6);
+ x1 = x0 + (glyphMetrics.width >> 6);
+ y1 = y0 + (glyphMetrics.height >> 6);
+ }
+
+ public void RasterizeGlyphBitmap(int glyphId, float fontSize, byte[] buffer, int startIndex, int outWidth, int outHeight, int outStride)
+ {
+ SetPixelSizes(0, fontSize);
+ LoadGlyph(glyphId);
+
+ IntPtr slotPtr = ReadFaceGlyphSlot(_face);
+
+ int renderMode = RenderMode switch
+ {
+ FreeTypeRenderMode.Normal => (int)FT_Render_Mode_.FT_RENDER_MODE_NORMAL,
+ FreeTypeRenderMode.Light => (int)FT_Render_Mode_.FT_RENDER_MODE_LIGHT,
+ FreeTypeRenderMode.Mono => (int)FT_Render_Mode_.FT_RENDER_MODE_MONO,
+ FreeTypeRenderMode.LCD => (int)FT_Render_Mode_.FT_RENDER_MODE_LCD,
+ FreeTypeRenderMode.LCDV => (int)FT_Render_Mode_.FT_RENDER_MODE_LCD_V,
+ FreeTypeRenderMode.SDF => (int)FT_Render_Mode_.FT_RENDER_MODE_SDF,
+ _ => throw new InvalidOperationException($"Unsupported render mode: {RenderMode}"),
+ };
+
+ FT_Render_Glyph(slotPtr, renderMode);
+
+ FT_Bitmap bitmap = ReadGlyphSlotBitmap(slotPtr);
+
+ for (int y = 0; y < outHeight; y++)
+ {
+ int dstPos = (y * outStride) + startIndex;
+ IntPtr srcRow = bitmap.buffer + (y * bitmap.pitch);
+
+ if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY)
+ {
+ Marshal.Copy(srcRow, buffer, dstPos, Math.Min(outWidth, Math.Abs(bitmap.pitch)));
+ }
+ else if (bitmap.pixel_mode == FT_PIXEL_MODE_MONO)
+ {
+ for (int x = 0; x < outWidth; x += 8)
+ {
+ byte bits = Marshal.ReadByte(srcRow, x / 8);
+ int count = Math.Min(8, outWidth - x);
+ for (int b = 0; b < count; b++)
+ {
+ buffer[dstPos + x + b] = ((bits >> (7 - b)) & 1) != 0 ? (byte)255 : (byte)0;
+ }
+ }
+ }
+ }
+ }
+
+ public int GetGlyphKernAdvance(int previousGlyphId, int glyphId, float fontSize)
+ {
+ int err = FT_Get_Kerning(_face, (uint)previousGlyphId, (uint)glyphId, FT_KERNING_DEFAULT, out FT_Vector kerning);
+ if (err != 0)
+ return 0;
+
+ return kerning.x >> 6;
+ }
+
+ public float CalculateScaleForTextShaper(float fontSize)
+ {
+ ushort unitsPerEM = ReadFaceUnitsPerEM(_face);
+ return fontSize / unitsPerEM;
+ }
+
+ private void SetPixelSizes(float width, float height)
+ {
+ int err = FT_Set_Pixel_Sizes(_face, (uint)width, (uint)height);
+ if (err != 0)
+ throw new InvalidOperationException($"FT_Set_Pixel_Sizes failed with error {err}");
+ }
+
+ private void LoadGlyph(int glyphId)
+ {
+ int err = FT_Load_Glyph(_face, (uint)glyphId, FT_LOAD_DEFAULT | FT_LOAD_COLOR);
+ if (err != 0)
+ throw new InvalidOperationException($"FT_Load_Glyph failed with error {err}");
+ }
+
+ #region Struct field reading via Marshal (correct Windows ABI offsets)
+
+ // On Windows, C 'long' is always 4 bytes (both x86 and x64).
+ // FreeType's FT_Pos and FT_Fixed are typedef'd as 'signed long'.
+ // Pointers are 4 bytes on x86 and 8 bytes on x64.
+ // We compute offsets dynamically based on IntPtr.Size to support both.
+
+ private static readonly int PtrSize = IntPtr.Size;
+
+ // FT_FaceRec field offsets (computed for Windows x86 and x64)
+ // Fields: num_faces(4), face_index(4), face_flags(4), style_flags(4), num_glyphs(4),
+ // [pad to ptr], family_name(ptr), style_name(ptr), num_fixed_sizes(4),
+ // [pad to ptr], available_sizes(ptr), num_charmaps(4), [pad to ptr], charmaps(ptr),
+ // generic(2*ptr), bbox(16), units_per_EM(2), ascender(2), descender(2), height(2),
+ // max_advance_width(2), max_advance_height(2), underline_position(2), underline_thickness(2),
+ // glyph(ptr), size(ptr)
+
+ private static int AlignTo(int offset, int alignment) => (offset + alignment - 1) & ~(alignment - 1);
+
+ private static int ComputeFaceFieldOffset(string field)
+ {
+ int p = PtrSize;
+ int offset = 0;
+
+ // 5 x int (FT_Long = 4 on Windows)
+ offset += 5 * 4; // num_faces, face_index, face_flags, style_flags, num_glyphs = 20
+
+ offset = AlignTo(offset, p);
+ // family_name (ptr)
+ offset += p;
+ // style_name (ptr)
+ offset += p;
+ // num_fixed_sizes (int)
+ offset += 4;
+ offset = AlignTo(offset, p);
+ // available_sizes (ptr)
+ offset += p;
+ // num_charmaps (int)
+ offset += 4;
+ offset = AlignTo(offset, p);
+ // charmaps (ptr)
+ offset += p;
+ // generic: FT_Generic = data(ptr) + finalizer(ptr)
+ offset += 2 * p;
+ // bbox: FT_BBox = 4 x FT_Pos(4) = 16
+ offset += 16;
+
+ if (field == "units_per_EM")
+ return offset;
+
+ // units_per_EM(2) + ascender(2) + descender(2) + height(2)
+ // + max_advance_width(2) + max_advance_height(2) + underline_position(2) + underline_thickness(2)
+ offset += 16;
+ offset = AlignTo(offset, p);
+
+ if (field == "glyph")
+ return offset;
+
+ offset += p; // glyph
+
+ if (field == "size")
+ return offset;
+
+ throw new ArgumentException($"Unknown FT_FaceRec field: {field}");
+ }
+
+ private static readonly int FaceGlyphOffset = ComputeFaceFieldOffset("glyph");
+ private static readonly int FaceSizeOffset = ComputeFaceFieldOffset("size");
+ private static readonly int FaceUnitsPerEMOffset = ComputeFaceFieldOffset("units_per_EM");
+
+ private static IntPtr ReadFaceGlyphSlot(IntPtr face) => Marshal.ReadIntPtr(face, FaceGlyphOffset);
+ private static IntPtr ReadFaceSize(IntPtr face) => Marshal.ReadIntPtr(face, FaceSizeOffset);
+ private static ushort ReadFaceUnitsPerEM(IntPtr face) => (ushort)Marshal.ReadInt16(face, FaceUnitsPerEMOffset);
+
+ // FT_SizeRec: face(ptr) + generic(2*ptr) + FT_Size_Metrics
+ // FT_Size_Metrics: x_ppem(2) + y_ppem(2) + x_scale(4) + y_scale(4) + ascender(4) + descender(4) + height(4) + max_advance(4)
+ private static readonly int SizeMetricsOffset = PtrSize + 2 * PtrSize; // face + generic
+
+ private static FT_Size_Metrics ReadSizeMetrics(IntPtr sizePtr)
+ {
+ return Marshal.PtrToStructure(sizePtr + SizeMetricsOffset);
+ }
+
+ // FT_GlyphSlotRec: library(ptr) + face(ptr) + next(ptr) + glyph_index(4) + [pad to ptr] + generic(2*ptr) + FT_Glyph_Metrics
+ private static readonly int GlyphSlotMetricsOffset = 3 * PtrSize + AlignTo(4, PtrSize) + 2 * PtrSize;
+
+ private static FT_Glyph_Metrics ReadGlyphSlotMetrics(IntPtr slotPtr)
+ {
+ return Marshal.PtrToStructure(slotPtr + GlyphSlotMetricsOffset);
+ }
+
+ // After FT_Glyph_Metrics(32): linearHoriAdvance(4) + linearVertAdvance(4) + advance(8) + format(4) + [pad to bitmap alignment]
+ // FT_Bitmap starts after format, aligned to pointer size (for buffer field)
+ private static readonly int GlyphSlotBitmapOffset = GlyphSlotMetricsOffset + 32 + 4 + 4 + 8 + AlignTo(4, PtrSize);
+
+ private static FT_Bitmap ReadGlyphSlotBitmap(IntPtr slotPtr)
+ {
+ return Marshal.PtrToStructure(slotPtr + GlyphSlotBitmapOffset);
+ }
+
+ #endregion
+
+ #region Native structs (correct for Windows where C 'long' = 4 bytes)
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct FT_Vector
+ {
+ public int x; // FT_Pos = C long = 4 bytes on Windows
+ public int y;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct FT_Size_Metrics
+ {
+ public ushort x_ppem;
+ public ushort y_ppem;
+ public int x_scale; // FT_Fixed = C long = 4 bytes on Windows
+ public int y_scale;
+ public int ascender; // FT_Pos
+ public int descender;
+ public int height;
+ public int max_advance;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct FT_Glyph_Metrics
+ {
+ public int width; // FT_Pos
+ public int height;
+ public int horiBearingX;
+ public int horiBearingY;
+ public int horiAdvance;
+ public int vertBearingX;
+ public int vertBearingY;
+ public int vertAdvance;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct FT_Bitmap
+ {
+ public uint rows;
+ public uint width;
+ public int pitch;
+ public IntPtr buffer;
+ public ushort num_grays;
+ public byte pixel_mode;
+ public byte palette_mode;
+ public IntPtr palette;
+ }
+
+ #endregion
+
+ #region FreeType constants
+
+ private const int FT_LOAD_DEFAULT = 0x0;
+ private const int FT_LOAD_COLOR = 0x20;
+
+ private const uint FT_KERNING_DEFAULT = 0;
+ private const byte FT_PIXEL_MODE_MONO = 1;
+ private const byte FT_PIXEL_MODE_GRAY = 2;
+
+ private enum FT_Render_Mode_ : int
+ {
+ FT_RENDER_MODE_NORMAL = 0,
+ FT_RENDER_MODE_LIGHT = 1,
+ FT_RENDER_MODE_MONO = 2,
+ FT_RENDER_MODE_LCD = 3,
+ FT_RENDER_MODE_LCD_V = 4,
+ FT_RENDER_MODE_SDF = 5,
+ FT_RENDER_MODE_MAX = 6
+ }
+
+ #endregion
+
+ #region P/Invoke declarations
+
+ private const string FreeTypeLib = "freetype";
+
+ [DllImport(FreeTypeLib, CallingConvention = CallingConvention.Cdecl)]
+ private static extern int FT_Init_FreeType(out IntPtr library);
+
+ [DllImport(FreeTypeLib, CallingConvention = CallingConvention.Cdecl)]
+ private static extern int FT_New_Memory_Face(IntPtr library, IntPtr file_base, int file_size, int face_index, out IntPtr face);
+
+ [DllImport(FreeTypeLib, CallingConvention = CallingConvention.Cdecl)]
+ private static extern int FT_Done_Face(IntPtr face);
+
+ [DllImport(FreeTypeLib, CallingConvention = CallingConvention.Cdecl)]
+ private static extern int FT_Set_Pixel_Sizes(IntPtr face, uint pixel_width, uint pixel_height);
+
+ [DllImport(FreeTypeLib, CallingConvention = CallingConvention.Cdecl)]
+ private static extern uint FT_Get_Char_Index(IntPtr face, uint charcode);
+
+ [DllImport(FreeTypeLib, CallingConvention = CallingConvention.Cdecl)]
+ private static extern int FT_Load_Glyph(IntPtr face, uint glyph_index, int load_flags);
+
+ [DllImport(FreeTypeLib, CallingConvention = CallingConvention.Cdecl)]
+ private static extern int FT_Render_Glyph(IntPtr slot, int render_mode);
+
+ [DllImport(FreeTypeLib, CallingConvention = CallingConvention.Cdecl)]
+ private static extern int FT_Get_Kerning(IntPtr face, uint left_glyph, uint right_glyph, uint kern_mode, out FT_Vector kerning);
+
+ #endregion
+}
diff --git a/FontManagement/FreeTypeRenderMode.cs b/FontManagement/FreeTypeRenderMode.cs
new file mode 100644
index 0000000..1eb3123
--- /dev/null
+++ b/FontManagement/FreeTypeRenderMode.cs
@@ -0,0 +1,12 @@
+namespace Rampastring.XNAUI.FontManagement;
+
+public enum FreeTypeRenderMode
+{
+ Normal = 0,
+ Light = 1,
+ Mono = 2,
+ LCD = 3,
+ LCDV = 4,
+ SDF = 5,
+ MAX = 6,
+}
diff --git a/FontManagement/TTFFontLoader.cs b/FontManagement/TTFFontLoader.cs
new file mode 100644
index 0000000..d325524
--- /dev/null
+++ b/FontManagement/TTFFontLoader.cs
@@ -0,0 +1,9 @@
+namespace Rampastring.XNAUI.FontManagement;
+
+public enum TTFFontLoader
+{
+ StbTrueType,
+ FreeType,
+ FreeTypeMono,
+ Forme,
+}
diff --git a/Rampastring.XNAUI.csproj b/Rampastring.XNAUI.csproj
index 528e112..6e3f610 100644
--- a/Rampastring.XNAUI.csproj
+++ b/Rampastring.XNAUI.csproj
@@ -186,6 +186,7 @@
+