using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using Quik.Media.Color; namespace Quik.Media.Font { public struct FontAtlasGlyphInfo { public int Codepoint; public QImage Image; public QRectangle UVs; } public class FontAtlas { private readonly int width, height; private readonly List atlases = new List(); private readonly Dictionary glyphs = new Dictionary(); private int index = 0; private AtlasPage last = null; private bool isSdf = false; private int expansion; public bool IsSdf { get => isSdf; set { foreach (AtlasPage page in atlases) { (page.Image as QImageBuffer).SetSdf(value); } isSdf = value; } } public FontAtlas(int width, int height, bool isSdf, int expansion = 4) { this.width = width; this.height = height; IsSdf = isSdf; this.expansion = expansion; } public bool GetGlyph(int codepoint, out FontAtlasGlyphInfo info) { return glyphs.TryGetValue(codepoint, out info); } public void PutGlyph(int codepoint, QImageLock source, out FontAtlasGlyphInfo info) { info = new FontAtlasGlyphInfo() { Codepoint = codepoint }; if (last == null || !last.WouldFit(source)) { AddPage(); } last.PutGlyph(source, ref info); } private void AddPage() { index++; if (index < atlases.Count) { last = atlases[index]; } else { last = new AtlasPage(width, height, expansion); (last.Image as QImageBuffer).SetSdf(IsSdf); atlases.Add(last); } } public void Clear() { // Trim any pages that were not used yet. for (int i = atlases.Count -1; i >= 0; i--) { if (atlases[i].PointerX != 0 && atlases[i].PointerY != 0) { for (int j = i + 1; j < atlases.Count; j++) { atlases[j].Dispose(); } if (i != atlases.Count - 1) atlases.RemoveRange(i+1, atlases.Count - i - 1); break; } } if (atlases.Count > 0) { last = atlases[0]; } else { last = null; } index = -1; glyphs.Clear(); } private class AtlasPage : IDisposable { public QImage Image; public int PointerX, PointerY; public int RowHeight; public int Expansion; public bool IsFull => PointerX > Image.Width || PointerY > Image.Height; public AtlasPage(int width, int height, int expansion) { Image = new QImageBuffer(QImageFormat.AlphaU8, width, height); Expansion = expansion; Reset(); } public void PutGlyph(QImageLock src, ref FontAtlasGlyphInfo prototype) { if (IsFull) throw new Exception("Page is full!"); Image.LockBits2d(out QImageLock dst, QImageLockOptions.Default); src.CopyTo(dst, PointerX, PointerY); Image.UnlockBits(); QVec2 min = new QVec2((float)PointerX/Image.Width, (float)PointerY/Image.Height); QVec2 size = new QVec2((float)src.Width/Image.Width, (float)src.Height/Image.Height); prototype.Image = Image; prototype.UVs = new QRectangle(min + size, min); AdvanceColumn(src.Width, src.Height); } public void Reset() { RowHeight = PointerX = PointerY = 0; } public void AdvanceRow() { PointerX = 0; PointerY += RowHeight + Expansion; RowHeight = 0; } public void AdvanceColumn(int width, int height) { RowHeight = Math.Max(RowHeight, height); PointerX += width + Expansion; if (PointerX > Image.Width) { AdvanceRow(); } } private bool isDisposed = false; public void Dispose() { if (isDisposed) return; Image?.Dispose(); isDisposed = true; } internal bool WouldFit(QImageLock source) { return !IsFull || PointerX + source.Width > Image.Width || PointerY + source.Height > Image.Height; } } } }