/* AngelCode bitmap font parsing using C# * http://www.cyotek.com/blog/angelcode-bitmap-font-parsing-using-csharp * * Copyright © 2012-2015 Cyotek Ltd. * * Licensed under the MIT License. See license.txt for the full text. */ // Some documentation derived from the BMFont file format specification // http://www.angelcode.com/products/bmfont/doc/file_format.html using System; using System.Collections; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Text; using System.Xml; #if !XENKO using Point = Microsoft.Xna.Framework.Point; using Rectangle = Microsoft.Xna.Framework.Rectangle; #else using Point = Xenko.Core.Mathematics.Point; using Rectangle = Xenko.Core.Mathematics.Rectangle; #endif namespace Cyotek.Drawing.BitmapFont { /// /// A bitmap font. /// /// internal class BitmapFont : IEnumerable { #region Constants /// /// When used with , specifies that no wrapping should occur. /// public const int NoMaxWidth = -1; #endregion #region Properties /// /// Gets or sets the alpha channel. /// /// /// The alpha channel. /// /// /// Set to 0 if the channel holds the glyph data, 1 if it holds the outline, 2 if it holds the glyph and the /// outline, 3 if its set to zero, and 4 if its set to one. /// public int AlphaChannel { get; set; } /// /// Gets or sets the number of pixels from the absolute top of the line to the base of the characters. /// /// /// The number of pixels from the absolute top of the line to the base of the characters. /// public int BaseHeight { get; set; } /// /// Gets or sets the blue channel. /// /// /// The blue channel. /// /// /// Set to 0 if the channel holds the glyph data, 1 if it holds the outline, 2 if it holds the glyph and the /// outline, 3 if its set to zero, and 4 if its set to one. /// public int BlueChannel { get; set; } /// /// Gets or sets a value indicating whether the font is bold. /// /// /// true if the font is bold, otherwise false. /// public bool Bold { get; set; } /// /// Gets or sets the characters that comprise the font. /// /// /// The characters that comprise the font. /// public IDictionary Characters { get; set; } /// /// Gets or sets the name of the OEM charset used. /// /// /// The name of the OEM charset used (when not unicode). /// public string Charset { get; set; } /// /// Gets or sets the name of the true type font. /// /// /// The font family name. /// public string FamilyName { get; set; } /// /// Gets or sets the size of the font. /// /// /// The size of the font. /// public int FontSize { get; set; } /// /// Gets or sets the green channel. /// /// /// The green channel. /// /// /// Set to 0 if the channel holds the glyph data, 1 if it holds the outline, 2 if it holds the glyph and the /// outline, 3 if its set to zero, and 4 if its set to one. /// public int GreenChannel { get; set; } /// /// Gets or sets a value indicating whether the font is italic. /// /// /// true if the font is italic, otherwise false. /// public bool Italic { get; set; } /// /// Indexer to get items within this collection using array index syntax. /// /// The character. /// /// The indexed item. /// public Character this[char character] => Characters[character]; /// /// Gets or sets the character kernings for the font. /// /// /// The character kernings for the font. /// public IDictionary Kernings { get; set; } /// /// Gets or sets the distance in pixels between each line of text. /// /// /// The distance in pixels between each line of text. /// public int LineHeight { get; set; } /// /// Gets or sets the outline thickness for the characters. /// /// /// The outline thickness for the characters. /// public int OutlineSize { get; set; } /// /// Gets or sets a value indicating whether the monochrome characters have been packed into each of the texture /// channels. /// /// /// true if the characters are packed, otherwise false. /// /// /// When packed, the property describes what is stored in each channel. /// public bool Packed { get; set; } /// /// Gets or sets the padding for each character. /// /// /// The padding for each character. /// public Padding Padding { get; set; } /// /// Gets or sets the texture pages for the font. /// /// /// The pages. /// public Page[] Pages { get; set; } /// /// Gets or sets the red channel. /// /// /// The red channel. /// /// /// Set to 0 if the channel holds the glyph data, 1 if it holds the outline, 2 if it holds the glyph and the /// outline, 3 if its set to zero, and 4 if its set to one. /// public int RedChannel { get; set; } /// /// Gets or sets a value indicating whether the font is smoothed. /// /// /// true if the font is smoothed, otherwise false. /// public bool Smoothed { get; set; } /// /// Gets or sets the spacing for each character. /// /// /// The spacing for each character. /// public Point Spacing { get; set; } /// /// Gets or sets the font height stretch. /// /// /// The font height stretch. /// /// 100% means no stretch. public int StretchedHeight { get; set; } /// /// Gets or sets the level of super sampling used by the font. /// /// /// The super sampling level of the font. /// /// A value of 1 indicates no super sampling is in use. public int SuperSampling { get; set; } /// /// Gets or sets the size of the texture images used by the font. /// /// /// The size of the texture. /// public Size TextureSize { get; set; } /// /// Gets or sets a value indicating whether the font is unicode. /// /// /// true if the font is unicode, otherwise false. /// public bool Unicode { get; set; } #endregion #region Methods /// /// Gets the kerning for the specified character combination. /// /// The previous character. /// The current character. /// /// The spacing between the specified characters. /// public int GetKerning(char previous, char current) { Kerning key; int result; key = new Kerning(previous, current, 0); if (!Kernings.TryGetValue(key, out result)) result = 0; return result; } /// /// Load font information from the specified . /// /// Thrown when one or more required arguments are null. /// /// Thrown when one or more arguments have unsupported or /// illegal values. /// /// Thrown when an Invalid Data error condition occurs. /// The stream to load. public virtual void Load(Stream stream) { byte[] buffer; string header; if (stream == null) throw new ArgumentNullException("stream"); if (!stream.CanSeek) throw new ArgumentException("Stream must be seekable in order to determine file format.", "stream"); // read the first five bytes so we can try and work out what the format is // then reset the position so the format loaders can work buffer = new byte[5]; stream.Read(buffer, 0, 5); stream.Seek(0, SeekOrigin.Begin); header = Encoding.ASCII.GetString(buffer); switch (header) { case "info ": LoadText(stream); break; case " /// Load font information from the specified file. /// /// Thrown when one or more required arguments are null. /// Thrown when the requested file is not present. /// The file name to load. public void Load(string fileName) { if (string.IsNullOrEmpty(fileName)) throw new ArgumentNullException("fileName"); if (!File.Exists(fileName)) throw new FileNotFoundException(string.Format("Cannot find file '{0}'.", fileName), fileName); using (Stream stream = File.OpenRead(fileName)) { Load(stream); } BitmapFontLoader.QualifyResourcePaths(this, Path.GetDirectoryName(fileName)); } /// /// Loads font information from the specified string. /// /// String containing the font to load. /// The source data must be in BMFont text format. public void LoadText(string text) { using (var reader = new StringReader(text)) { LoadText(reader); } } /// /// Loads font information from the specified stream. /// /// /// The source data must be in BMFont text format. /// /// Thrown when one or more required arguments are null. /// The stream containing the font to load. public void LoadText(Stream stream) { if (stream == null) throw new ArgumentNullException("stream"); using (TextReader reader = new StreamReader(stream)) { LoadText(reader); } } /// /// Loads font information from the specified . /// /// /// The source data must be in BMFont text format. /// /// Thrown when one or more required arguments are null. /// The TextReader used to feed the data into the font. public virtual void LoadText(TextReader reader) { IDictionary pageData; IDictionary kerningDictionary; IDictionary charDictionary; string line; if (reader == null) throw new ArgumentNullException("reader"); pageData = new SortedDictionary(); kerningDictionary = new Dictionary(); charDictionary = new Dictionary(); do { line = reader.ReadLine(); if (line != null) { string[] parts; parts = BitmapFontLoader.Split(line, ' '); if (parts.Length != 0) switch (parts[0]) { case "info": FamilyName = BitmapFontLoader.GetNamedString(parts, "face"); FontSize = BitmapFontLoader.GetNamedInt(parts, "size"); Bold = BitmapFontLoader.GetNamedBool(parts, "bold"); Italic = BitmapFontLoader.GetNamedBool(parts, "italic"); Charset = BitmapFontLoader.GetNamedString(parts, "charset"); Unicode = BitmapFontLoader.GetNamedBool(parts, "unicode"); StretchedHeight = BitmapFontLoader.GetNamedInt(parts, "stretchH"); Smoothed = BitmapFontLoader.GetNamedBool(parts, "smooth"); SuperSampling = BitmapFontLoader.GetNamedInt(parts, "aa"); Padding = BitmapFontLoader.ParsePadding( BitmapFontLoader.GetNamedString(parts, "padding")); Spacing = BitmapFontLoader.ParsePoint( BitmapFontLoader.GetNamedString(parts, "spacing")); OutlineSize = BitmapFontLoader.GetNamedInt(parts, "outline"); break; case "common": LineHeight = BitmapFontLoader.GetNamedInt(parts, "lineHeight"); BaseHeight = BitmapFontLoader.GetNamedInt(parts, "base"); TextureSize = new Size(BitmapFontLoader.GetNamedInt(parts, "scaleW"), BitmapFontLoader.GetNamedInt(parts, "scaleH")); Packed = BitmapFontLoader.GetNamedBool(parts, "packed"); AlphaChannel = BitmapFontLoader.GetNamedInt(parts, "alphaChnl"); RedChannel = BitmapFontLoader.GetNamedInt(parts, "redChnl"); GreenChannel = BitmapFontLoader.GetNamedInt(parts, "greenChnl"); BlueChannel = BitmapFontLoader.GetNamedInt(parts, "blueChnl"); break; case "page": int id; string name; id = BitmapFontLoader.GetNamedInt(parts, "id"); name = BitmapFontLoader.GetNamedString(parts, "file"); pageData.Add(id, new Page(id, name)); break; case "char": Character charData; charData = new Character { Char = (char)BitmapFontLoader.GetNamedInt(parts, "id"), Bounds = new Rectangle(BitmapFontLoader.GetNamedInt(parts, "x"), BitmapFontLoader.GetNamedInt(parts, "y"), BitmapFontLoader.GetNamedInt(parts, "width"), BitmapFontLoader.GetNamedInt(parts, "height")), Offset = new Point(BitmapFontLoader.GetNamedInt(parts, "xoffset"), BitmapFontLoader.GetNamedInt(parts, "yoffset")), XAdvance = BitmapFontLoader.GetNamedInt(parts, "xadvance"), TexturePage = BitmapFontLoader.GetNamedInt(parts, "page"), Channel = BitmapFontLoader.GetNamedInt(parts, "chnl") }; charDictionary.Add(charData.Char, charData); break; case "kerning": Kerning key; key = new Kerning((char)BitmapFontLoader.GetNamedInt(parts, "first"), (char)BitmapFontLoader.GetNamedInt(parts, "second"), BitmapFontLoader.GetNamedInt(parts, "amount")); if (!kerningDictionary.ContainsKey(key)) kerningDictionary.Add(key, key.Amount); break; } } } while (line != null); Pages = BitmapFontLoader.ToArray(pageData.Values); Characters = charDictionary; Kernings = kerningDictionary; } /// /// Loads font information from the specified string. /// /// String containing the font to load. /// The source data must be in BMFont XML format. public void LoadXml(string xml) { using (var reader = new StringReader(xml)) { LoadXml(reader); } } /// /// Loads font information from the specified . /// /// /// The source data must be in BMFont XML format. /// /// Thrown when one or more required arguments are null. /// The TextReader used to feed the data into the font. public virtual void LoadXml(TextReader reader) { XmlDocument document; IDictionary pageData; IDictionary kerningDictionary; IDictionary charDictionary; XmlNode root; XmlNode properties; if (reader == null) throw new ArgumentNullException("reader"); document = new XmlDocument(); pageData = new SortedDictionary(); kerningDictionary = new Dictionary(); charDictionary = new Dictionary(); document.Load(reader); root = document.DocumentElement; // load the basic attributes properties = root.SelectSingleNode("info"); FamilyName = properties.Attributes["face"].Value; FontSize = Convert.ToInt32(properties.Attributes["size"].Value); Bold = Convert.ToInt32(properties.Attributes["bold"].Value) != 0; Italic = Convert.ToInt32(properties.Attributes["italic"].Value) != 0; Unicode = Convert.ToInt32(properties.Attributes["unicode"].Value) != 0; StretchedHeight = Convert.ToInt32(properties.Attributes["stretchH"].Value); Charset = properties.Attributes["charset"].Value; Smoothed = Convert.ToInt32(properties.Attributes["smooth"].Value) != 0; SuperSampling = Convert.ToInt32(properties.Attributes["aa"].Value); Padding = BitmapFontLoader.ParsePadding(properties.Attributes["padding"].Value); Spacing = BitmapFontLoader.ParsePoint(properties.Attributes["spacing"].Value); OutlineSize = Convert.ToInt32(properties.Attributes["outline"].Value); // common attributes properties = root.SelectSingleNode("common"); BaseHeight = Convert.ToInt32(properties.Attributes["base"].Value); LineHeight = Convert.ToInt32(properties.Attributes["lineHeight"].Value); TextureSize = new Size(Convert.ToInt32(properties.Attributes["scaleW"].Value), Convert.ToInt32(properties.Attributes["scaleH"].Value)); Packed = Convert.ToInt32(properties.Attributes["packed"].Value) != 0; AlphaChannel = Convert.ToInt32(properties.Attributes["alphaChnl"].Value); RedChannel = Convert.ToInt32(properties.Attributes["redChnl"].Value); GreenChannel = Convert.ToInt32(properties.Attributes["greenChnl"].Value); BlueChannel = Convert.ToInt32(properties.Attributes["blueChnl"].Value); // load texture information foreach (XmlNode node in root.SelectNodes("pages/page")) { Page page; page = new Page(); page.Id = Convert.ToInt32(node.Attributes["id"].Value); page.FileName = node.Attributes["file"].Value; pageData.Add(page.Id, page); } Pages = BitmapFontLoader.ToArray(pageData.Values); // load character information foreach (XmlNode node in root.SelectNodes("chars/char")) { Character character; character = new Character(); character.Char = (char)Convert.ToInt32(node.Attributes["id"].Value); character.Bounds = new Rectangle(Convert.ToInt32(node.Attributes["x"].Value), Convert.ToInt32(node.Attributes["y"].Value), Convert.ToInt32(node.Attributes["width"].Value), Convert.ToInt32(node.Attributes["height"].Value)); character.Offset = new Point(Convert.ToInt32(node.Attributes["xoffset"].Value), Convert.ToInt32(node.Attributes["yoffset"].Value)); character.XAdvance = Convert.ToInt32(node.Attributes["xadvance"].Value); character.TexturePage = Convert.ToInt32(node.Attributes["page"].Value); character.Channel = Convert.ToInt32(node.Attributes["chnl"].Value); charDictionary.Add(character.Char, character); } Characters = charDictionary; // loading kerning information foreach (XmlNode node in root.SelectNodes("kernings/kerning")) { Kerning key; key = new Kerning((char)Convert.ToInt32(node.Attributes["first"].Value), (char)Convert.ToInt32(node.Attributes["second"].Value), Convert.ToInt32(node.Attributes["amount"].Value)); if (!kerningDictionary.ContainsKey(key)) kerningDictionary.Add(key, key.Amount); } Kernings = kerningDictionary; } /// /// Loads font information from the specified stream. /// /// /// The source data must be in BMFont XML format. /// /// Thrown when one or more required arguments are null. /// The stream containing the font to load. public void LoadXml(Stream stream) { if (stream == null) throw new ArgumentNullException("stream"); using (TextReader reader = new StreamReader(stream)) { LoadXml(reader); } } /// /// Provides the size, in pixels, of the specified text when drawn with this font. /// /// The text to measure. /// /// The , in pixels, of drawn with this font. /// public Size MeasureFont(string text) { return MeasureFont(text, NoMaxWidth); } /// /// Provides the size, in pixels, of the specified text when drawn with this font, automatically wrapping to keep /// within the specified with. /// /// The text to measure. /// The maximum width. /// /// The , in pixels, of drawn with this font. /// /// /// The MeasureText method uses the parameter to automatically wrap when determining /// text size. /// public Size MeasureFont(string text, double maxWidth) { Size result; if (!string.IsNullOrEmpty(text)) { char previousCharacter; int currentLineWidth; int currentLineHeight; int blockWidth; int blockHeight; int length; List lineHeights; length = text.Length; previousCharacter = ' '; currentLineWidth = 0; currentLineHeight = LineHeight; blockWidth = 0; blockHeight = 0; lineHeights = new List(); for (var i = 0; i < length; i++) { char character; character = text[i]; if (character == '\n' || character == '\r') { if (character == '\n' || i + 1 == length || text[i + 1] != '\n') { lineHeights.Add(currentLineHeight); blockWidth = Math.Max(blockWidth, currentLineWidth); currentLineWidth = 0; currentLineHeight = LineHeight; } } else { Character data; int width; data = this[character]; width = data.XAdvance + GetKerning(previousCharacter, character); if (maxWidth != NoMaxWidth && currentLineWidth + width >= maxWidth) { lineHeights.Add(currentLineHeight); blockWidth = Math.Max(blockWidth, currentLineWidth); currentLineWidth = 0; currentLineHeight = LineHeight; } currentLineWidth += width; currentLineHeight = Math.Max(currentLineHeight, data.Bounds.Height + data.Offset.Y); previousCharacter = character; } } // finish off the current line if required if (currentLineHeight != 0) lineHeights.Add(currentLineHeight); // reduce any lines other than the last back to the base for (var i = 0; i < lineHeights.Count - 1; i++) lineHeights[i] = LineHeight; // calculate the final block height foreach (var lineHeight in lineHeights) blockHeight += lineHeight; result = new Size(Math.Max(currentLineWidth, blockWidth), blockHeight); } else { result = Size.Empty; } return result; } #endregion #region IEnumerable Interface /// /// Returns an enumerator that iterates through the collection. /// /// /// A that can be used to iterate through /// the collection. /// /// public IEnumerator GetEnumerator() { foreach (var pair in Characters) yield return pair.Value; } /// /// Gets the enumerator. /// /// /// The enumerator. /// IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion } }