using System.Buffers.Binary;
using System.Text;
using LunarLabs.Fonts;
namespace XFESystem.Platform;
public sealed record PlatformFontAssets(byte[] Mono, byte[] Sans, byte[] SansBold, byte[] Cjk, byte[] CjkBold, byte[] Icons,
byte[]? ArialFallback = null, byte[]? ArialFallbackBold = null);
public readonly record struct PlatformTextMask(int Width, int Height, byte[] Alpha);
/// <summary>Font bytes are trusted bundled assets, never downloaded author fonts. Each canvas owns its caches.</summary>
public sealed class PlatformFonts
{
private sealed class Face(byte[] bytes, string name)
{
public readonly Font Font = new(bytes);
public readonly string Name = name;
public readonly int UnitsPerEm = ReadUnitsPerEm(bytes);
public int Ascent, Descent, Gap;
}
private readonly Face mono, sans, bold, cjk, cjkBold, icons, arialFallback, arialFallbackBold;
private readonly PlatformFontAssets assets;
private readonly Dictionary<(Face Face, char Character, double Size), FontGlyph> glyphs = [];
private readonly Dictionary<(string Family, bool Bold), Face> familyChoices = [];
private long cachedGlyphBytes;
public int CachedGlyphCount => glyphs.Count;
public long CachedGlyphBytes => cachedGlyphBytes;
public PlatformFonts(PlatformFontAssets assets)
{
ArgumentNullException.ThrowIfNull(assets);
this.assets = assets;
mono = Load(assets.Mono, "Cascadia Mono"); sans = Load(assets.Sans, "Lato"); bold = Load(assets.SansBold, "Lato Bold");
cjk = Load(assets.Cjk, "XFE Web CJK"); cjkBold = Load(assets.CjkBold, "XFE Web CJK Bold"); icons = Load(assets.Icons, "Bootstrap Icons");
arialFallback = assets.ArialFallback is { } regular ? Load(regular, "Liberation Sans") : sans;
arialFallbackBold = assets.ArialFallbackBold is { } heavy ? Load(heavy, "Liberation Sans Bold") : bold;
}
public static PlatformFonts CreateBundled() => new(Bundled.Assets);
public PlatformFonts CreateIndependent() => new(assets);
internal bool HasSameAssets(PlatformFonts other) => ReferenceEquals(assets, other.assets);
private static class Bundled
{
internal static readonly PlatformFontAssets Assets = new(Read("Mono.ttf"), Read("Sans.ttf"), Read("SansBold.ttf"), Read("Cjk.ttf"), Read("CjkBold.ttf"), Read("Icons.ttf"),
Read("LiberationSans.ttf"), Read("LiberationSansBold.ttf"));
private static byte[] Read(string name) => PlatformResources.Read("PlatformFonts/" + name);
}
private static Face Load(byte[] bytes, string name)
{
var face = new Face(bytes, name);
face.Font.GetFontVMetrics(out face.Ascent, out face.Descent, out face.Gap);
if (face.Ascent <= face.Descent) throw new InvalidDataException("Font vertical metrics are invalid.");
return face;
}
public string DescribeFace(string requested, int weight) => Primary(requested, weight).Name;
public bool HasGlyph(char character, string family, int weight) => Resolve(Primary(family, weight), character, weight).Font.HasGlyph(character);
public double LineHeight(double size, string family, int weight)
{
size = ValidSize(size); var face = Primary(family, weight);
return (face.Ascent - face.Descent + Math.Max(0, face.Gap)) * size / face.UnitsPerEm;
}
public double Measure(string text, double size, string family, int weight)
{
ArgumentNullException.ThrowIfNull(text); size = ValidSize(size);
var primary = Primary(family, weight); double position = 0; Face? previousFace = null; char previous = '\0';
foreach (var rune in text.EnumerateRunes())
{
char ch = Bmp(rune); if (ch is '\r' or '\n') break;
var face = Resolve(primary, ch, weight); ch = Supported(face, ch);
if (ch == '\t') { position += Advance(face, ' ', size) * 4; previousFace = null; continue; }
if (ReferenceEquals(face, previousFace)) position += face.Font.GetKerning(previous, ch, (float)(size / face.UnitsPerEm));
position += Advance(face, ch, size); previousFace = face; previous = ch;
}
return Math.Max(0, position);
}
public PlatformTextMask Rasterize(string text, double size, string family, int weight)
{
ArgumentNullException.ThrowIfNull(text); size = ValidSize(size);
int width = Math.Max(1, (int)Math.Ceiling(Measure(text, size, family, weight)));
int height = Math.Max(1, (int)Math.Ceiling(LineHeight(size, family, weight)));
if ((long)width * height > 4 * 1024 * 1024) throw new ArgumentException("Text coverage exceeds its bounded raster budget.", nameof(text));
byte[] coverage = new byte[width * height]; var primary = Primary(family, weight);
double position = 0; Face? previousFace = null; char previous = '\0';
double baseline = primary.Ascent * size / primary.UnitsPerEm;
foreach (var rune in text.EnumerateRunes())
{
char ch = Bmp(rune); if (ch is '\r' or '\n') break;
var face = Resolve(primary, ch, weight); ch = Supported(face, ch);
if (ch == '\t') { position += Advance(face, ' ', size) * 4; previousFace = null; continue; }
if (ReferenceEquals(face, previousFace)) position += face.Font.GetKerning(previous, ch, (float)(size / face.UnitsPerEm));
// Blank glyphs (including Monaco's U+00A0 non-breaking spaces)
// have real advances but no outline. The unmodified LunarFonts
// bitmap API rejects a zero-sized outline; only advance these.
if (!char.IsWhiteSpace(ch))
{
var glyph = Glyph(face, ch, size);
if (glyph is not null)
{
int gx = (int)Math.Round(position) + glyph.xOfs, gy = (int)Math.Round(baseline) + glyph.yOfs;
for (int y = 0; y < glyph.Image.Height; y++)
{
int targetY = gy + y; if (targetY < 0 || targetY >= height) continue;
for (int x = 0; x < glyph.Image.Width; x++)
{
int targetX = gx + x; if (targetX < 0 || targetX >= width) continue;
int at = targetY * width + targetX; byte alpha = glyph.Image.Pixels[y * glyph.Image.Width + x];
coverage[at] = (byte)(alpha + (coverage[at] * (255 - alpha) + 127) / 255);
}
}
}
}
position += Advance(face, ch, size); previousFace = face; previous = ch;
}
return new(width, height, coverage);
}
private FontGlyph? Glyph(Face face, char character, double size)
{
var key = (face, character, size); if (glyphs.TryGetValue(key, out var found)) return found;
var glyph = face.Font.RenderGlyph(character, (float)(size / face.UnitsPerEm));
if (glyph is null) return null;
if (glyphs.Count < 4096 && cachedGlyphBytes + glyph.Image.Pixels.Length <= 8 * 1024 * 1024)
{ glyphs.Add(key, glyph); cachedGlyphBytes += glyph.Image.Pixels.Length; }
return glyph;
}
private Face Primary(string family, int weight)
{
ArgumentNullException.ThrowIfNull(family);
var key = (family, weight >= 600);
if (familyChoices.TryGetValue(key, out var cached)) return cached;
var selected = ChooseFace(family, weight);
if (familyChoices.Count < 64 && family.Length <= 512) familyChoices.Add(key, selected);
return selected;
}
private Face ChooseFace(string family, int weight)
{
foreach (string item in family.Split(','))
{
string candidate = item.Trim(' ', '\'', '"');
if (candidate.Equals("bootstrap-icons", StringComparison.OrdinalIgnoreCase)) return icons;
if (candidate.Contains("mono", StringComparison.OrdinalIgnoreCase) || candidate.Equals("Consolas", StringComparison.OrdinalIgnoreCase) || candidate.StartsWith("Courier", StringComparison.OrdinalIgnoreCase)) return mono;
if (candidate.Equals("Arial", StringComparison.OrdinalIgnoreCase) || candidate.Equals("Helvetica", StringComparison.OrdinalIgnoreCase) || candidate.Equals("Liberation Sans", StringComparison.OrdinalIgnoreCase))
return weight >= 600 ? arialFallbackBold : arialFallback;
if (candidate.Equals("sans-serif", StringComparison.OrdinalIgnoreCase) || candidate.Equals("system-ui", StringComparison.OrdinalIgnoreCase) || candidate.Equals("Segoe UI", StringComparison.OrdinalIgnoreCase) || candidate.Equals("Lato", StringComparison.OrdinalIgnoreCase))
return weight >= 600 ? bold : sans;
}
return weight >= 600 ? bold : sans;
}
private Face Resolve(Face primary, char ch, int weight)
{
if (primary.Font.HasGlyph(ch)) return primary;
var fallback = weight >= 600 ? cjkBold : cjk;
return fallback.Font.HasGlyph(ch) ? fallback : primary;
}
private static char Supported(Face face, char character) => face.Font.HasGlyph(character) ? character : face.Font.HasGlyph('\uFFFD') ? '\uFFFD' : '?';
private static char Bmp(Rune rune) => rune.Value <= char.MaxValue ? (char)rune.Value : '\uFFFD';
private static double Advance(Face face, char ch, double size)
{ face.Font.GetCodepointHMetrics(ch, out int advance, out _); return advance * size / face.UnitsPerEm; }
private static double ValidSize(double size)
{
if (!double.IsFinite(size) || size <= 0 || size > 1024) throw new ArgumentOutOfRangeException(nameof(size));
return size;
}
private static int ReadUnitsPerEm(byte[] bytes)
{
if (bytes.Length < 12) throw new InvalidDataException("Font header is truncated.");
int count = BinaryPrimitives.ReadUInt16BigEndian(bytes.AsSpan(4, 2));
if (count > 256 || 12L + count * 16 > bytes.Length) throw new InvalidDataException("Font directory is invalid.");
for (int i = 0; i < count; i++)
{
int p = 12 + i * 16;
uint offset = BinaryPrimitives.ReadUInt32BigEndian(bytes.AsSpan(p + 8, 4)), length = BinaryPrimitives.ReadUInt32BigEndian(bytes.AsSpan(p + 12, 4));
if ((ulong)offset + length > (ulong)bytes.Length) throw new InvalidDataException("Font table extends beyond its bytes.");
if (bytes[p] != 'h' || bytes[p + 1] != 'e' || bytes[p + 2] != 'a' || bytes[p + 3] != 'd') continue;
if (length < 54) throw new InvalidDataException("Font head table is truncated.");
int em = BinaryPrimitives.ReadUInt16BigEndian(bytes.AsSpan(checked((int)offset + 18), 2));
if (em is < 16 or > 16384) throw new InvalidDataException("Font em size is invalid.");
return em;
}
throw new InvalidDataException("The font has no head table.");
}
}
internal static class PlatformResources
{
internal static byte[] Read(string name)
{
using var stream = typeof(PlatformResources).Assembly.GetManifestResourceStream(name) ?? throw new InvalidOperationException("Missing bundled resource: " + name);
if (stream.Length > 32 * 1024 * 1024) throw new InvalidDataException("Bundled asset is too large.");
byte[] bytes = new byte[checked((int)stream.Length)]; stream.ReadExactly(bytes); return bytes;
}
}
using System.Buffers.Binary;
using System.Text;
using LunarLabs.Fonts;
namespace XFESystem.Platform;
public sealed record PlatformFontAssets(byte[] Mono, byte[] Sans, byte[] SansBold, byte[] Cjk, byte[] CjkBold, byte[] Icons,
byte[]? ArialFallback = null, byte[]? ArialFallbackBold = null);
public readonly record struct PlatformTextMask(int Width, int Height, byte[] Alpha);
/// <summary>Font bytes are trusted bundled assets, never downloaded author fonts. Each canvas owns its caches.</summary>
public sealed class PlatformFonts
{
private sealed class Face(byte[] bytes, string name)
{
public readonly Font Font = new(bytes);
public readonly string Name = name;
public readonly int UnitsPerEm = ReadUnitsPerEm(bytes);
public int Ascent, Descent, Gap;
}
private readonly Face mono, sans, bold, cjk, cjkBold, icons, arialFallback, arialFallbackBold;
private readonly PlatformFontAssets assets;
private readonly Dictionary<(Face Face, char Character, double Size), FontGlyph> glyphs = [];
private readonly Dictionary<(string Family, bool Bold), Face> familyChoices = [];
private long cachedGlyphBytes;
public int CachedGlyphCount => glyphs.Count;
public long CachedGlyphBytes => cachedGlyphBytes;
public PlatformFonts(PlatformFontAssets assets)
{
ArgumentNullException.ThrowIfNull(assets);
this.assets = assets;
mono = Load(assets.Mono, "Cascadia Mono"); sans = Load(assets.Sans, "Lato"); bold = Load(assets.SansBold, "Lato Bold");
cjk = Load(assets.Cjk, "XFE Web CJK"); cjkBold = Load(assets.CjkBold, "XFE Web CJK Bold"); icons = Load(assets.Icons, "Bootstrap Icons");
arialFallback = assets.ArialFallback is { } regular ? Load(regular, "Liberation Sans") : sans;
arialFallbackBold = assets.ArialFallbackBold is { } heavy ? Load(heavy, "Liberation Sans Bold") : bold;
}
public static PlatformFonts CreateBundled() => new(Bundled.Assets);
public PlatformFonts CreateIndependent() => new(assets);
internal bool HasSameAssets(PlatformFonts other) => ReferenceEquals(assets, other.assets);
private static class Bundled
{
internal static readonly PlatformFontAssets Assets = new(Read("Mono.ttf"), Read("Sans.ttf"), Read("SansBold.ttf"), Read("Cjk.ttf"), Read("CjkBold.ttf"), Read("Icons.ttf"),
Read("LiberationSans.ttf"), Read("LiberationSansBold.ttf"));
private static byte[] Read(string name) => PlatformResources.Read("PlatformFonts/" + name);
}
private static Face Load(byte[] bytes, string name)
{
var face = new Face(bytes, name);
face.Font.GetFontVMetrics(out face.Ascent, out face.Descent, out face.Gap);
if (face.Ascent <= face.Descent) throw new InvalidDataException("Font vertical metrics are invalid.");
return face;
}
public string DescribeFace(string requested, int weight) => Primary(requested, weight).Name;
public bool HasGlyph(char character, string family, int weight) => Resolve(Primary(family, weight), character, weight).Font.HasGlyph(character);
public double LineHeight(double size, string family, int weight)
{
size = ValidSize(size); var face = Primary(family, weight);
return (face.Ascent - face.Descent + Math.Max(0, face.Gap)) * size / face.UnitsPerEm;
}
public double Measure(string text, double size, string family, int weight)
{
ArgumentNullException.ThrowIfNull(text); size = ValidSize(size);
var primary = Primary(family, weight); double position = 0; Face? previousFace = null; char previous = '\0';
foreach (var rune in text.EnumerateRunes())
{
char ch = Bmp(rune); if (ch is '\r' or '\n') break;
var face = Resolve(primary, ch, weight); ch = Supported(face, ch);
if (ch == '\t') { position += Advance(face, ' ', size) * 4; previousFace = null; continue; }
if (ReferenceEquals(face, previousFace)) position += face.Font.GetKerning(previous, ch, (float)(size / face.UnitsPerEm));
position += Advance(face, ch, size); previousFace = face; previous = ch;
}
return Math.Max(0, position);
}
public PlatformTextMask Rasterize(string text, double size, string family, int weight)
{
ArgumentNullException.ThrowIfNull(text); size = ValidSize(size);
int width = Math.Max(1, (int)Math.Ceiling(Measure(text, size, family, weight)));
int height = Math.Max(1, (int)Math.Ceiling(LineHeight(size, family, weight)));
if ((long)width * height > 4 * 1024 * 1024) throw new ArgumentException("Text coverage exceeds its bounded raster budget.", nameof(text));
byte[] coverage = new byte[width * height]; var primary = Primary(family, weight);
double position = 0; Face? previousFace = null; char previous = '\0';
double baseline = primary.Ascent * size / primary.UnitsPerEm;
foreach (var rune in text.EnumerateRunes())
{
char ch = Bmp(rune); if (ch is '\r' or '\n') break;
var face = Resolve(primary, ch, weight); ch = Supported(face, ch);
if (ch == '\t') { position += Advance(face, ' ', size) * 4; previousFace = null; continue; }
if (ReferenceEquals(face, previousFace)) position += face.Font.GetKerning(previous, ch, (float)(size / face.UnitsPerEm));
// Blank glyphs (including Monaco's U+00A0 non-breaking spaces)
// have real advances but no outline. The unmodified LunarFonts
// bitmap API rejects a zero-sized outline; only advance these.
if (!char.IsWhiteSpace(ch))
{
var glyph = Glyph(face, ch, size);
if (glyph is not null)
{
int gx = (int)Math.Round(position) + glyph.xOfs, gy = (int)Math.Round(baseline) + glyph.yOfs;
for (int y = 0; y < glyph.Image.Height; y++)
{
int targetY = gy + y; if (targetY < 0 || targetY >= height) continue;
for (int x = 0; x < glyph.Image.Width; x++)
{
int targetX = gx + x; if (targetX < 0 || targetX >= width) continue;
int at = targetY * width + targetX; byte alpha = glyph.Image.Pixels[y * glyph.Image.Width + x];
coverage[at] = (byte)(alpha + (coverage[at] * (255 - alpha) + 127) / 255);
}
}
}
}
position += Advance(face, ch, size); previousFace = face; previous = ch;
}
return new(width, height, coverage);
}
private FontGlyph? Glyph(Face face, char character, double size)
{
var key = (face, character, size); if (glyphs.TryGetValue(key, out var found)) return found;
var glyph = face.Font.RenderGlyph(character, (float)(size / face.UnitsPerEm));
if (glyph is null) return null;
if (glyphs.Count < 4096 && cachedGlyphBytes + glyph.Image.Pixels.Length <= 8 * 1024 * 1024)
{ glyphs.Add(key, glyph); cachedGlyphBytes += glyph.Image.Pixels.Length; }
return glyph;
}
private Face Primary(string family, int weight)
{
ArgumentNullException.ThrowIfNull(family);
var key = (family, weight >= 600);
if (familyChoices.TryGetValue(key, out var cached)) return cached;
var selected = ChooseFace(family, weight);
if (familyChoices.Count < 64 && family.Length <= 512) familyChoices.Add(key, selected);
return selected;
}
private Face ChooseFace(string family, int weight)
{
foreach (string item in family.Split(','))
{
string candidate = item.Trim(' ', '\'', '"');
if (candidate.Equals("bootstrap-icons", StringComparison.OrdinalIgnoreCase)) return icons;
if (candidate.Contains("mono", StringComparison.OrdinalIgnoreCase) || candidate.Equals("Consolas", StringComparison.OrdinalIgnoreCase) || candidate.StartsWith("Courier", StringComparison.OrdinalIgnoreCase)) return mono;
if (candidate.Equals("Arial", StringComparison.OrdinalIgnoreCase) || candidate.Equals("Helvetica", StringComparison.OrdinalIgnoreCase) || candidate.Equals("Liberation Sans", StringComparison.OrdinalIgnoreCase))
return weight >= 600 ? arialFallbackBold : arialFallback;
if (candidate.Equals("sans-serif", StringComparison.OrdinalIgnoreCase) || candidate.Equals("system-ui", StringComparison.OrdinalIgnoreCase) || candidate.Equals("Segoe UI", StringComparison.OrdinalIgnoreCase) || candidate.Equals("Lato", StringComparison.OrdinalIgnoreCase))
return weight >= 600 ? bold : sans;
}
return weight >= 600 ? bold : sans;
}
private Face Resolve(Face primary, char ch, int weight)
{
if (primary.Font.HasGlyph(ch)) return primary;
var fallback = weight >= 600 ? cjkBold : cjk;
return fallback.Font.HasGlyph(ch) ? fallback : primary;
}
private static char Supported(Face face, char character) => face.Font.HasGlyph(character) ? character : face.Font.HasGlyph('\uFFFD') ? '\uFFFD' : '?';
private static char Bmp(Rune rune) => rune.Value <= char.MaxValue ? (char)rune.Value : '\uFFFD';
private static double Advance(Face face, char ch, double size)
{ face.Font.GetCodepointHMetrics(ch, out int advance, out _); return advance * size / face.UnitsPerEm; }
private static double ValidSize(double size)
{
if (!double.IsFinite(size) || size <= 0 || size > 1024) throw new ArgumentOutOfRangeException(nameof(size));
return size;
}
private static int ReadUnitsPerEm(byte[] bytes)
{
if (bytes.Length < 12) throw new InvalidDataException("Font header is truncated.");
int count = BinaryPrimitives.ReadUInt16BigEndian(bytes.AsSpan(4, 2));
if (count > 256 || 12L + count * 16 > bytes.Length) throw new InvalidDataException("Font directory is invalid.");
for (int i = 0; i < count; i++)
{
int p = 12 + i * 16;
uint offset = BinaryPrimitives.ReadUInt32BigEndian(bytes.AsSpan(p + 8, 4)), length = BinaryPrimitives.ReadUInt32BigEndian(bytes.AsSpan(p + 12, 4));
if ((ulong)offset + length > (ulong)bytes.Length) throw new InvalidDataException("Font table extends beyond its bytes.");
if (bytes[p] != 'h' || bytes[p + 1] != 'e' || bytes[p + 2] != 'a' || bytes[p + 3] != 'd') continue;
if (length < 54) throw new InvalidDataException("Font head table is truncated.");
int em = BinaryPrimitives.ReadUInt16BigEndian(bytes.AsSpan(checked((int)offset + 18), 2));
if (em is < 16 or > 16384) throw new InvalidDataException("Font em size is invalid.");
return em;
}
throw new InvalidDataException("The font has no head table.");
}
}
internal static class PlatformResources
{
internal static byte[] Read(string name)
{
using var stream = typeof(PlatformResources).Assembly.GetManifestResourceStream(name) ?? throw new InvalidOperationException("Missing bundled resource: " + name);
if (stream.Length > 32 * 1024 * 1024) throw new InvalidDataException("Bundled asset is too large.");
byte[] bytes = new byte[checked((int)stream.Length)]; stream.ReadExactly(bytes); return bytes;
}
}