using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using SharpFont; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace Barotrauma { public class ScalableFont { private static List FontList = new List(); private static Library Lib = null; private string filename; private Face face; private uint size; private int baseHeight; //private int lineHeight; private Dictionary texCoords; private List textures; private GraphicsDevice graphicsDevice; public uint Size { get { return size; } set { size = value; if (graphicsDevice!=null) RenderAtlas(graphicsDevice, charRanges, texDims, baseChar); } } private uint[] charRanges; private int texDims; private uint baseChar; private struct GlyphData { public int texIndex; public Vector2 drawOffset; public float advance; public Rectangle texCoords; } public ScalableFont(string filename, uint size, GraphicsDevice gd=null) { if (Lib == null) Lib = new Library(); this.filename = filename; this.face = null; foreach (ScalableFont font in FontList) { if (font.filename == filename) { this.face = font.face; break; } } if (this.face == null) { this.face = new Face(Lib, filename); } this.size = size; this.textures = new List(); this.texCoords = new Dictionary(); if (gd != null) RenderAtlas(gd); FontList.Add(this); } /// /// Renders the font into at least one texture atlas, which is simply a collection of all glyphs in the ranges defined by charRanges. /// Don't call this too often or with very large sizes. /// /// Graphics device, required to create textures. /// Character ranges between each even element with their corresponding odd element. Default is 0x20 to 0xFFFF. /// Texture dimensions. Default is 512x512. /// Base character used to shift all other characters downwards when rendering. Defaults to T. public void RenderAtlas(GraphicsDevice gd, uint[] charRanges = null, int texDims = 512, uint baseChar = 0x54) { if (charRanges == null) { charRanges = new uint[] { 0x20, 0xFFFF }; } this.charRanges = charRanges; this.texDims = texDims; this.baseChar = baseChar; face.SetPixelSizes(0, size); textures.ForEach(t => t.Dispose()); textures.Clear(); texCoords.Clear(); uint[] pixelBuffer = new uint[texDims * texDims]; for (int i = 0; i < texDims * texDims; i++) { pixelBuffer[i] = 0; } textures.Add(new Texture2D(gd, texDims, texDims, false, SurfaceFormat.Color)); int texIndex = 0; Vector2 currentCoords = Vector2.Zero; int nextY = 0; face.LoadGlyph(face.GetCharIndex(baseChar), LoadFlags.Default, LoadTarget.Normal); baseHeight = face.Glyph.Metrics.Height.ToInt32(); //lineHeight = baseHeight; for (int i = 0; i < charRanges.Count(); i += 2) { uint start = charRanges[i]; uint end = charRanges[i + 1]; for (uint j = start; j <= end; j++) { uint glyphIndex = face.GetCharIndex(j); if (glyphIndex == 0) continue; face.LoadGlyph(glyphIndex, LoadFlags.Default, LoadTarget.Normal); if (face.Glyph.Metrics.Width == 0 || face.Glyph.Metrics.Height == 0) { if (face.Glyph.Metrics.HorizontalAdvance > 0) { //glyph is empty, but char still applies advance GlyphData blankData = new GlyphData(); blankData.advance = (float)face.Glyph.Metrics.HorizontalAdvance; blankData.texIndex = -1; //indicates no texture because the glyph is empty texCoords.Add(j, blankData); } continue; } //stacktrace doesn't really work that well when RenderGlyph throws an exception face.Glyph.RenderGlyph(RenderMode.Normal); byte[] bitmap = face.Glyph.Bitmap.BufferData; int glyphWidth = face.Glyph.Bitmap.Width; int glyphHeight = bitmap.Count() / glyphWidth; //if (glyphHeight>lineHeight) lineHeight=glyphHeight; if (glyphWidth > texDims - 1 || glyphHeight > texDims - 1) { throw new Exception(filename + ", " + size.ToString() + ", "+ (char)j + "; Glyph dimensions exceed texture atlas dimensions"); } nextY = Math.Max(nextY, glyphHeight + 2); if (currentCoords.X + glyphWidth + 2 > texDims - 1) { currentCoords.X = 0; currentCoords.Y += nextY; nextY = 0; } if (currentCoords.Y + glyphHeight + 2 > texDims - 1) { currentCoords.X = 0; currentCoords.Y = 0; textures[texIndex].SetData(pixelBuffer); textures.Add(new Texture2D(gd, texDims, texDims, false, SurfaceFormat.Color)); texIndex++; for (int k = 0; k < texDims * texDims; k++) { pixelBuffer[k] = 0; } } GlyphData newData = new GlyphData(); newData.advance = (float)face.Glyph.Metrics.HorizontalAdvance; newData.texIndex = texIndex; newData.texCoords = new Rectangle((int)currentCoords.X, (int)currentCoords.Y, glyphWidth, glyphHeight); newData.drawOffset = new Vector2(face.Glyph.BitmapLeft, baseHeight*14/10 - face.Glyph.BitmapTop); texCoords.Add(j, newData); for (int y = 0; y < glyphHeight; y++) { for (int x = 0; x < glyphWidth; x++) { byte byteColor = bitmap[x + y * glyphWidth]; pixelBuffer[((int)currentCoords.X + x) + ((int)currentCoords.Y + y) * texDims] = (uint)(byteColor << 24 | byteColor << 16 | byteColor << 8 | byteColor); } } currentCoords.X += glyphWidth + 2; } textures[texIndex].SetData(pixelBuffer); } graphicsDevice = gd; } public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth) { if (textures.Count == 0) return; int lineNum = 0; Vector2 currentPos = position; Vector2 advanceUnit = new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)); for (int i = 0; i < text.Length; i++) { if (text[i]=='\n') { lineNum++; currentPos = position; currentPos.X += baseHeight * 18 / 10 * lineNum * advanceUnit.Y * scale.Y; currentPos.Y += baseHeight * 18 / 10 * lineNum * advanceUnit.X * scale.Y; continue; } uint charIndex = text[i]; GlyphData gd; if (texCoords.TryGetValue(charIndex, out gd)) { if (gd.texIndex >= 0) { Texture2D tex = textures[gd.texIndex]; Vector2 drawOffset; drawOffset.X = gd.drawOffset.X * advanceUnit.X - gd.drawOffset.Y * advanceUnit.Y; drawOffset.Y = gd.drawOffset.X * advanceUnit.Y + gd.drawOffset.Y * advanceUnit.X; sb.Draw(tex, currentPos + drawOffset, null, gd.texCoords, origin, rotation, scale, color, se, layerDepth); } currentPos += gd.advance * advanceUnit * scale.X; } } } public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth) { DrawString(sb, text, position, color, rotation, origin, new Vector2(scale), se, layerDepth); } public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color) { if (textures.Count == 0) return; Vector2 currentPos = position; for (int i = 0; i < text.Length; i++) { if (text[i] == '\n') { currentPos.X = position.X; currentPos.Y += baseHeight * 18 / 10; continue; } uint charIndex = text[i]; GlyphData gd; if (texCoords.TryGetValue(charIndex, out gd)) { if (gd.texIndex >= 0) { Texture2D tex = textures[gd.texIndex]; sb.Draw(tex, currentPos + gd.drawOffset, gd.texCoords, color); } currentPos.X += gd.advance; } } } public Vector2 MeasureString(string text) { float currentLineX = 0.0f; Vector2 retVal = Vector2.Zero; retVal.Y = baseHeight*18/10; for (int i = 0; i < text.Length; i++) { if (text[i] == '\n') { currentLineX = 0.0f; retVal.Y += baseHeight*18/10; continue; } uint charIndex = text[i]; GlyphData gd; if (texCoords.TryGetValue(charIndex, out gd)) { currentLineX += gd.advance; } retVal.X = Math.Max(retVal.X,currentLineX); } return retVal; } } }