using System; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Text; using JetBrains.Annotations; using Robust.Client.Utility; using Robust.Shared.Log; using Robust.Shared.Maths; using Robust.Shared.Utility; using SharpFont; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; namespace Robust.Client.Graphics { internal sealed class FontManager : IFontManagerInternal { private const int SheetWidth = 256; private const int SheetHeight = 256; private readonly IClyde _clyde; private readonly ISawmill _sawmill; private uint _baseFontDpi = 96; private readonly Library _library; private readonly Dictionary<(FontFaceHandle, int fontSize), FontInstanceHandle> _loadedInstances = new(); public FontManager(IClyde clyde, ILogManager logManager) { _clyde = clyde; _library = new Library(); _sawmill = logManager.GetSawmill("font"); } public IFontFaceHandle Load(Stream stream, int index = 0) { // Freetype directly operates on the font memory managed by us. // As such, the font data should be pinned in POH. var fontData = stream.CopyToPinnedArray(); return Load(new ArrayMemoryHandle(fontData), index); } public IFontFaceHandle Load(IFontMemoryHandle memory, int index = 0) { var face = FaceLoad(memory, index); var handle = new FontFaceHandle(face, memory); return handle; } public IFontFaceHandle LoadWithPostscriptName(IFontMemoryHandle memory, string postscriptName) { var numFaces = 1; for (var i = 0; i < numFaces; i++) { var face = FaceLoad(memory, i); numFaces = face.FaceCount; if (face.GetPostscriptName() == postscriptName) return new FontFaceHandle(face, memory); face.Dispose(); } // Fallback, load SOMETHING. _sawmill.Warning($"Failed to load correct font via postscript name! {postscriptName}"); return new FontFaceHandle(FaceLoad(memory, 0), memory); } private unsafe Face FaceLoad(IFontMemoryHandle memory, int index) { return new Face(_library, (nint)memory.GetData(), checked((int)memory.GetDataSize()), index); } void IFontManagerInternal.SetFontDpi(uint fontDpi) { _baseFontDpi = fontDpi; } public IFontInstanceHandle MakeInstance(IFontFaceHandle handle, int size) { var fontFaceHandle = (FontFaceHandle) handle; if (_loadedInstances.TryGetValue((fontFaceHandle, size), out var instance)) { return instance; } instance = new FontInstanceHandle(this, size, fontFaceHandle); _loadedInstances.Add((fontFaceHandle, size), instance); return instance; } public void ClearFontCache() { foreach (var fontInstance in _loadedInstances) { fontInstance.Value.ClearSizeData(); } } private ScaledFontData _generateScaledDatum(FontInstanceHandle instance, float scale) { var ftFace = instance.FaceHandle.Face; ftFace.SetCharSize(0, instance.Size, 0, (uint) (_baseFontDpi * scale)); var ascent = ftFace.Size.Metrics.Ascender.ToInt32(); var descent = -ftFace.Size.Metrics.Descender.ToInt32(); var lineHeight = ftFace.Size.Metrics.Height.ToInt32(); var data = new ScaledFontData(ascent, descent, ascent + descent, lineHeight); return data; } private GlyphInfo EnsureGlyphCached(FontInstanceHandle instance, ScaledFontData scaled, float scale, uint glyph) { // Check if already cached. if (scaled.GlyphInfos.TryGetValue(glyph, out var info)) return info; info = new GlyphInfo(); var face = instance.FaceHandle.Face; face.SetCharSize(0, instance.Size, 0, (uint) (_baseFontDpi * scale)); face.LoadGlyph(glyph, LoadFlags.Default, LoadTarget.Normal); face.Glyph.RenderGlyph(RenderMode.Normal); var glyphMetrics = face.Glyph.Metrics; info.Metrics = new CharMetrics(glyphMetrics.HorizontalBearingX.ToInt32(), glyphMetrics.HorizontalBearingY.ToInt32(), glyphMetrics.HorizontalAdvance.ToInt32(), glyphMetrics.Width.ToInt32(), glyphMetrics.Height.ToInt32()); using var bitmap = face.Glyph.Bitmap; if (bitmap.Pitch < 0) { throw new NotImplementedException(); } if (bitmap.Pitch != 0) { Image img; switch (bitmap.PixelMode) { case PixelMode.Mono: { img = MonoBitMapToImage(bitmap); break; } case PixelMode.Gray: { ReadOnlySpan span; unsafe { span = new ReadOnlySpan((void*) bitmap.Buffer, bitmap.Pitch * bitmap.Rows); } img = new Image(bitmap.Width, bitmap.Rows); span.Blit( bitmap.Pitch, UIBox2i.FromDimensions(0, 0, bitmap.Pitch, bitmap.Rows), img, (0, 0)); break; } case PixelMode.Gray2: case PixelMode.Gray4: case PixelMode.Lcd: case PixelMode.VerticalLcd: case PixelMode.Bgra: throw new NotImplementedException(); default: throw new ArgumentOutOfRangeException(); } OwnedTexture sheet; if (scaled.AtlasTextures.Count == 0) sheet = GenSheet(); else sheet = scaled.AtlasTextures[^1]; var (sheetW, sheetH) = sheet.Size; if (sheetW - scaled.CurSheetX < img.Width) { scaled.CurSheetX = 0; // +1 Adds a pixel of vertical padding, to avoid arifacts when aliasing scaled.CurSheetY = scaled.CurSheetMaxY + 1; } if (sheetH - scaled.CurSheetY < img.Height) { // Make new sheet. scaled.CurSheetY = 0; scaled.CurSheetX = 0; scaled.CurSheetMaxY = 0; sheet = GenSheet(); } sheet.SetSubImage((scaled.CurSheetX, scaled.CurSheetY), img); var atlasTexture = new AtlasTexture( sheet, UIBox2.FromDimensions( scaled.CurSheetX, scaled.CurSheetY, bitmap.Width, bitmap.Rows)); info.Texture = atlasTexture; scaled.CurSheetMaxY = Math.Max(scaled.CurSheetMaxY, scaled.CurSheetY + bitmap.Rows); // +1 adds a pixel of horizontal padding, to avoid artifacts when aliasing scaled.CurSheetX += bitmap.Width + 1; } scaled.GlyphInfos.Add(glyph, info); return info; OwnedTexture GenSheet() { var sheet = _clyde.CreateBlankTexture((SheetWidth, SheetHeight), $"font-{face.FamilyName}-{instance.Size}-{(uint) (_baseFontDpi * scale)}-sheet{scaled.AtlasTextures.Count}"); scaled.AtlasTextures.Add(sheet); return sheet; } } private static Image MonoBitMapToImage(FTBitmap bitmap) { DebugTools.Assert(bitmap.PixelMode == PixelMode.Mono); DebugTools.Assert(bitmap.Pitch > 0); ReadOnlySpan span; unsafe { span = new ReadOnlySpan((void*) bitmap.Buffer, bitmap.Rows * bitmap.Pitch); } var bitmapImage = new Image(bitmap.Width, bitmap.Rows); for (var y = 0; y < bitmap.Rows; y++) { for (var x = 0; x < bitmap.Width; x++) { var byteIndex = y * bitmap.Pitch + (x / 8); var bitIndex = x % 8; var bit = (span[byteIndex] & (1 << (7 - bitIndex))) != 0; bitmapImage[x, y] = new A8(bit ? byte.MaxValue : byte.MinValue); } } return bitmapImage; } private sealed class FontFaceHandle : IFontFaceHandle { // Keep this alive to avoid it being GC'd. private readonly IFontMemoryHandle _memoryHandle; public Face Face { get; } public FontFaceHandle(Face face, IFontMemoryHandle memoryHandle) { _memoryHandle = memoryHandle; Face = face; } } [PublicAPI] private sealed class FontInstanceHandle : IFontInstanceHandle { public FontFaceHandle FaceHandle { get; } public int Size { get; } private readonly Dictionary _scaledData = new(); private readonly FontManager _fontManager; public readonly Dictionary GlyphMap; public FontInstanceHandle(FontManager fontManager, int size, FontFaceHandle faceHandle) { GlyphMap = new Dictionary(); _fontManager = fontManager; Size = size; FaceHandle = faceHandle; } public void ClearSizeData() { foreach (var scaleData in _scaledData) { foreach (var ownedTexture in scaleData.Value.AtlasTextures) { ownedTexture.Dispose(); } } _scaledData.Clear(); } public Texture? GetCharTexture(Rune codePoint, float scale) { var glyph = GetGlyph(codePoint); if (glyph == 0) return null; var scaled = GetScaleDatum(scale); var glyphInfo = _fontManager.EnsureGlyphCached(this, scaled, scale, glyph); return glyphInfo.Texture; } public CharMetrics? GetCharMetrics(Rune codePoint, float scale) { var glyph = GetGlyph(codePoint); if (glyph == 0) { return null; } var scaled = GetScaleDatum(scale); var info = _fontManager.EnsureGlyphCached(this, scaled, scale, glyph); return info.Metrics; } public int GetAscent(float scale) { var scaled = GetScaleDatum(scale); return scaled.Ascent; } public int GetDescent(float scale) { var scaled = GetScaleDatum(scale); return scaled.Descent; } public int GetHeight(float scale) { var scaled = GetScaleDatum(scale); return scaled.Height; } public int GetLineHeight(float scale) { var scaled = GetScaleDatum(scale); return scaled.LineHeight; } private uint GetGlyph(Rune chr) { if (GlyphMap.TryGetValue(chr, out var glyph)) { return glyph; } // Check FreeType to see if it exists. var index = FaceHandle.Face.GetCharIndex((uint) chr.Value); GlyphMap.Add(chr, index); return index; } private ScaledFontData GetScaleDatum(float scale) { if (_scaledData.TryGetValue(scale, out var datum)) { return datum; } datum = _fontManager._generateScaledDatum(this, scale); _scaledData.Add(scale, datum); return datum; } } private sealed class ScaledFontData { public ScaledFontData(int ascent, int descent, int height, int lineHeight) { Ascent = ascent; Descent = descent; Height = height; LineHeight = lineHeight; } public readonly List AtlasTextures = new(); public readonly Dictionary GlyphInfos = new(); public readonly int Ascent; public readonly int Descent; public readonly int Height; public readonly int LineHeight; public int CurSheetX; public int CurSheetY; public int CurSheetMaxY; } public sealed class GlyphInfo { public CharMetrics Metrics; public AtlasTexture? Texture; } private sealed class ArrayMemoryHandle(byte[] array) : IFontMemoryHandle { private GCHandle _gcHandle = GCHandle.Alloc(array, GCHandleType.Pinned); public unsafe byte* GetData() { return (byte*) _gcHandle.AddrOfPinnedObject(); } public IntPtr GetDataSize() { return array.Length; } public void Dispose() { _gcHandle.Free(); _gcHandle = default; GC.SuppressFinalize(this); } ~ArrayMemoryHandle() { Dispose(); } } } }