diff --git a/.editorconfig b/.editorconfig index e8cef19a..9f90c20d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,7 +7,7 @@ indent_style = space insert_final_newline = true trim_trailing_whitespace = true -file_header_template = Copyright (c) Christopher Whitley. All rights reserved.\nLicensed under the MIT license.\nSee LICENSE file in the project root for full license information. +file_header_template = Copyright (c) Craftwork Games. All rights reserved.\nLicensed under the MIT license.\nSee LICENSE file in the project root for full license information. ################################################################################ ### Xml files diff --git a/CHANGELOG.md b/CHANGELOG.md index 35df1dbe..217bfa48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated solution (.sln) to Visual Studio 2022 [@nkast](https://github.com/nkast) [#880](https://github.com/craftworkgames/MonoGame.Extended/pull/880) - Updated project types to .Net.Sdk [@nkast](https://github.com/nkast) [#880](https://github.com/craftworkgames/MonoGame.Extended/pull/880) - Use `System.IO.Compression.ZLibStream` [@nkast](https://github.com/nkast) [#822](https://github.com/craftworkgames/MonoGame.Extended/pull/882) +- `BitmapFont` refactored. Now supports all three BMFont export types (XML, Text, and Binary). [@AristurtleDev](https://github.com/AristurtleDev) [#877](https://github.com/craftworkgames/MonoGame.Extended/pull/887) ### Removed - All projects now output build artifacts to a common `.artifacts` directory at the root of the repository. [@AristurtleDev](https://github.com/AristurtleDev) [#865](https://github.com/craftworkgames/MonoGame.Extended/pull/865) @@ -29,7 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removed `Size3`. It was redundant since `Microsoft.Xna.Framework.Vector3` exists. [@AristurtleDev](https://github.com/AristurtleDev) [#872](https://github.com/craftworkgames/MonoGame.Extended/pull/872) - Removed `Point3`. It was redundant since `Microsoft.Xna.Framework.Vector3` exists. [@AristurtleDev](https://github.com/AristurtleDev) [#874](https://github.com/craftworkgames/MonoGame.Extended/pull/874) - Removed `Point2`. It was redundant since `Microsoft.Xna.Framework.Vector2` exists. [@AristurtleDev](https://github.com/AristurtleDev) [#875](https://github.com/craftworkgames/MonoGame.Extended/pull/875) -- Removed unneccessary dependency introduced by previous pull requests [@nkast](https://github.com/nkast) [#881](https://github.com/craftworkgames/MonoGame.Extended/pull/881) +- Removed unnecessary dependency introduced by previous pull requests [@nkast](https://github.com/nkast) [#881](https://github.com/craftworkgames/MonoGame.Extended/pull/881) ### Fixed diff --git a/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontChar.cs b/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontChar.cs deleted file mode 100644 index c012da4d..00000000 --- a/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontChar.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Xml.Serialization; - -namespace MonoGame.Extended.Content.Pipeline.BitmapFonts -{ - // ---- AngelCode BmFont XML serializer ---------------------- - // ---- By DeadlyDan @ deadlydan@gmail.com ------------------- - // ---- There's no license restrictions, use as you will. ---- - // ---- Credits to http://www.angelcode.com/ ----------------- - public class BitmapFontChar - { - [XmlAttribute("id")] - public int Id { get; set; } - - [XmlAttribute("x")] - public int X { get; set; } - - [XmlAttribute("y")] - public int Y { get; set; } - - [XmlAttribute("width")] - public int Width { get; set; } - - [XmlAttribute("height")] - public int Height { get; set; } - - [XmlAttribute("xoffset")] - public int XOffset { get; set; } - - [XmlAttribute("yoffset")] - public int YOffset { get; set; } - - [XmlAttribute("xadvance")] - public int XAdvance { get; set; } - - [XmlAttribute("page")] - public int Page { get; set; } - - [XmlAttribute("chnl")] - public int Channel { get; set; } - } -} \ No newline at end of file diff --git a/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontCommon.cs b/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontCommon.cs deleted file mode 100644 index 6247fcf0..00000000 --- a/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontCommon.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Xml.Serialization; - -namespace MonoGame.Extended.Content.Pipeline.BitmapFonts -{ - // ---- AngelCode BmFont XML serializer ---------------------- - // ---- By DeadlyDan @ deadlydan@gmail.com ------------------- - // ---- There's no license restrictions, use as you will. ---- - // ---- Credits to http://www.angelcode.com/ ----------------- - public class BitmapFontCommon - { - [XmlAttribute("lineHeight")] - public int LineHeight { get; set; } - - [XmlAttribute("base")] - public int Base { get; set; } - - [XmlAttribute("scaleW")] - public int ScaleW { get; set; } - - [XmlAttribute("scaleH")] - public int ScaleH { get; set; } - - [XmlAttribute("pages")] - public int Pages { get; set; } - - [XmlAttribute("packed")] - public int Packed { get; set; } - - [XmlAttribute("alphaChnl")] - public int AlphaChannel { get; set; } - - [XmlAttribute("redChnl")] - public int RedChannel { get; set; } - - [XmlAttribute("greenChnl")] - public int GreenChannel { get; set; } - - [XmlAttribute("blueChnl")] - public int BlueChannel { get; set; } - } -} \ No newline at end of file diff --git a/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontFile.cs b/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontFile.cs deleted file mode 100644 index db772708..00000000 --- a/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontFile.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using System.Xml.Serialization; - -namespace MonoGame.Extended.Content.Pipeline.BitmapFonts -{ - // ---- AngelCode BmFont XML serializer ---------------------- - // ---- By DeadlyDan @ deadlydan@gmail.com ------------------- - // ---- There's no license restrictions, use as you will. ---- - // ---- Credits to http://www.angelcode.com/ ----------------- - [XmlRoot("font")] - public class BitmapFontFile - { - [XmlElement("info")] - public BitmapFontInfo Info { get; set; } - - [XmlElement("common")] - public BitmapFontCommon Common { get; set; } - - [XmlArray("pages")] - [XmlArrayItem("page")] - public List Pages { get; set; } - - [XmlArray("chars")] - [XmlArrayItem("char")] - public List Chars { get; set; } - - [XmlArray("kernings")] - [XmlArrayItem("kerning")] - public List Kernings { get; set; } - } -} \ No newline at end of file diff --git a/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontImporter.cs b/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontImporter.cs index ea4c5281..b2d3161b 100644 --- a/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontImporter.cs +++ b/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontImporter.cs @@ -1,22 +1,22 @@ using System.IO; using System.Xml.Serialization; using Microsoft.Xna.Framework.Content.Pipeline; +using MonoGame.Extended.BitmapFonts; +using MonoGame.Extended.BitmapFonts.BmfTypes; namespace MonoGame.Extended.Content.Pipeline.BitmapFonts { [ContentImporter(".fnt", DefaultProcessor = "BitmapFontProcessor", DisplayName = "BMFont Importer - MonoGame.Extended")] - public class BitmapFontImporter : ContentImporter + public class BitmapFontImporter : ContentImporter> { - public override BitmapFontFile Import(string filename, ContentImporterContext context) + public override ContentImporterResult Import(string filename, ContentImporterContext context) { - context.Logger.LogMessage("Importing XML file: {0}", filename); + context.Logger.LogMessage("Importing FNT file: {0}", filename); - using (var streamReader = new StreamReader(filename)) - { - var deserializer = new XmlSerializer(typeof(BitmapFontFile)); - return (BitmapFontFile)deserializer.Deserialize(streamReader); - } + using FileStream stream = File.OpenRead(filename); + BmfFile file = BmfFile.FromStream(stream); + return new ContentImporterResult(filename, file); } } -} \ No newline at end of file +} diff --git a/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontInfo.cs b/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontInfo.cs deleted file mode 100644 index 1f50cf88..00000000 --- a/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontInfo.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Xml.Serialization; - -namespace MonoGame.Extended.Content.Pipeline.BitmapFonts -{ - // ---- AngelCode BmFont XML serializer ---------------------- - // ---- By DeadlyDan @ deadlydan@gmail.com ------------------- - // ---- There's no license restrictions, use as you will. ---- - // ---- Credits to http://www.angelcode.com/ ----------------- - public class BitmapFontInfo - { - [XmlAttribute("face")] - public string Face { get; set; } - - [XmlAttribute("size")] - public int Size { get; set; } - - [XmlAttribute("bold")] - public int Bold { get; set; } - - [XmlAttribute("italic")] - public int Italic { get; set; } - - [XmlAttribute("charset")] - public string CharSet { get; set; } - - [XmlAttribute("unicode")] - public int Unicode { get; set; } - - [XmlAttribute("stretchH")] - public int StretchHeight { get; set; } - - [XmlAttribute("smooth")] - public int Smooth { get; set; } - - [XmlAttribute("aa")] - public int SuperSampling { get; set; } - - [XmlAttribute("padding")] - public string Padding { get; set; } - - [XmlAttribute("spacing")] - public string Spacing { get; set; } - - [XmlAttribute("outline")] - public int OutLine { get; set; } - } -} \ No newline at end of file diff --git a/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontKerning.cs b/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontKerning.cs deleted file mode 100644 index 77caf13c..00000000 --- a/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontKerning.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Xml.Serialization; - -namespace MonoGame.Extended.Content.Pipeline.BitmapFonts -{ - // ---- AngelCode BmFont XML serializer ---------------------- - // ---- By DeadlyDan @ deadlydan@gmail.com ------------------- - // ---- There's no license restrictions, use as you will. ---- - // ---- Credits to http://www.angelcode.com/ ----------------- - public class BitmapFontKerning - { - [XmlAttribute("first")] - public int First { get; set; } - - [XmlAttribute("second")] - public int Second { get; set; } - - [XmlAttribute("amount")] - public int Amount { get; set; } - } -} \ No newline at end of file diff --git a/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontPage.cs b/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontPage.cs deleted file mode 100644 index 3841ff5b..00000000 --- a/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontPage.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Xml.Serialization; - -namespace MonoGame.Extended.Content.Pipeline.BitmapFonts -{ - // ---- AngelCode BmFont XML serializer ---------------------- - // ---- By DeadlyDan @ deadlydan@gmail.com ------------------- - // ---- There's no license restrictions, use as you will. ---- - // ---- Credits to http://www.angelcode.com/ ----------------- - public class BitmapFontPage - { - [XmlAttribute("id")] - public int Id { get; set; } - - [XmlAttribute("file")] - public string File { get; set; } - } -} \ No newline at end of file diff --git a/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessor.cs b/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessor.cs index a859eb53..8dc7eea2 100644 --- a/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessor.cs +++ b/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessor.cs @@ -1,22 +1,24 @@ using System; using System.IO; using Microsoft.Xna.Framework.Content.Pipeline; +using MonoGame.Extended.BitmapFonts.BmfTypes; namespace MonoGame.Extended.Content.Pipeline.BitmapFonts { [ContentProcessor(DisplayName = "BMFont Processor - MonoGame.Extended")] - public class BitmapFontProcessor : ContentProcessor + public class BitmapFontProcessor : ContentProcessor, BitmapFontProcessorResult> { - public override BitmapFontProcessorResult Process(BitmapFontFile bitmapFontFile, ContentProcessorContext context) + public override BitmapFontProcessorResult Process(ContentImporterResult importerResult, ContentProcessorContext context) { try { + BmfFile bmfFile = importerResult.Data; context.Logger.LogMessage("Processing BMFont"); - var result = new BitmapFontProcessorResult(bitmapFontFile); + var result = new BitmapFontProcessorResult(bmfFile); - foreach (var fontPage in bitmapFontFile.Pages) + foreach (var fontPage in bmfFile.Pages) { - var assetName = Path.GetFileNameWithoutExtension(fontPage.File); + var assetName = Path.GetFileNameWithoutExtension(fontPage); context.Logger.LogMessage("Expected texture asset: {0}", assetName); result.TextureAssets.Add(assetName); } @@ -30,4 +32,4 @@ namespace MonoGame.Extended.Content.Pipeline.BitmapFonts } } } -} \ No newline at end of file +} diff --git a/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessorResult.cs b/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessorResult.cs index 5841acc5..75e6a6df 100644 --- a/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessorResult.cs +++ b/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessorResult.cs @@ -1,16 +1,17 @@ using System.Collections.Generic; +using MonoGame.Extended.BitmapFonts.BmfTypes; namespace MonoGame.Extended.Content.Pipeline.BitmapFonts { public class BitmapFontProcessorResult { public List TextureAssets { get; private set; } - public BitmapFontFile FontFile { get; private set; } + public BmfFile FontFile { get; private set; } - public BitmapFontProcessorResult(BitmapFontFile fontFile) + public BitmapFontProcessorResult(BmfFile fontFile) { FontFile = fontFile; TextureAssets = new List(); } } -} \ No newline at end of file +} diff --git a/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontWriter.cs b/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontWriter.cs index 343c40c8..57a7b72d 100644 --- a/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontWriter.cs +++ b/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontWriter.cs @@ -14,12 +14,14 @@ namespace MonoGame.Extended.Content.Pipeline.BitmapFonts writer.Write(textureAsset); var fontFile = result.FontFile; + writer.Write(fontFile.FontName); + writer.Write(fontFile.Info.FontSize); writer.Write(fontFile.Common.LineHeight); - writer.Write(fontFile.Chars.Count); + writer.Write(fontFile.Characters.Count); - foreach (var c in fontFile.Chars) + foreach (var c in fontFile.Characters) { - writer.Write(c.Id); + writer.Write(c.ID); writer.Write(c.Page); writer.Write(c.X); writer.Write(c.Y); @@ -49,4 +51,4 @@ namespace MonoGame.Extended.Content.Pipeline.BitmapFonts return "MonoGame.Extended.BitmapFonts.BitmapFontReader, MonoGame.Extended"; } } -} \ No newline at end of file +} diff --git a/source/MonoGame.Extended.Graphics/Batcher2D.cs b/source/MonoGame.Extended.Graphics/Batcher2D.cs index aeb03c61..0cc0c72e 100644 --- a/source/MonoGame.Extended.Graphics/Batcher2D.cs +++ b/source/MonoGame.Extended.Graphics/Batcher2D.cs @@ -221,7 +221,7 @@ namespace MonoGame.Extended.Graphics var lineSpacing = bitmapFont.LineHeight; var offset = new Vector2(0, 0); - BitmapFontRegion lastGlyph = null; + BitmapFontCharacter lastGlyph = null; for (var i = 0; i < text.Length;) { int character; @@ -253,7 +253,7 @@ namespace MonoGame.Extended.Graphics continue; } - var fontRegion = bitmapFont.GetCharacterRegion(character); + var fontRegion = bitmapFont.GetCharacter(character); if (fontRegion == null) continue; @@ -266,7 +266,7 @@ namespace MonoGame.Extended.Graphics DrawSprite(textureRegion.Texture, ref transform1Matrix, ref bounds, color, flags, depth); var advance = fontRegion.XAdvance + bitmapFont.LetterSpacing; - if (BitmapFont.UseKernings && lastGlyph != null) + if (bitmapFont.UseKernings && lastGlyph != null) { int amount; if (lastGlyph.Kernings.TryGetValue(character, out amount)) @@ -277,7 +277,7 @@ namespace MonoGame.Extended.Graphics offset.X += i != text.Length - 1 ? advance - : fontRegion.XOffset + fontRegion.Width; + : fontRegion.XOffset + fontRegion.TextureRegion.Width; lastGlyph = fontRegion; } @@ -355,7 +355,7 @@ namespace MonoGame.Extended.Graphics transform1Matrix.M31 += glyph.Position.X; transform1Matrix.M32 += glyph.Position.Y; - var texture = glyph.FontRegion.TextureRegion.Texture; + var texture = glyph.Character.TextureRegion.Texture; var bounds = texture.Bounds; DrawSprite(texture, ref transform1Matrix, ref bounds, color, flags, depth); } diff --git a/source/MonoGame.Extended.Gui/Controls/TextBox.cs b/source/MonoGame.Extended.Gui/Controls/TextBox.cs index c0766699..8235e7a0 100644 --- a/source/MonoGame.Extended.Gui/Controls/TextBox.cs +++ b/source/MonoGame.Extended.Gui/Controls/TextBox.cs @@ -126,7 +126,7 @@ namespace MonoGame.Extended.Gui.Controls foreach (var glyph in font.GetGlyphs(textInfo.Text, textInfo.Position)) { - var fontRegionWidth = glyph.FontRegion?.Width ?? 0; + var fontRegionWidth = glyph.Character?.TextureRegion?.Width ?? 0; var glyphMiddle = (int) (glyph.Position.X + fontRegionWidth * 0.5f); if (position.X >= glyphMiddle) diff --git a/source/MonoGame.Extended.Gui/Serialization/GuiJsonSerializerOptionsProvider.cs b/source/MonoGame.Extended.Gui/Serialization/GuiJsonSerializerOptionsProvider.cs index 5ebc880f..6a521a50 100644 --- a/source/MonoGame.Extended.Gui/Serialization/GuiJsonSerializerOptionsProvider.cs +++ b/source/MonoGame.Extended.Gui/Serialization/GuiJsonSerializerOptionsProvider.cs @@ -25,7 +25,7 @@ public static class GuiJsonSerializerOptionsProvider options.Converters.Add(new Size2JsonConverter()); options.Converters.Add(new ColorJsonConverter()); options.Converters.Add(new ThicknessJsonConverter()); - options.Converters.Add(new ContentManagerJsonConverter(contentManager, font => font.Name)); + options.Converters.Add(new ContentManagerJsonConverter(contentManager, font => font.Face)); options.Converters.Add(new ControlStyleJsonConverter(customControlTypes)); options.Converters.Add(new GuiTextureAtlasJsonConverter(contentManager, textureRegionService)); options.Converters.Add(new GuiNinePatchRegion2DJsonConverter(textureRegionService)); diff --git a/source/MonoGame.Extended/BitmapFonts/BitmapFont.Extensions.cs b/source/MonoGame.Extended/BitmapFonts/BitmapFont.Extensions.cs new file mode 100644 index 00000000..bde58b60 --- /dev/null +++ b/source/MonoGame.Extended/BitmapFonts/BitmapFont.Extensions.cs @@ -0,0 +1,134 @@ +//// Copyright (c) Craftwork Games. All rights reserved. +//// Licensed under the MIT license. +//// See LICENSE file in the project root for full license information. + +//using System; +//using System.Text; +//using Microsoft.Xna.Framework; + +//namespace MonoGame.Extended.BitmapFonts; + +///// +///// Provides extension methods for . +///// +//public static class BitmapFontExtensions +//{ +// /// +// /// Measures the size of the given text when rendered with the specified font. +// /// +// /// The used to render the text. +// /// The text to measure. +// /// The size of the rendered text. +// public static SizeF MeasureString(this BitmapFont font, string text) +// { +// SizeF size = new SizeF(0, font.LineHeight); +// SizeF offset = new SizeF(0, 0); +// bool firstCharacterOfLine = true; +// MeasureStringInternal(font, text, ref size, ref offset, ref firstCharacterOfLine); +// return size; +// } + +// /// +// /// Measures the size of the given text when rendered with the specified font. +// /// +// /// The used to render the text. +// /// The text to measure. +// /// The size of the rendered text. +// public static SizeF MeasureString(this BitmapFont font, StringBuilder text) +// { +// SizeF size = new SizeF(0, font.LineHeight); +// SizeF offset = new SizeF(0, 0); +// bool firstCharacterOfLine = true; + +// foreach (ReadOnlyMemory chunk in text.GetChunks()) +// { +// MeasureStringInternal(font, chunk.Span, ref size, ref offset, ref firstCharacterOfLine); +// } + +// return size; +// } + +// /// +// /// Gets the rectangular bounds of the specified text when rendered with the specified font. +// /// +// /// The used to render the text. +// /// The text to measure. +// /// The top-left corner of the bounding rectangle. +// /// The rectangular bounds of the rendered text. +// public static RectangleF GetStringBounds(this BitmapFont font, string text, Vector2 location) +// { +// SizeF size = font.MeasureString(text); +// return new RectangleF(location, size); +// } + +// /// +// /// Gets the rectangular bounds of the specified text when rendered with the specified font. +// /// +// /// The used to render the text. +// /// The text to measure. +// /// The top-left corner of the bounding rectangle. +// /// The rectangular bounds of the rendered text. +// public static RectangleF GetStringBounds(this BitmapFont font, StringBuilder text, Vector2 location) +// { +// SizeF size = font.MeasureString(text); +// return new RectangleF(location, size); +// } + +// private static void MeasureStringInternal(this BitmapFont font, ReadOnlySpan text, ref SizeF size, ref SizeF offset, ref bool firstCharacterOfLine) +// { +// if (text.Length == 0) +// { +// return; +// } + +// for (int i = 0; i < text.Length; i++) +// { +// char c = text[i]; + +// if (c == '\r') +// { +// continue; +// } + +// if (c == '\n') +// { +// offset.Width = 0; +// offset.Height += font.LineHeight; +// firstCharacterOfLine = true; +// continue; +// } + +// if (!font.Characters.TryGetValue(c, out BitmapFontCharacter? character)) +// { +// continue; +// } + +// if (firstCharacterOfLine) +// { +// offset.Width = Math.Max(character.XOffset, 0); +// firstCharacterOfLine = false; +// } +// else +// { +// offset.Width += character.XOffset; +// } + +// offset.Width += character.TextureRegion.Width; + +// float proposedWidth = offset.Width + character.XAdvance; +// if (proposedWidth > size.Width) +// { +// size.Width = proposedWidth; +// } + +// offset.Width += character.XAdvance; + +// if (character.TextureRegion.Height > size.Height) +// { +// size.Height = character.TextureRegion.Height; +// } +// } + +// size.Height += offset.Height; +// } +//} diff --git a/source/MonoGame.Extended/BitmapFonts/BitmapFont.cs b/source/MonoGame.Extended/BitmapFonts/BitmapFont.cs index 61b0d81b..d3e8f246 100644 --- a/source/MonoGame.Extended/BitmapFonts/BitmapFont.cs +++ b/source/MonoGame.Extended/BitmapFonts/BitmapFont.cs @@ -1,354 +1,410 @@ +// Copyright (c) Craftwork Games. All rights reserved. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. + using System; using System.Collections; using System.Collections.Generic; +using System.IO; using System.Text; using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Extended.BitmapFonts.BmfTypes; +using MonoGame.Extended.TextureAtlases; -namespace MonoGame.Extended.BitmapFonts +namespace MonoGame.Extended.BitmapFonts; + +public sealed class BitmapFont { - public class BitmapFont + private readonly Dictionary _characters; + public string Face { get; } + public int Size { get; } + public int LineHeight { get; } + + public int LetterSpacing { get; set; } + + public bool UseKernings { get; set; } = true; + + public BitmapFont(string face, int size, int lineHeight, IEnumerable characters) { - private readonly Dictionary _characterMap = new Dictionary(); + Face = face; + Size = size; + LineHeight = lineHeight; + _characters = new Dictionary(); - public BitmapFont(string name, IEnumerable regions, int lineHeight) + foreach (BitmapFontCharacter character in characters) { - foreach (var region in regions) - _characterMap.Add(region.Character, region); + _characters.Add(character.Character, character); + } + } - Name = name; - LineHeight = lineHeight; + public BitmapFontCharacter GetCharacter(int character) => _characters[character]; + public bool TryGetCharacter(int character, out BitmapFontCharacter value) => _characters.TryGetValue(character, out value); + + public SizeF MeasureString(string text) + { + if (string.IsNullOrEmpty(text)) + return SizeF.Empty; + + var stringRectangle = GetStringRectangle(text); + return new SizeF(stringRectangle.Width, stringRectangle.Height); + } + + public SizeF MeasureString(StringBuilder text) + { + if (text == null || text.Length == 0) + return SizeF.Empty; + + var stringRectangle = GetStringRectangle(text); + return new SizeF(stringRectangle.Width, stringRectangle.Height); + } + + public RectangleF GetStringRectangle(string text) + { + return GetStringRectangle(text, Vector2.Zero); + } + + public RectangleF GetStringRectangle(string text, Vector2 position) + { + var glyphs = GetGlyphs(text, position); + var rectangle = new RectangleF(position.X, position.Y, 0, LineHeight); + + foreach (var glyph in glyphs) + { + if (glyph.Character != null) + { + var right = glyph.Position.X + glyph.Character.TextureRegion.Width; + + if (right > rectangle.Right) + rectangle.Width = (int)(right - rectangle.Left); + } + + if (glyph.CharacterID == '\n') + rectangle.Height += LineHeight; } - public string Name { get; } - public int LineHeight { get; } - public int LetterSpacing { get; set; } - public static bool UseKernings { get; set; } = true; + return rectangle; + } - public BitmapFontRegion GetCharacterRegion(int character) + public RectangleF GetStringRectangle(StringBuilder text, Vector2? position = null) + { + var position1 = position ?? new Vector2(); + var glyphs = GetGlyphs(text, position1); + var rectangle = new RectangleF(position1.X, position1.Y, 0, LineHeight); + + foreach (var glyph in glyphs) { - return _characterMap.TryGetValue(character, out var region) ? region : null; + if (glyph.Character != null) + { + var right = glyph.Position.X + glyph.Character.TextureRegion.Width; + + if (right > rectangle.Right) + rectangle.Width = (int)(right - rectangle.Left); + } + + if (glyph.CharacterID == '\n') + rectangle.Height += LineHeight; } - public SizeF MeasureString(string text) - { - if (string.IsNullOrEmpty(text)) - return SizeF.Empty; - - var stringRectangle = GetStringRectangle(text); - return new SizeF(stringRectangle.Width, stringRectangle.Height); - } - - public SizeF MeasureString(StringBuilder text) - { - if (text == null || text.Length == 0) - return SizeF.Empty; - - var stringRectangle = GetStringRectangle(text); - return new SizeF(stringRectangle.Width, stringRectangle.Height); - } - - public RectangleF GetStringRectangle(string text) - { - return GetStringRectangle(text, Vector2.Zero); - } - - public RectangleF GetStringRectangle(string text, Vector2 position) - { - var glyphs = GetGlyphs(text, position); - var rectangle = new RectangleF(position.X, position.Y, 0, LineHeight); - - foreach (var glyph in glyphs) - { - if (glyph.FontRegion != null) - { - var right = glyph.Position.X + glyph.FontRegion.Width; - - if (right > rectangle.Right) - rectangle.Width = (int)(right - rectangle.Left); - } - - if (glyph.Character == '\n') - rectangle.Height += LineHeight; - } - - return rectangle; - } - - public RectangleF GetStringRectangle(StringBuilder text, Vector2? position = null) - { - var position1 = position ?? new Vector2(); - var glyphs = GetGlyphs(text, position1); - var rectangle = new RectangleF(position1.X, position1.Y, 0, LineHeight); - - foreach (var glyph in glyphs) - { - if (glyph.FontRegion != null) - { - var right = glyph.Position.X + glyph.FontRegion.Width; - - if (right > rectangle.Right) - rectangle.Width = (int)(right - rectangle.Left); - } - - if (glyph.Character == '\n') - rectangle.Height += LineHeight; - } - - return rectangle; - } - - public StringGlyphEnumerable GetGlyphs(string text, Vector2? position = null) - { - return new StringGlyphEnumerable(this, text, position); - } - - public StringBuilderGlyphEnumerable GetGlyphs(StringBuilder text, Vector2? position) - { - return new StringBuilderGlyphEnumerable(this, text, position); - } - - public override string ToString() - { - return $"{Name}"; - } - - public struct StringGlyphEnumerable : IEnumerable - { - private readonly StringGlyphEnumerator _enumerator; - - public StringGlyphEnumerable(BitmapFont font, string text, Vector2? position) - { - _enumerator = new StringGlyphEnumerator(font, text, position); - } - - public StringGlyphEnumerator GetEnumerator() - { - return _enumerator; - } - - IEnumerator IEnumerable.GetEnumerator() - { - return _enumerator; - } - - IEnumerator IEnumerable.GetEnumerator() - { - return _enumerator; - } - } - - public struct StringGlyphEnumerator : IEnumerator - { - private readonly BitmapFont _font; - private readonly string _text; - private int _index; - private readonly Vector2 _position; - private Vector2 _positionDelta; - private BitmapFontGlyph _currentGlyph; - private BitmapFontGlyph? _previousGlyph; - - object IEnumerator.Current - { - get - { - // casting a struct to object will box it, behaviour we want to avoid... - throw new InvalidOperationException(); - } - } - - public BitmapFontGlyph Current => _currentGlyph; - - public StringGlyphEnumerator(BitmapFont font, string text, Vector2? position) - { - _font = font; - _text = text; - _index = -1; - _position = position ?? new Vector2(); - _positionDelta = new Vector2(); - _currentGlyph = new BitmapFontGlyph(); - _previousGlyph = null; - } - - public bool MoveNext() - { - if (++_index >= _text.Length) - return false; - - var character = GetUnicodeCodePoint(_text, ref _index); - _currentGlyph.Character = character; - _currentGlyph.FontRegion = _font.GetCharacterRegion(character); - _currentGlyph.Position = _position + _positionDelta; - - if (_currentGlyph.FontRegion != null) - { - _currentGlyph.Position.X += _currentGlyph.FontRegion.XOffset; - _currentGlyph.Position.Y += _currentGlyph.FontRegion.YOffset; - _positionDelta.X += _currentGlyph.FontRegion.XAdvance + _font.LetterSpacing; - } - - if (UseKernings && _previousGlyph?.FontRegion != null) - { - if (_previousGlyph.Value.FontRegion.Kernings.TryGetValue(character, out var amount)) - { - _positionDelta.X += amount; - _currentGlyph.Position.X += amount; - } - } - - _previousGlyph = _currentGlyph; - - if (character != '\n') - return true; - - _positionDelta.Y += _font.LineHeight; - _positionDelta.X = 0; - _previousGlyph = null; - - return true; - } - - private static int GetUnicodeCodePoint(string text, ref int index) - { - return char.IsHighSurrogate(text[index]) && ++index < text.Length - ? char.ConvertToUtf32(text[index - 1], text[index]) - : text[index]; - } - - public void Dispose() - { - } - - public void Reset() - { - _positionDelta = new Vector2(); - _index = -1; - _previousGlyph = null; - } - } - - public struct StringBuilderGlyphEnumerable : IEnumerable - { - private readonly StringBuilderGlyphEnumerator _enumerator; - - public StringBuilderGlyphEnumerable(BitmapFont font, StringBuilder text, Vector2? position) - { - _enumerator = new StringBuilderGlyphEnumerator(font, text, position); - } - - public StringBuilderGlyphEnumerator GetEnumerator() - { - return _enumerator; - } - - IEnumerator IEnumerable.GetEnumerator() - { - return _enumerator; - } - - IEnumerator IEnumerable.GetEnumerator() - { - return _enumerator; - } - } - - public struct StringBuilderGlyphEnumerator : IEnumerator - { - private readonly BitmapFont _font; - private readonly StringBuilder _text; - private int _index; - private readonly Vector2 _position; - private Vector2 _positionDelta; - private BitmapFontGlyph _currentGlyph; - private BitmapFontGlyph? _previousGlyph; - - object IEnumerator.Current - { - get - { - // casting a struct to object will box it, behaviour we want to avoid... - throw new InvalidOperationException(); - } - } - - public BitmapFontGlyph Current => _currentGlyph; - - public StringBuilderGlyphEnumerator(BitmapFont font, StringBuilder text, Vector2? position) - { - _font = font; - _text = text; - _index = -1; - _position = position ?? new Vector2(); - _positionDelta = new Vector2(); - _currentGlyph = new BitmapFontGlyph(); - _previousGlyph = null; - } - - public bool MoveNext() - { - if (++_index >= _text.Length) - return false; - - var character = GetUnicodeCodePoint(_text, ref _index); - _currentGlyph = new BitmapFontGlyph - { - Character = character, - FontRegion = _font.GetCharacterRegion(character), - Position = _position + _positionDelta - }; - - if (_currentGlyph.FontRegion != null) - { - _currentGlyph.Position.X += _currentGlyph.FontRegion.XOffset; - _currentGlyph.Position.Y += _currentGlyph.FontRegion.YOffset; - _positionDelta.X += _currentGlyph.FontRegion.XAdvance + _font.LetterSpacing; - } - - if (UseKernings && _previousGlyph.HasValue && _previousGlyph.Value.FontRegion != null) - { - int amount; - if (_previousGlyph.Value.FontRegion.Kernings.TryGetValue(character, out amount)) - { - _positionDelta.X += amount; - _currentGlyph.Position.X += amount; - } - } - - _previousGlyph = _currentGlyph; - - if (character != '\n') - return true; - - _positionDelta.Y += _font.LineHeight; - _positionDelta.X = _position.X; - _previousGlyph = null; - - return true; - } - - private static int GetUnicodeCodePoint(StringBuilder text, ref int index) - { - return char.IsHighSurrogate(text[index]) && ++index < text.Length - ? char.ConvertToUtf32(text[index - 1], text[index]) - : text[index]; - } - - public void Dispose() - { - } - - public void Reset() - { - _positionDelta = new Vector2(); - _index = -1; - _previousGlyph = null; - } - } + return rectangle; } public struct BitmapFontGlyph { - public int Character; + public int CharacterID; public Vector2 Position; - public BitmapFontRegion FontRegion; + public BitmapFontCharacter Character; + } + public StringGlyphEnumerable GetGlyphs(string text, Vector2? position = null) + { + return new StringGlyphEnumerable(this, text, position); + } + + public StringBuilderGlyphEnumerable GetGlyphs(StringBuilder text, Vector2? position) + { + return new StringBuilderGlyphEnumerable(this, text, position); + } + + public struct StringGlyphEnumerable : IEnumerable + { + private readonly StringGlyphEnumerator _enumerator; + + public StringGlyphEnumerable(BitmapFont font, string text, Vector2? position) + { + _enumerator = new StringGlyphEnumerator(font, text, position); + } + + public StringGlyphEnumerator GetEnumerator() + { + return _enumerator; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _enumerator; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _enumerator; + } + } + + public struct StringGlyphEnumerator : IEnumerator + { + private readonly BitmapFont _font; + private readonly string _text; + private int _index; + private readonly Vector2 _position; + private Vector2 _positionDelta; + private BitmapFontGlyph _currentGlyph; + private BitmapFontGlyph? _previousGlyph; + + object IEnumerator.Current + { + get + { + // casting a struct to object will box it, behaviour we want to avoid... + throw new InvalidOperationException(); + } + } + + public BitmapFontGlyph Current => _currentGlyph; + + public StringGlyphEnumerator(BitmapFont font, string text, Vector2? position) + { + _font = font; + _text = text; + _index = -1; + _position = position ?? new Vector2(); + _positionDelta = new Vector2(); + _currentGlyph = new BitmapFontGlyph(); + _previousGlyph = null; + } + + public bool MoveNext() + { + if (++_index >= _text.Length) + return false; + + var character = GetUnicodeCodePoint(_text, ref _index); + _currentGlyph.CharacterID = character; + _font.TryGetCharacter(character, out _currentGlyph.Character); + _currentGlyph.Position = _position + _positionDelta; + + if (_currentGlyph.Character != null) + { + _currentGlyph.Position.X += _currentGlyph.Character.XOffset; + _currentGlyph.Position.Y += _currentGlyph.Character.YOffset; + _positionDelta.X += _currentGlyph.Character.XAdvance + _font.LetterSpacing; + } + + if (_font.UseKernings && _previousGlyph?.Character != null) + { + if (_previousGlyph.Value.Character.Kernings.TryGetValue(character, out var amount)) + { + _positionDelta.X += amount; + _currentGlyph.Position.X += amount; + } + } + + _previousGlyph = _currentGlyph; + + if (character != '\n') + return true; + + _positionDelta.Y += _font.LineHeight; + _positionDelta.X = 0; + _previousGlyph = null; + + return true; + } + + private static int GetUnicodeCodePoint(string text, ref int index) + { + return char.IsHighSurrogate(text[index]) && ++index < text.Length + ? char.ConvertToUtf32(text[index - 1], text[index]) + : text[index]; + } + + public void Dispose() + { + } + + public void Reset() + { + _positionDelta = new Vector2(); + _index = -1; + _previousGlyph = null; + } + } + + public struct StringBuilderGlyphEnumerable : IEnumerable + { + private readonly StringBuilderGlyphEnumerator _enumerator; + + public StringBuilderGlyphEnumerable(BitmapFont font, StringBuilder text, Vector2? position) + { + _enumerator = new StringBuilderGlyphEnumerator(font, text, position); + } + + public StringBuilderGlyphEnumerator GetEnumerator() + { + return _enumerator; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _enumerator; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _enumerator; + } + } + + public struct StringBuilderGlyphEnumerator : IEnumerator + { + private readonly BitmapFont _font; + private readonly StringBuilder _text; + private int _index; + private readonly Vector2 _position; + private Vector2 _positionDelta; + private BitmapFontGlyph _currentGlyph; + private BitmapFontGlyph? _previousGlyph; + + object IEnumerator.Current + { + get + { + // casting a struct to object will box it, behaviour we want to avoid... + throw new InvalidOperationException(); + } + } + + public BitmapFontGlyph Current => _currentGlyph; + + public StringBuilderGlyphEnumerator(BitmapFont font, StringBuilder text, Vector2? position) + { + _font = font; + _text = text; + _index = -1; + _position = position ?? new Vector2(); + _positionDelta = new Vector2(); + _currentGlyph = new BitmapFontGlyph(); + _previousGlyph = null; + } + + public bool MoveNext() + { + if (++_index >= _text.Length) + return false; + + var character = GetUnicodeCodePoint(_text, ref _index); + _currentGlyph = new BitmapFontGlyph + { + CharacterID = character, + Character = _font.GetCharacter(character), + Position = _position + _positionDelta + }; + + if (_currentGlyph.Character != null) + { + _currentGlyph.Position.X += _currentGlyph.Character.XOffset; + _currentGlyph.Position.Y += _currentGlyph.Character.YOffset; + _positionDelta.X += _currentGlyph.Character.XAdvance + _font.LetterSpacing; + } + + if (_font.UseKernings && _previousGlyph.HasValue && _previousGlyph.Value.Character != null) + { + int amount; + if (_previousGlyph.Value.Character.Kernings.TryGetValue(character, out amount)) + { + _positionDelta.X += amount; + _currentGlyph.Position.X += amount; + } + } + + _previousGlyph = _currentGlyph; + + if (character != '\n') + return true; + + _positionDelta.Y += _font.LineHeight; + _positionDelta.X = _position.X; + _previousGlyph = null; + + return true; + } + + private static int GetUnicodeCodePoint(StringBuilder text, ref int index) + { + return char.IsHighSurrogate(text[index]) && ++index < text.Length + ? char.ConvertToUtf32(text[index - 1], text[index]) + : text[index]; + } + + public void Dispose() + { + } + + public void Reset() + { + _positionDelta = new Vector2(); + _index = -1; + _previousGlyph = null; + } + } + + + + /// + public override string ToString() => $"{Face}"; + + public static BitmapFont FromFile(GraphicsDevice graphicsDevice, string path) + { + using FileStream stream = File.OpenRead(path); + return FromStream(graphicsDevice, stream); + } + + public static BitmapFont FromStream(GraphicsDevice graphicsDevice, FileStream stream) + { + BmfFile bmfFile = BmfFile.FromStream(stream); + + // Load page textures + Dictionary pages = new Dictionary(); + for (int i = 0; i < bmfFile.Pages.Count; i++) + { + if (!pages.ContainsKey(bmfFile.Pages[i])) + { + string texturePath = Path.Combine(bmfFile.Path, bmfFile.Pages[i]); + Texture2D texture = Texture2D.FromFile(graphicsDevice, texturePath); + pages.Add(bmfFile.Pages[i], texture); + } + } + + // Load Characters + Dictionary characters = new Dictionary(); + for (int i = 0; i < bmfFile.Characters.Count; i++) + { + BmfCharsBlock charBlock = bmfFile.Characters[i]; + Texture2D texture = pages[bmfFile.Pages[charBlock.Page]]; + TextureRegion2D region = new TextureRegion2D(texture, charBlock.X, charBlock.Y, charBlock.Width, charBlock.Height); + BitmapFontCharacter character = new BitmapFontCharacter((int)charBlock.ID, region, charBlock.XOffset, charBlock.YOffset, charBlock.XAdvance); + characters.Add(character.Character, character); + } + + // Load kernings + for (int i = 0; i < bmfFile.Kernings.Count; i++) + { + BmfKerningPairsBlock kerningBlock = bmfFile.Kernings[i]; + if (characters.TryGetValue((int)kerningBlock.First, out BitmapFontCharacter character)) + { + character.Kernings.Add((int)kerningBlock.Second, kerningBlock.Amount); + } + } + + return new BitmapFont(bmfFile.FontName, bmfFile.Info.FontSize, bmfFile.Common.LineHeight, characters.Values); } } diff --git a/source/MonoGame.Extended/BitmapFonts/BitmapFontCharacter.cs b/source/MonoGame.Extended/BitmapFonts/BitmapFontCharacter.cs new file mode 100644 index 00000000..64d3cc06 --- /dev/null +++ b/source/MonoGame.Extended/BitmapFonts/BitmapFontCharacter.cs @@ -0,0 +1,62 @@ +// Copyright (c) Craftwork Games. All rights reserved. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.BitmapFonts; + +/// +/// Represents a character in a bitmap font. This class cannot be inherited. +/// +public sealed class BitmapFontCharacter +{ + /// + /// Gets the character code. + /// + public int Character { get; } + + /// + /// Gets the texture region that contains the character's image. + /// + public TextureRegion2D TextureRegion { get; } + + /// + /// Gets the horizontal offset for rendering the character. + /// + public int XOffset { get; } + + /// + /// Gets the vertical offset for rendering the character. + /// + public int YOffset { get; } + + /// + /// Gets the horizontal advance value for rendering the next character. + /// + public int XAdvance { get; } + + /// + /// Gets the dictionary of kerning values for pairs of characters. + /// + public Dictionary Kernings { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The character code. + /// The texture region that contains the character's image. + /// The horizontal offset for rendering the character. + /// The vertical offset for rendering the character. + /// The horizontal advance value for rendering the next character. + public BitmapFontCharacter(int character, TextureRegion2D textureRegion, int xOffset, int yOffset, int xAdvance) + { + Character = character; + TextureRegion = textureRegion; + XOffset = xOffset; + YOffset = yOffset; + XAdvance = xAdvance; + Kernings = new Dictionary(); + } +} diff --git a/source/MonoGame.Extended/BitmapFonts/BitmapFontExtensions.cs b/source/MonoGame.Extended/BitmapFonts/BitmapFontExtensions.cs index da072c50..7a6852e7 100644 --- a/source/MonoGame.Extended/BitmapFonts/BitmapFontExtensions.cs +++ b/source/MonoGame.Extended/BitmapFonts/BitmapFontExtensions.cs @@ -40,10 +40,10 @@ namespace MonoGame.Extended.BitmapFonts var glyphs = bitmapFont.GetGlyphs(text, position); foreach (var glyph in glyphs) { - if (glyph.FontRegion == null) + if (glyph.Character == null) continue; var characterOrigin = position - glyph.Position + origin; - spriteBatch.Draw(glyph.FontRegion.TextureRegion, position, color, rotation, characterOrigin, scale, effect, layerDepth, clippingRectangle); + spriteBatch.Draw(glyph.Character.TextureRegion, position, color, rotation, characterOrigin, scale, effect, layerDepth, clippingRectangle); } } @@ -58,10 +58,10 @@ namespace MonoGame.Extended.BitmapFonts var glyphs = bitmapFont.GetGlyphs(text, position); foreach (var glyph in glyphs) { - if (glyph.FontRegion == null) + if (glyph.Character == null) continue; var characterOrigin = position - glyph.Position + origin; - spriteBatch.Draw(glyph.FontRegion.TextureRegion, position, color, rotation, characterOrigin, scale, effect, layerDepth, clippingRectangle); + spriteBatch.Draw(glyph.Character.TextureRegion, position, color, rotation, characterOrigin, scale, effect, layerDepth, clippingRectangle); } } @@ -124,7 +124,7 @@ namespace MonoGame.Extended.BitmapFonts /// Clips the boundaries of the text so that it's not drawn outside the clipping rectangle public static void DrawString(this SpriteBatch spriteBatch, BitmapFont font, string text, Vector2 position, Color color, float layerDepth, Rectangle? clippingRectangle = null) { - DrawString(spriteBatch, font, text, position, color, rotation: 0, origin: Vector2.Zero, scale: Vector2.One, effect: SpriteEffects.None, + DrawString(spriteBatch, font, text, position, color, rotation: 0, origin: Vector2.Zero, scale: Vector2.One, effect: SpriteEffects.None, layerDepth: layerDepth, clippingRectangle: clippingRectangle); } @@ -143,7 +143,7 @@ namespace MonoGame.Extended.BitmapFonts /// Clips the boundaries of the text so that it's not drawn outside the clipping rectangle public static void DrawString(this SpriteBatch spriteBatch, BitmapFont font, string text, Vector2 position, Color color, Rectangle? clippingRectangle = null) { - DrawString(spriteBatch, font, text, position, color, rotation: 0, origin: Vector2.Zero, scale: Vector2.One, effect: SpriteEffects.None, + DrawString(spriteBatch, font, text, position, color, rotation: 0, origin: Vector2.Zero, scale: Vector2.One, effect: SpriteEffects.None, layerDepth: 0, clippingRectangle: clippingRectangle); } @@ -153,4 +153,4 @@ namespace MonoGame.Extended.BitmapFonts layerDepth: 0, clippingRectangle: clippingRectangle); } } -} \ No newline at end of file +} diff --git a/source/MonoGame.Extended/BitmapFonts/BitmapFontReader.cs b/source/MonoGame.Extended/BitmapFonts/BitmapFontReader.cs index c6d69fcc..643fc410 100644 --- a/source/MonoGame.Extended/BitmapFonts/BitmapFontReader.cs +++ b/source/MonoGame.Extended/BitmapFonts/BitmapFontReader.cs @@ -1,3 +1,7 @@ +// Copyright (c) Craftwork Games. All rights reserved. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. + using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework.Content; @@ -24,23 +28,25 @@ namespace MonoGame.Extended.BitmapFonts .Select(textureName => reader.ContentManager.Load(reader.GetRelativeAssetName(textureName))) .ToArray(); - var lineHeight = reader.ReadInt32(); + var face = reader.ReadString(); + var fontSize = reader.ReadInt16(); + var lineHeight = reader.ReadUInt16(); var regionCount = reader.ReadInt32(); - var regions = new BitmapFontRegion[regionCount]; + var regions = new BitmapFontCharacter[regionCount]; for (var r = 0; r < regionCount; r++) { - var character = reader.ReadInt32(); - var textureIndex = reader.ReadInt32(); - var x = reader.ReadInt32(); - var y = reader.ReadInt32(); - var width = reader.ReadInt32(); - var height = reader.ReadInt32(); - var xOffset = reader.ReadInt32(); - var yOffset = reader.ReadInt32(); - var xAdvance = reader.ReadInt32(); + var character = reader.ReadUInt32(); + var textureIndex = reader.ReadByte(); + var x = reader.ReadUInt16(); + var y = reader.ReadUInt16(); + var width = reader.ReadUInt16(); + var height = reader.ReadUInt16(); + var xOffset = reader.ReadInt16(); + var yOffset = reader.ReadInt16(); + var xAdvance = reader.ReadInt16(); var textureRegion = new TextureRegion2D(textures[textureIndex], x, y, width, height); - regions[r] = new BitmapFontRegion(textureRegion, character, xOffset, yOffset, xAdvance); + regions[r] = new BitmapFontCharacter((int)character, textureRegion, xOffset, yOffset, xAdvance); } var characterMap = regions.ToDictionary(r => r.Character); @@ -48,18 +54,18 @@ namespace MonoGame.Extended.BitmapFonts for (var k = 0; k < kerningsCount; k++) { - var first = reader.ReadInt32(); - var second = reader.ReadInt32(); - var amount = reader.ReadInt32(); + var first = reader.ReadUInt32(); + var second = reader.ReadUInt32(); + var amount = reader.ReadInt16(); // Find region - if (!characterMap.TryGetValue(first, out var region)) + if (!characterMap.TryGetValue((int)first, out var region)) continue; - region.Kernings[second] = amount; + region.Kernings[(int)second] = amount; } - return new BitmapFont(reader.AssetName, regions, lineHeight); + return new BitmapFont(face, fontSize, lineHeight, regions); } } -} \ No newline at end of file +} diff --git a/source/MonoGame.Extended/BitmapFonts/BitmapFontRegion.cs b/source/MonoGame.Extended/BitmapFonts/BitmapFontRegion.cs deleted file mode 100644 index 5254f484..00000000 --- a/source/MonoGame.Extended/BitmapFonts/BitmapFontRegion.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using MonoGame.Extended.TextureAtlases; -using System.Collections.Generic; - -namespace MonoGame.Extended.BitmapFonts -{ - public class BitmapFontRegion - { - public BitmapFontRegion(TextureRegion2D textureRegion, int character, int xOffset, int yOffset, int xAdvance) - { - TextureRegion = textureRegion; - Character = character; - XOffset = xOffset; - YOffset = yOffset; - XAdvance = xAdvance; - Kernings = new Dictionary(); - } - - public int Character { get; } - public TextureRegion2D TextureRegion { get; } - public int XOffset { get; } - public int YOffset { get; } - public int XAdvance { get; } - public int Width => TextureRegion.Width; - public int Height => TextureRegion.Height; - public Dictionary Kernings { get; } - - public override string ToString() - { - return $"{Convert.ToChar(Character)} {TextureRegion}"; - } - } -} \ No newline at end of file diff --git a/source/MonoGame.Extended/BitmapFonts/BmfTypes/BmfCharsBlock.cs b/source/MonoGame.Extended/BitmapFonts/BmfTypes/BmfCharsBlock.cs new file mode 100644 index 00000000..50385cc7 --- /dev/null +++ b/source/MonoGame.Extended/BitmapFonts/BmfTypes/BmfCharsBlock.cs @@ -0,0 +1,24 @@ +// Copyright (c) Craftwork Games. All rights reserved. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; + +namespace MonoGame.Extended.BitmapFonts.BmfTypes; + +[StructLayout(LayoutKind.Explicit)] +public struct BmfCharsBlock +{ + public const int StructSize = 20; + + [FieldOffset(0)] public uint ID; + [FieldOffset(4)] public ushort X; + [FieldOffset(6)] public ushort Y; + [FieldOffset(8)] public ushort Width; + [FieldOffset(10)] public ushort Height; + [FieldOffset(12)] public short XOffset; + [FieldOffset(14)] public short YOffset; + [FieldOffset(16)] public short XAdvance; + [FieldOffset(18)] public byte Page; + [FieldOffset(19)] public byte Chnl; +} diff --git a/source/MonoGame.Extended/BitmapFonts/BmfTypes/BmfCommonBlock.cs b/source/MonoGame.Extended/BitmapFonts/BmfTypes/BmfCommonBlock.cs new file mode 100644 index 00000000..1648592a --- /dev/null +++ b/source/MonoGame.Extended/BitmapFonts/BmfTypes/BmfCommonBlock.cs @@ -0,0 +1,24 @@ +// Copyright (c) Craftwork Games. All rights reserved. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; + +namespace MonoGame.Extended.BitmapFonts.BmfTypes; + +[StructLayout(LayoutKind.Explicit)] +public struct BmfCommonBlock +{ + public const int StructSize = 15; + + [FieldOffset(0)] public ushort LineHeight; + [FieldOffset(2)] public ushort Base; + [FieldOffset(4)] public ushort ScaleW; + [FieldOffset(6)] public ushort ScaleH; + [FieldOffset(8)] public ushort Pages; + [FieldOffset(10)] public byte BitField; + [FieldOffset(11)] public byte AlphaChnl; + [FieldOffset(12)] public byte RedChnl; + [FieldOffset(13)] public byte GreenChnl; + [FieldOffset(14)] public byte BlueChnl; +} diff --git a/source/MonoGame.Extended/BitmapFonts/BmfTypes/BmfFile.cs b/source/MonoGame.Extended/BitmapFonts/BmfTypes/BmfFile.cs new file mode 100644 index 00000000..3645ad63 --- /dev/null +++ b/source/MonoGame.Extended/BitmapFonts/BmfTypes/BmfFile.cs @@ -0,0 +1,60 @@ +// Copyright (c) Craftwork Games. All rights reserved. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using MonoGame.Extended.BitmapFonts.Serialization; + +namespace MonoGame.Extended.BitmapFonts.BmfTypes; + +public sealed class BmfFile +{ + public string Path; + public BmfHeader Header; + public BmfCommonBlock Common; + public BmfInfoBlock Info; + public string FontName; + public readonly List Pages = new List(); + public readonly List Characters = new List(); + public readonly List Kernings = new List(); + + public static BmfFile FromStream(FileStream stream) + { + using BinaryReader reader = new BinaryReader(stream); + + long position = stream.Position; + ReadOnlySpan binaryHeader = stackalloc byte[] { 66, 77, 70, 3 }; + ReadOnlySpan xmlHeader = stackalloc byte[] { 60, 63, 120, 109 }; + ReadOnlySpan textHeader = stackalloc byte[] { 105, 110, 102, 111 }; + + Span buffer = stackalloc byte[4]; + stream.Read(buffer); + stream.Position = position; + + BmfFile bmfFile = new BmfFile(); + bmfFile.Path = stream.Name; + + + if (buffer.SequenceEqual(binaryHeader)) + { + BmfBinaryLoader.Load(bmfFile, stream); + return bmfFile; + } + + if (buffer.SequenceEqual(xmlHeader)) + { + BmfXmlLoader.Load(bmfFile, stream); + return bmfFile; + } + + if (buffer.SequenceEqual(textHeader)) + { + BmfTextLoader.Load(bmfFile, stream); + return bmfFile; + } + + throw new InvalidOperationException("This does not appear to be a valid BMFont file!"); + } +} diff --git a/source/MonoGame.Extended/BitmapFonts/BmfTypes/BmfHeader.cs b/source/MonoGame.Extended/BitmapFonts/BmfTypes/BmfHeader.cs new file mode 100644 index 00000000..0f345cf0 --- /dev/null +++ b/source/MonoGame.Extended/BitmapFonts/BmfTypes/BmfHeader.cs @@ -0,0 +1,20 @@ +// Copyright (c) Craftwork Games. All rights reserved. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; + +namespace MonoGame.Extended.BitmapFonts.BmfTypes; + +[StructLayout(LayoutKind.Explicit)] +public struct BmfHeader +{ + public const int StructSize = 4; + + [FieldOffset(0)] public byte B; + [FieldOffset(1)] public byte M; + [FieldOffset(2)] public byte F; + [FieldOffset(3)] public byte Version; + + public readonly bool IsValid => B == 0x42 && M == 0x4D && F == 0x46 && Version == 3; +} diff --git a/source/MonoGame.Extended/BitmapFonts/BmfTypes/BmfInfoBlock.cs b/source/MonoGame.Extended/BitmapFonts/BmfTypes/BmfInfoBlock.cs new file mode 100644 index 00000000..07dca173 --- /dev/null +++ b/source/MonoGame.Extended/BitmapFonts/BmfTypes/BmfInfoBlock.cs @@ -0,0 +1,22 @@ +using System.Runtime.InteropServices; + +namespace MonoGame.Extended.BitmapFonts.BmfTypes; + +[StructLayout(LayoutKind.Explicit)] +public struct BmfInfoBlock +{ + public const int StructSize = 14; + + [FieldOffset(0)] public short FontSize; + [FieldOffset(2)] public byte BitField; + [FieldOffset(3)] public byte CharSet; + [FieldOffset(4)] public ushort StretchH; + [FieldOffset(6)] public byte AA; + [FieldOffset(7)] public byte PaddingUp; + [FieldOffset(8)] public byte PaddingRight; + [FieldOffset(9)] public byte PaddingDown; + [FieldOffset(10)] public byte PaddingLeft; + [FieldOffset(11)] public byte SpacingHoriz; + [FieldOffset(12)] public byte SpacingVert; + [FieldOffset(13)] public byte Outline; +} diff --git a/source/MonoGame.Extended/BitmapFonts/BmfTypes/BmfKerningPairsBlock.cs b/source/MonoGame.Extended/BitmapFonts/BmfTypes/BmfKerningPairsBlock.cs new file mode 100644 index 00000000..b1f8946f --- /dev/null +++ b/source/MonoGame.Extended/BitmapFonts/BmfTypes/BmfKerningPairsBlock.cs @@ -0,0 +1,17 @@ +// Copyright (c) Craftwork Games. All rights reserved. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; + +namespace MonoGame.Extended.BitmapFonts.BmfTypes; + +[StructLayout(LayoutKind.Explicit)] +public struct BmfKerningPairsBlock +{ + public const int StructSize = 10; + + [FieldOffset(0)] public uint First; + [FieldOffset(4)] public uint Second; + [FieldOffset(8)] public short Amount; +} diff --git a/source/MonoGame.Extended/BitmapFonts/Serialization/BMFBinaryLoader.cs b/source/MonoGame.Extended/BitmapFonts/Serialization/BMFBinaryLoader.cs new file mode 100644 index 00000000..cfadca1e --- /dev/null +++ b/source/MonoGame.Extended/BitmapFonts/Serialization/BMFBinaryLoader.cs @@ -0,0 +1,99 @@ +// Copyright (c) Craftwork Games. All rights reserved. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using MonoGame.Extended.BitmapFonts.BmfTypes; + +namespace MonoGame.Extended.BitmapFonts.Serialization; + +public class BmfBinaryLoader +{ + public static void Load(BmfFile bmfFile, Stream stream) + { + using BinaryReader reader = new BinaryReader(stream); + bmfFile.Header = LoadHeader(reader); + BmfCharsBlock[] characters = Array.Empty(); + BmfKerningPairsBlock[] kernings = Array.Empty(); + + while (reader.BaseStream.Position < reader.BaseStream.Length) + { + byte blockType = reader.ReadByte(); + int blockSize = reader.ReadInt32(); + + switch (blockType) + { + case 1: + bmfFile.Info = AsType(reader.ReadBytes(BmfInfoBlock.StructSize)); + int stringLen = blockSize - BmfInfoBlock.StructSize; + bmfFile.FontName = Encoding.UTF8.GetString(reader.ReadBytes(stringLen)).Replace("\0", string.Empty); + break; + + case 2: + bmfFile.Common = AsType(reader.ReadBytes(BmfCommonBlock.StructSize)); + break; + + case 3: + string[] pages = Encoding.UTF8.GetString(reader.ReadBytes(blockSize)).Split('\0', StringSplitOptions.RemoveEmptyEntries); + bmfFile.Pages.AddRange(pages); + break; + + case 4: + int count = blockSize / BmfCharsBlock.StructSize; + for (int c = 0; c < count; c++) + { + BmfCharsBlock character = AsType(reader.ReadBytes(BmfCharsBlock.StructSize)); + bmfFile.Characters.Add(character); + } + break; + + case 5: + int kerningCount = blockSize / BmfKerningPairsBlock.StructSize; + kernings = new BmfKerningPairsBlock[kerningCount]; + for (int k = 0; k < kerningCount; k++) + { + BmfKerningPairsBlock kerning = AsType(reader.ReadBytes(BmfKerningPairsBlock.StructSize)); + bmfFile.Kernings.Add(kerning); + } + break; + + default: + reader.BaseStream.Seek(blockSize, SeekOrigin.Current); + break; + } + } + } + + private static BmfHeader LoadHeader(BinaryReader reader) + { + BmfHeader header = AsType(reader.ReadBytes(BmfHeader.StructSize)); + if (!header.IsValid) + { + throw new InvalidOperationException($"The BMFont file header is invalid, this does not appear to be a valid BMFont file."); + } + return header; + } + + private static T AsType(ReadOnlySpan buffer) where T : struct + { + T value; + try + { + unsafe + { + fixed (byte* ptr = buffer) + { + value = Marshal.PtrToStructure((IntPtr)ptr); + } + } + return value; + } + catch + { + return default; + } + } +} diff --git a/source/MonoGame.Extended/BitmapFonts/Serialization/BMFTextLoader.cs b/source/MonoGame.Extended/BitmapFonts/Serialization/BMFTextLoader.cs new file mode 100644 index 00000000..88205bb5 --- /dev/null +++ b/source/MonoGame.Extended/BitmapFonts/Serialization/BMFTextLoader.cs @@ -0,0 +1,310 @@ +// Copyright (c) Craftwork Games. All rights reserved. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using MonoGame.Extended.BitmapFonts.BmfTypes; + +namespace MonoGame.Extended.BitmapFonts.Serialization; + +public static class BmfTextLoader +{ + public static void Load(BmfFile bmfFile, Stream stream) + { + using StreamReader reader = new StreamReader(stream); + + // Text does not contain the header like binary so we manually create it + bmfFile.Header = new BmfHeader() { B = (byte)'B', M = (byte)'M', F = (byte)'F', Version = 3 }; + + string line = default; + while ((line = reader.ReadLine()) != null) + { + List tokens = GetTokens(line); + if (tokens.Count == 0) + { + continue; + } + + switch (tokens[0]) + { + case "info": + LoadInfoLine(bmfFile, CollectionsMarshal.AsSpan(tokens)[1..]); + break; + case "common": + LoadCommonLine(bmfFile, CollectionsMarshal.AsSpan(tokens)[1..]); + break; + case "page": + LoadPageLine(bmfFile, CollectionsMarshal.AsSpan(tokens)[1..]); + break; + case "char": + LoadCharacterLine(bmfFile, CollectionsMarshal.AsSpan(tokens)[1..]); + break; + case "kerning": + LoadKerningLine(bmfFile, CollectionsMarshal.AsSpan(tokens)[1..]); + break; + } + } + } + + + private static void LoadInfoLine(BmfFile bmfFile, ReadOnlySpan tokens) + { + for (int i = 0; i < tokens.Length; ++i) + { + string[] split = tokens[i].Split('='); + + if (split.Length != 2) + { + continue; + } + + switch (split[0]) + { + case "face": + bmfFile.FontName = split[1].Replace("\"", string.Empty); + break; + case "size": + bmfFile.Info.FontSize = Convert.ToInt16(split[1], CultureInfo.InvariantCulture); + break; + case "smooth": + byte smooth = Convert.ToByte(split[1], CultureInfo.InvariantCulture); + bmfFile.Info.BitField |= (byte)(smooth << 7); + break; + case "unicode": + byte unicode = Convert.ToByte(split[1], CultureInfo.InvariantCulture); + bmfFile.Info.BitField |= (byte)(unicode << 6); + break; + case "italic": + byte italic = Convert.ToByte(split[1], CultureInfo.InvariantCulture); + bmfFile.Info.BitField |= (byte)(italic << 5); + break; + case "bold": + byte bold = Convert.ToByte(split[1], CultureInfo.InvariantCulture); + bmfFile.Info.BitField |= (byte)(bold << 4); + break; + case "fixedHeight": + byte fixedHeight = Convert.ToByte(split[1], CultureInfo.InvariantCulture); + bmfFile.Info.BitField |= (byte)(fixedHeight << 3); + break; + case "stretchH": + bmfFile.Info.StretchH = Convert.ToUInt16(split[1], CultureInfo.InvariantCulture); + break; + case "aa": + bmfFile.Info.AA = Convert.ToByte(split[1], CultureInfo.InvariantCulture); + break; + case "padding": + string[] paddingValues = split[1].Split(','); + if (paddingValues.Length == 4) + { + bmfFile.Info.PaddingUp = Convert.ToByte(paddingValues[0], CultureInfo.InvariantCulture); + bmfFile.Info.PaddingRight = Convert.ToByte(paddingValues[1], CultureInfo.InvariantCulture); + bmfFile.Info.PaddingDown = Convert.ToByte(paddingValues[2], CultureInfo.InvariantCulture); + bmfFile.Info.PaddingLeft = Convert.ToByte(paddingValues[3], CultureInfo.InvariantCulture); + } + break; + case "spacing": + string[] spacingValues = split[1].Split(','); + if (spacingValues.Length == 2) + { + bmfFile.Info.SpacingHoriz = Convert.ToByte(spacingValues[0], CultureInfo.InvariantCulture); + bmfFile.Info.SpacingVert = Convert.ToByte(spacingValues[1], CultureInfo.InvariantCulture); + } + break; + case "outline": + bmfFile.Info.Outline = Convert.ToByte(split[1], CultureInfo.InvariantCulture); + break; + } + } + } + + private static void LoadCommonLine(BmfFile bmfFile, ReadOnlySpan tokens) + { + for (int i = 0; i < tokens.Length; ++i) + { + string[] split = tokens[i].Split('='); + + if (split.Length != 2) + { + continue; + } + + switch (split[0]) + { + case "lineHeight": + bmfFile.Common.LineHeight = Convert.ToUInt16(split[1], CultureInfo.InvariantCulture); + break; + case "base": + bmfFile.Common.Base = Convert.ToUInt16(split[1], CultureInfo.InvariantCulture); + break; + case "scaleW": + bmfFile.Common.ScaleW = Convert.ToUInt16(split[1], CultureInfo.InvariantCulture); + break; + case "scaleH": + bmfFile.Common.ScaleH = Convert.ToUInt16(split[1], CultureInfo.InvariantCulture); + break; + case "pages": + bmfFile.Common.Pages = Convert.ToUInt16(split[1], CultureInfo.InvariantCulture); + break; + case "packed": + byte packed = Convert.ToByte(split[1], CultureInfo.InvariantCulture); + bmfFile.Common.BitField |= (byte)(packed << 7); + break; + case "alphaChnl": + bmfFile.Common.AlphaChnl = Convert.ToByte(split[1], CultureInfo.InvariantCulture); + break; + case "redChnl": + bmfFile.Common.RedChnl = Convert.ToByte(split[1], CultureInfo.InvariantCulture); + break; + case "greenChnl": + bmfFile.Common.GreenChnl = Convert.ToByte(split[1], CultureInfo.InvariantCulture); + break; + case "blueChnl": + bmfFile.Common.BlueChnl = Convert.ToByte(split[1], CultureInfo.InvariantCulture); + break; + } + } + } + + private static void LoadPageLine(BmfFile bmfFile, ReadOnlySpan tokens) + { + for (int i = 0; i < tokens.Length; ++i) + { + string[] split = tokens[i].Split('='); + + if (split.Length != 2) + { + continue; + } + + if (split[0] == "file") + { + string page = split[1].Replace("\"", string.Empty); + bmfFile.Pages.Add(page); + } + } + } + + private static void LoadCharacterLine(BmfFile bmfFile, ReadOnlySpan tokens) + { + BmfCharsBlock character = default; + + for (int i = 0; i < tokens.Length; ++i) + { + string[] split = tokens[i].Split('='); + + if (split.Length != 2) + { + continue; + } + + switch (split[0]) + { + case "id": + character.ID = Convert.ToUInt32(split[1], CultureInfo.InvariantCulture); + break; + case "x": + character.X = Convert.ToUInt16(split[1], CultureInfo.InvariantCulture); + break; + case "y": + character.Y = Convert.ToUInt16(split[1], CultureInfo.InvariantCulture); + break; + case "width": + character.Width = Convert.ToUInt16(split[1], CultureInfo.InvariantCulture); + break; + case "height": + character.Height = Convert.ToUInt16(split[1], CultureInfo.InvariantCulture); + break; + case "xoffset": + character.XOffset = Convert.ToInt16(split[1], CultureInfo.InvariantCulture); + break; + case "yoffset": + character.YOffset = Convert.ToInt16(split[1], CultureInfo.InvariantCulture); + break; + case "xadvance": + character.XAdvance = Convert.ToInt16(split[1], CultureInfo.InvariantCulture); + break; + case "page": + character.Page = Convert.ToByte(split[1], CultureInfo.InvariantCulture); + break; + case "chnl": + character.Chnl = Convert.ToByte(split[1], CultureInfo.InvariantCulture); + break; + } + } + + bmfFile.Characters.Add(character); + } + + private static void LoadKerningLine(BmfFile bmfFile, ReadOnlySpan tokens) + { + BmfKerningPairsBlock kerning = default; + + for (int i = 0; i < tokens.Length; ++i) + { + string[] split = tokens[i].Split('='); + + if (split.Length != 2) + { + continue; + } + + switch (split[0]) + { + case "first": + kerning.First = Convert.ToUInt32(split[1], CultureInfo.InvariantCulture); + break; + case "second": + kerning.Second = Convert.ToUInt32(split[1], CultureInfo.InvariantCulture); + break; + case "amount": + kerning.Amount = Convert.ToInt16(split[1], CultureInfo.InvariantCulture); + break; + } + } + + bmfFile.Kernings.Add(kerning); + } + + + + private static List GetTokens(string line) + { + List tokens = new List(); + StringBuilder currentToken = new StringBuilder(); + bool inQuotes = false; + + for (int i = 0; i < line.Length; i++) + { + char c = line[i]; + + if (c == ' ' && !inQuotes) + { + if (currentToken.Length > 0) + { + tokens.Add(currentToken.ToString()); + currentToken.Clear(); + } + } + else if (c == '"') + { + inQuotes = !inQuotes; + } + else + { + currentToken.Append(c); + } + } + + if (currentToken.Length > 0) + { + tokens.Add(currentToken.ToString()); + } + + return tokens; + } +} diff --git a/source/MonoGame.Extended/BitmapFonts/Serialization/BMFXmlLoader.cs b/source/MonoGame.Extended/BitmapFonts/Serialization/BMFXmlLoader.cs new file mode 100644 index 00000000..44c46730 --- /dev/null +++ b/source/MonoGame.Extended/BitmapFonts/Serialization/BMFXmlLoader.cs @@ -0,0 +1,134 @@ +// Copyright (c) Craftwork Games. All rights reserved. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. + +using System.IO; +using System.Xml; +using MonoGame.Extended.BitmapFonts.BmfTypes; + +namespace MonoGame.Extended.BitmapFonts.Serialization; + +public static class BmfXmlLoader +{ + public static void Load(BmfFile bmfFile, Stream stream) + { + // XML does not contain the header like binary so we manually create it + bmfFile.Header = new BmfHeader() { B = (byte)'B', M = (byte)'M', F = (byte)'F', Version = 3 }; + + XmlDocument document = new XmlDocument(); + document.Load(stream); + XmlNode root = document.DocumentElement; + + LoadInfoNode(bmfFile, root); + LoadCommonNode(bmfFile, root); + LoadPageNodes(bmfFile, root); + LoadCharacterNodes(bmfFile, root); + LoadKerningNodes(bmfFile, root); + } + + private static void LoadInfoNode(BmfFile bmfFile, XmlNode root) + { + BmfInfoBlock info; + + XmlNode node = root.SelectSingleNode("info"); + bmfFile.FontName = node.GetStringAttribute("face"); + bmfFile.Info.FontSize = node.GetInt16Attribute("size"); + + byte smooth = node.GetByteAttribute("smooth"); + byte unicode = node.GetByteAttribute("unicode"); + byte italic = node.GetByteAttribute("italic"); + byte bold = node.GetByteAttribute("bold"); + byte fixedHeight = node.GetByteAttribute("fixedHeight"); + bmfFile.Info.BitField = (byte)(smooth << 7 | unicode << 6 | italic << 5 | bold << 4 | fixedHeight << 3); + + bmfFile.Info.CharSet = node.GetByteAttribute("charSet"); + bmfFile.Info.StretchH = node.GetUInt16Attribute("stretchH"); + bmfFile.Info.AA = node.GetByteAttribute("aa"); + + byte[] paddingValues = node.GetByteDelimitedAttribute("padding", 4); + bmfFile.Info.PaddingUp = paddingValues[0]; + bmfFile.Info.PaddingRight = paddingValues[1]; + bmfFile.Info.PaddingDown = paddingValues[2]; + bmfFile.Info.PaddingLeft = paddingValues[3]; + + byte[] spacingValues = node.GetByteDelimitedAttribute("spacing", 2); + bmfFile.Info.SpacingHoriz = spacingValues[0]; + bmfFile.Info.SpacingVert = spacingValues[1]; + + bmfFile.Info.Outline = node.GetByteAttribute("outline"); + } + + private static void LoadCommonNode(BmfFile bmfFile, XmlNode root) + { + BmfCommonBlock common; + + XmlNode node = root.SelectSingleNode("common"); + bmfFile.Common.LineHeight = node.GetUInt16Attribute("lineHeight"); + bmfFile.Common.Base = node.GetUInt16Attribute("base"); + bmfFile.Common.ScaleW = node.GetUInt16Attribute("scaleW"); + bmfFile.Common.ScaleH = node.GetUInt16Attribute("scaleH"); + bmfFile.Common.Pages = node.GetUInt16Attribute("pages"); + + byte packed = node.GetByteAttribute("packed"); + bmfFile.Common.BitField = (byte)(packed << 7); + + bmfFile.Common.AlphaChnl = node.GetByteAttribute("alphaChnl"); + bmfFile.Common.RedChnl = node.GetByteAttribute("redChnl"); + bmfFile.Common.GreenChnl = node.GetByteAttribute("greenChnl"); + bmfFile.Common.BlueChnl = node.GetByteAttribute("blueChnl"); + } + + private static void LoadPageNodes(BmfFile bmfFile, XmlNode root) + { + XmlNodeList nodes = root.SelectNodes("pages/page"); + + for (int i = 0; i < nodes.Count; ++i) + { + XmlNode node = nodes[i]; + + string file = node.GetStringAttribute("file"); + bmfFile.Pages.Add(file); + } + } + + private static void LoadCharacterNodes(BmfFile bmfFile, XmlNode root) + { + XmlNodeList characterNodes = root.SelectNodes("chars/char"); + + for (int i = 0; i < characterNodes.Count; ++i) + { + XmlNode node = characterNodes[i]; + + BmfCharsBlock character; + character.ID = node.GetUInt32Attribute("id"); + character.X = node.GetUInt16Attribute("x"); + character.Y = node.GetUInt16Attribute("y"); + character.Width = node.GetUInt16Attribute("width"); + character.Height = node.GetUInt16Attribute("height"); + character.XOffset = node.GetInt16Attribute("xoffset"); + character.YOffset = node.GetInt16Attribute("yoffset"); + character.XAdvance = node.GetInt16Attribute("xadvance"); + character.Page = node.GetByteAttribute("page"); + character.Chnl = node.GetByteAttribute("chnl"); + + bmfFile.Characters.Add(character); + } + } + + private static void LoadKerningNodes(BmfFile bmfFile, XmlNode root) + { + XmlNodeList kerningNodes = root.SelectNodes("kernings/kerning"); + + for (int i = 0; i < kerningNodes.Count; ++i) + { + XmlNode node = kerningNodes[i]; + + BmfKerningPairsBlock kerning; + kerning.First = node.GetUInt32Attribute("first"); + kerning.Second = node.GetUInt32Attribute("second"); + kerning.Amount = node.GetInt16Attribute("amount"); + + bmfFile.Kernings.Add(kerning); + } + } +} diff --git a/source/MonoGame.Extended/MonoGame.Extended.csproj b/source/MonoGame.Extended/MonoGame.Extended.csproj index f910e65a..f51651c7 100644 --- a/source/MonoGame.Extended/MonoGame.Extended.csproj +++ b/source/MonoGame.Extended/MonoGame.Extended.csproj @@ -1,11 +1,19 @@ - + - - It makes MonoGame more awesome. - monogame extended pipeline bmfont tiled texture atlas input viewport fps shapes sprite - + + It makes MonoGame more awesome. + monogame extended pipeline bmfont tiled texture atlas input viewport fps shapes sprite + + + + + + <_Parameter1>MonoGame.Extended.Tests + + + <_Parameter1>MonoGame.Extended.Content.Pipeline + + - - diff --git a/source/MonoGame.Extended/Xml/XmlNode.Extensions.cs b/source/MonoGame.Extended/Xml/XmlNode.Extensions.cs new file mode 100644 index 00000000..2584ae12 --- /dev/null +++ b/source/MonoGame.Extended/Xml/XmlNode.Extensions.cs @@ -0,0 +1,208 @@ +using System; +using System.Globalization; +using System.Xml; + +/// +/// Provides extension methods for the class. +/// +public static class XmlNodeExtensions +{ + /// + /// Retrieves the value of the specified attribute from the . + /// + /// The XML node. + /// The name of the attribute. + /// + /// When this method returns, contains the value of the attribute, if found; otherwise, an empty string. + /// + /// true if the attribute is found and has a non-empty value; otherwise, false. + private static bool GetAttributeValue(this XmlNode node, string attribute, out string value) + { + value = string.Empty; + XmlAttribute attr = node.Attributes[attribute]; + if (attr is not null) + { + value = attr.Value; + } + return !string.IsNullOrEmpty(value); + } + + /// + /// Retrieves the string value of the specified attribute. + /// + /// The XML node. + /// The name of the attribute. + /// + /// The string value of the attribute, or the default value if the attribute is not found or is empty. + /// + public static string GetStringAttribute(this XmlNode node, string attribute) + { + if (node.GetAttributeValue(attribute, out string value)) + { + return value; + } + return default; + } + + /// + /// Retrieves the byte value of the specified attribute. + /// + /// The XML node. + /// The name of the attribute. + /// + /// The byte value of the attribute, or the default value if the attribute is not found or is empty. + /// + public static byte GetByteAttribute(this XmlNode node, string attribute) + { + if (node.GetAttributeValue(attribute, out string value)) + { + return Convert.ToByte(value, CultureInfo.InvariantCulture); + } + return default; + } + + /// + /// Retrieves the ushort value of the specified attribute. + /// + /// The XML node. + /// The name of the attribute. + /// + /// The ushort value of the attribute, or the default value if the attribute is not found or is empty. + /// + public static ushort GetUInt16Attribute(this XmlNode node, string attribute) + { + if (node.GetAttributeValue(attribute, out string value)) + { + return Convert.ToUInt16(value, CultureInfo.InvariantCulture); + } + return default; + } + + /// + /// Retrieves the short value of the specified attribute. + /// + /// The XML node. + /// The name of the attribute. + /// + /// The short value of the attribute, or the default value if the attribute is not found or is empty. + /// + public static short GetInt16Attribute(this XmlNode node, string attribute) + { + if (node.GetAttributeValue(attribute, out string value)) + { + return Convert.ToInt16(value, CultureInfo.InvariantCulture); + } + return default; + } + + /// + /// Retrieves the uint value of the specified attribute. + /// + /// The XML node. + /// The name of the attribute. + /// + /// The uint value of the attribute, or the default value if the attribute is not found or is empty. + /// + public static uint GetUInt32Attribute(this XmlNode node, string attribute) + { + if (node.GetAttributeValue(attribute, out string value)) + { + return Convert.ToUInt32(value, CultureInfo.InvariantCulture); + } + return default; + } + + /// + /// Retrieves the int value of the specified attribute. + /// + /// The XML node. + /// The name of the attribute. + /// + /// The int value of the attribute, or the default value if the attribute is not found or is empty. + /// + public static int GetInt32Attribute(this XmlNode node, string attribute) + { + if (node.GetAttributeValue(attribute, out string value)) + { + return Convert.ToInt32(value, CultureInfo.InvariantCulture); + } + return default; + } + + /// + /// Retrieves the float value of the specified attribute. + /// + /// The XML node. + /// The name of the attribute. + /// + /// The float value of the attribute, or the default value if the attribute is not found or is empty. + /// + public static float GetSingleAttribute(this XmlNode node, string attribute) + { + if (node.GetAttributeValue(attribute, out string value)) + { + return Convert.ToSingle(value, CultureInfo.InvariantCulture); + } + return default; + } + + /// + /// Retrieves the double value of the specified attribute. + /// + /// The XML node. + /// The name of the attribute. + /// + /// The double value of the attribute, or the default value if the attribute is not found or is empty. + /// + public static double GetDoubleAttribute(this XmlNode node, string attribute) + { + if (node.GetAttributeValue(attribute, out string value)) + { + return Convert.ToDouble(value, CultureInfo.InvariantCulture); + } + return default; + } + + /// + /// Retrieves the boolean value of the specified attribute. + /// + /// The XML node. + /// The name of the attribute. + /// + /// The boolean value of the attribute, or the default value if the attribute is not found or is empty. + /// + public static bool GetBoolAttribute(this XmlNode node, string attribute) + { + if (node.GetAttributeValue(attribute, out string value)) + { + return Convert.ToBoolean(value, CultureInfo.InvariantCulture); + } + return default; + } + + /// + /// Retrieves the byte values from the specified delimited attribute and returns them in an array. + /// + /// The XML node. + /// The name of the attribute. + /// The expected number of byte values. + /// + /// An array of byte values parsed from the attribute, or an array of default values if the attribute is not found + /// or is empty. + /// + public static byte[] GetByteDelimitedAttribute(this XmlNode node, string attribute, int expectedCount) + { + byte[] result = new byte[expectedCount]; + + if (node.GetAttributeValue(attribute, out string value)) + { + string[] split = value.Split(','); + for (int i = 0; i < expectedCount; ++i) + { + result[i] = Convert.ToByte(split[i], CultureInfo.InvariantCulture); + } + } + + return result; + } +} diff --git a/tests/MonoGame.Extended.Tests/BitmapFonts/BitmapFontFileReaderTests.cs b/tests/MonoGame.Extended.Tests/BitmapFonts/BitmapFontFileReaderTests.cs new file mode 100644 index 00000000..4873fed3 --- /dev/null +++ b/tests/MonoGame.Extended.Tests/BitmapFonts/BitmapFontFileReaderTests.cs @@ -0,0 +1,147 @@ +// Copyright (c) Craftwork Games. All rights reserved. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. + + +using System; +using System.IO; +using System.Linq; +using MonoGame.Extended.BitmapFonts.BmfTypes; +using MonoGame.Extended.BitmapFonts.Serialization; + +namespace MonoGame.Extended.BitmapFonts.Tests; + +public class BitmapFontFileReaderTests +{ + private readonly BmfFile _expected; + + public BitmapFontFileReaderTests() + { + _expected = CreateExpected(); + } + + private static BmfFile CreateExpected() + { + BmfFile bmfFile = new BmfFile() + { + Header = new BmfHeader() + { + B = (byte)'B', + M = (byte)'M', + F = (byte)'F', + Version = 3 + }, + Info = new BmfInfoBlock() + { + FontSize = 32, + BitField = 0b1100_0000, + CharSet = 0, + StretchH = 50, + AA = 1, + PaddingUp = 1, + PaddingRight = 2, + PaddingDown = 3, + PaddingLeft = 4, + SpacingHoriz = 6, + SpacingVert = 5, + Outline = 2 + }, + FontName = "Cute Dino", + Common = new BmfCommonBlock() + { + LineHeight = 16, + Base = 12, + ScaleW = 256, + ScaleH = 256, + Pages = 1, + BitField = 0b0000_0000, + AlphaChnl = 1, + RedChnl = 0, + GreenChnl = 0, + BlueChnl = 0, + } + }; + + bmfFile.Pages.Add("test-font_0.png"); + bmfFile.Characters.Add(new BmfCharsBlock() + { + ID = 70, + X = 34, + Y = 0, + Width = 27, + Height = 20, + XOffset = -5, + YOffset = -3, + XAdvance = 17, + Page = 0, + Chnl = 15 + }); + bmfFile.Characters.Add(new BmfCharsBlock() + { + ID = 74, + X = 0, + Y = 0, + Width = 28, + Height = 20, + XOffset = -6, + YOffset = -3, + XAdvance = 18, + Page = 0, + Chnl = 15, + }); + + bmfFile.Kernings.Add(new BmfKerningPairsBlock() + { + First = 70, + Second = 74, + Amount = -1 + }); + + return bmfFile; + } + + [Fact] + public void Read_BinaryFile_Test() + { + using FileStream stream = File.OpenRead("BitmapFonts/files/bmfont/test-font-binary.fnt"); + BmfFile actual = new BmfFile(); + BmfBinaryLoader.Load(actual, stream); + Assert.Equal(_expected.Header, actual.Header); + Assert.Equal(_expected.Info, actual.Info); + Assert.Equal(_expected.Common, actual.Common); + Assert.Equal(_expected.FontName, actual.FontName); + Assert.True(_expected.Pages.SequenceEqual(actual.Pages)); + Assert.True(_expected.Characters.SequenceEqual(actual.Characters)); + Assert.True(_expected.Kernings.SequenceEqual(actual.Kernings)); + } + + [Fact] + public void Read_XmlFile_Test() + { + using FileStream stream = File.OpenRead("BitmapFonts/files/bmfont/test-font-xml.fnt"); + BmfFile actual = new BmfFile(); + BmfXmlLoader.Load(actual, stream); + Assert.Equal(_expected.Header, actual.Header); + Assert.Equal(_expected.Info, actual.Info); + Assert.Equal(_expected.Common, actual.Common); + Assert.Equal(_expected.FontName, actual.FontName); + Assert.True(_expected.Pages.SequenceEqual(actual.Pages)); + Assert.True(_expected.Characters.SequenceEqual(actual.Characters)); + Assert.True(_expected.Kernings.SequenceEqual(actual.Kernings)); + } + + [Fact] + public void Read_Text_Test() + { + using FileStream stream = File.OpenRead("BitmapFonts/files/bmfont/test-font-text.fnt"); + BmfFile actual = new BmfFile(); + BmfTextLoader.Load(actual, stream); + Assert.Equal(_expected.Header, actual.Header); + Assert.Equal(_expected.Info, actual.Info); + Assert.Equal(_expected.Common, actual.Common); + Assert.Equal(_expected.FontName, actual.FontName); + Assert.True(_expected.Pages.SequenceEqual(actual.Pages)); + Assert.True(_expected.Characters.SequenceEqual(actual.Characters)); + Assert.True(_expected.Kernings.SequenceEqual(actual.Kernings)); + } +} diff --git a/tests/MonoGame.Extended.Tests/BitmapFonts/BitmapFontTests.cs b/tests/MonoGame.Extended.Tests/BitmapFonts/BitmapFontTests.cs index bea02f22..6a0c0eba 100644 --- a/tests/MonoGame.Extended.Tests/BitmapFonts/BitmapFontTests.cs +++ b/tests/MonoGame.Extended.Tests/BitmapFonts/BitmapFontTests.cs @@ -13,7 +13,7 @@ namespace MonoGame.Extended.Tests.BitmapFonts { var font = CreateTestFont(); - Assert.Equal("Impact", font.Name); + Assert.Equal("Impact", font.Face); Assert.Equal(22, font.LineHeight); } @@ -43,6 +43,7 @@ namespace MonoGame.Extended.Tests.BitmapFonts public void BitmapFont_MeasureString_MultipleLines_Test() { var font = CreateTestFont(); + //var size = font.MeasureString("box fox\nbox of fox"); var size = font.MeasureString("box fox\nbox of fox"); Assert.Equal(123, size.Width); @@ -68,8 +69,8 @@ namespace MonoGame.Extended.Tests.BitmapFonts { var font = CreateTestFont(); - var noSpaceAtEnd = font.MeasureString("Hello World"); - var spaceAtEnd = font.MeasureString("Hello World "); + var noSpaceAtEnd = font.MeasureString("bfox"); + var spaceAtEnd = font.MeasureString("bfox "); Assert.NotEqual(noSpaceAtEnd, spaceAtEnd); @@ -81,14 +82,14 @@ namespace MonoGame.Extended.Tests.BitmapFonts var regions = new[] { // extracted from 'Impact' font. 'x' is particularly interesting because it has a negative x offset - new BitmapFontRegion(textureRegion, character: ' ', xOffset: 0, yOffset: 0, xAdvance: 6), - new BitmapFontRegion(textureRegion, character: 'b', xOffset: 0, yOffset: 7, xAdvance: 17), - new BitmapFontRegion(textureRegion, character: 'f', xOffset: 0, yOffset: 7, xAdvance: 9), - new BitmapFontRegion(textureRegion, character: 'o', xOffset: 0, yOffset: 11, xAdvance: 16), - new BitmapFontRegion(textureRegion, character: 'x', xOffset: -1, yOffset: 11, xAdvance: 13), + new BitmapFontCharacter(textureRegion: textureRegion, character: ' ', xOffset: 0, yOffset: 0, xAdvance: 6), + new BitmapFontCharacter(textureRegion: textureRegion, character: 'b', xOffset: 0, yOffset: 7, xAdvance: 17), + new BitmapFontCharacter(textureRegion: textureRegion, character: 'f', xOffset: 0, yOffset: 7, xAdvance: 9), + new BitmapFontCharacter(textureRegion: textureRegion, character: 'o', xOffset: 0, yOffset: 11, xAdvance: 16), + new BitmapFontCharacter(textureRegion: textureRegion, character: 'x', xOffset: -1, yOffset: 11, xAdvance: 13), }; - var font = new BitmapFont("Impact", regions, lineHeight: 22); + var font = new BitmapFont("Impact", size: 32, lineHeight: 22, regions); return font; } } diff --git a/tests/MonoGame.Extended.Tests/BitmapFonts/files/bmfont/test-font-binary.fnt b/tests/MonoGame.Extended.Tests/BitmapFonts/files/bmfont/test-font-binary.fnt new file mode 100644 index 00000000..3d29dca3 Binary files /dev/null and b/tests/MonoGame.Extended.Tests/BitmapFonts/files/bmfont/test-font-binary.fnt differ diff --git a/tests/MonoGame.Extended.Tests/BitmapFonts/files/bmfont/test-font-text.fnt b/tests/MonoGame.Extended.Tests/BitmapFonts/files/bmfont/test-font-text.fnt new file mode 100644 index 00000000..b6aecfb1 --- /dev/null +++ b/tests/MonoGame.Extended.Tests/BitmapFonts/files/bmfont/test-font-text.fnt @@ -0,0 +1,8 @@ +info face="Cute Dino" size=32 bold=0 italic=0 charset="" unicode=1 stretchH=50 smooth=1 aa=1 padding=1,2,3,4 spacing=6,5 outline=2 +common lineHeight=16 base=12 scaleW=256 scaleH=256 pages=1 packed=0 alphaChnl=1 redChnl=0 greenChnl=0 blueChnl=0 +page id=0 file="test-font_0.png" +chars count=2 +char id=70 x=34 y=0 width=27 height=20 xoffset=-5 yoffset=-3 xadvance=17 page=0 chnl=15 +char id=74 x=0 y=0 width=28 height=20 xoffset=-6 yoffset=-3 xadvance=18 page=0 chnl=15 +kernings count=1 +kerning first=70 second=74 amount=-1 diff --git a/tests/MonoGame.Extended.Tests/BitmapFonts/files/bmfont/test-font-xml.fnt b/tests/MonoGame.Extended.Tests/BitmapFonts/files/bmfont/test-font-xml.fnt new file mode 100644 index 00000000..3e3fcf05 --- /dev/null +++ b/tests/MonoGame.Extended.Tests/BitmapFonts/files/bmfont/test-font-xml.fnt @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/tests/MonoGame.Extended.Tests/BitmapFonts/files/bmfont/test-font_0.png b/tests/MonoGame.Extended.Tests/BitmapFonts/files/bmfont/test-font_0.png new file mode 100644 index 00000000..2fc23a50 Binary files /dev/null and b/tests/MonoGame.Extended.Tests/BitmapFonts/files/bmfont/test-font_0.png differ diff --git a/tests/MonoGame.Extended.Tests/MonoGame.Extended.Tests.csproj b/tests/MonoGame.Extended.Tests/MonoGame.Extended.Tests.csproj index 9e831ff0..c12f9d2c 100644 --- a/tests/MonoGame.Extended.Tests/MonoGame.Extended.Tests.csproj +++ b/tests/MonoGame.Extended.Tests/MonoGame.Extended.Tests.csproj @@ -1,12 +1,15 @@ - + - - - - + + + + - - - + + + + + +