XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
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;
    }
}