Refactor BitmapFont (#887)

* Update file header

* Created initial project to move BitmapFont too

* XMl, Text, and Binar reading now supported for BMFont

* Refactor BittmapFont

* Update content pipeline from BitmapFont refactor

* Update changelog
This commit is contained in:
Christopher Whitley
2024-06-11 00:25:10 -04:00
committed by GitHub
parent 1f0235c605
commit 66277568c8
39 changed files with 1775 additions and 641 deletions
@@ -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; }
}
}
@@ -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; }
}
}
@@ -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<BitmapFontPage> Pages { get; set; }
[XmlArray("chars")]
[XmlArrayItem("char")]
public List<BitmapFontChar> Chars { get; set; }
[XmlArray("kernings")]
[XmlArrayItem("kerning")]
public List<BitmapFontKerning> Kernings { get; set; }
}
}
@@ -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<BitmapFontFile>
public class BitmapFontImporter : ContentImporter<ContentImporterResult<BmfFile>>
{
public override BitmapFontFile Import(string filename, ContentImporterContext context)
public override ContentImporterResult<BmfFile> 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<BmfFile>(filename, file);
}
}
}
}
@@ -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; }
}
}
@@ -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; }
}
}
@@ -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; }
}
}
@@ -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<BitmapFontFile, BitmapFontProcessorResult>
public class BitmapFontProcessor : ContentProcessor<ContentImporterResult<BmfFile>, BitmapFontProcessorResult>
{
public override BitmapFontProcessorResult Process(BitmapFontFile bitmapFontFile, ContentProcessorContext context)
public override BitmapFontProcessorResult Process(ContentImporterResult<BmfFile> 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
}
}
}
}
}
@@ -1,16 +1,17 @@
using System.Collections.Generic;
using MonoGame.Extended.BitmapFonts.BmfTypes;
namespace MonoGame.Extended.Content.Pipeline.BitmapFonts
{
public class BitmapFontProcessorResult
{
public List<string> 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<string>();
}
}
}
}
@@ -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";
}
}
}
}
@@ -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);
}
@@ -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)
@@ -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<BitmapFont>(contentManager, font => font.Name));
options.Converters.Add(new ContentManagerJsonConverter<BitmapFont>(contentManager, font => font.Face));
options.Converters.Add(new ControlStyleJsonConverter(customControlTypes));
options.Converters.Add(new GuiTextureAtlasJsonConverter(contentManager, textureRegionService));
options.Converters.Add(new GuiNinePatchRegion2DJsonConverter(textureRegionService));
@@ -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;
///// <summary>
///// Provides extension methods for <see cref="BitmapFont"/>.
///// </summary>
//public static class BitmapFontExtensions
//{
// /// <summary>
// /// Measures the size of the given text when rendered with the specified font.
// /// </summary>
// /// <param name="font">The <see cref="BitmapFont"/> used to render the text.</param>
// /// <param name="text">The text to measure.</param>
// /// <returns>The size of the rendered text.</returns>
// 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;
// }
// /// <summary>
// /// Measures the size of the given text when rendered with the specified font.
// /// </summary>
// /// <param name="font">The <see cref="BitmapFont"/> used to render the text.</param>
// /// <param name="text">The text to measure.</param>
// /// <returns>The size of the rendered text.</returns>
// 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<char> chunk in text.GetChunks())
// {
// MeasureStringInternal(font, chunk.Span, ref size, ref offset, ref firstCharacterOfLine);
// }
// return size;
// }
// /// <summary>
// /// Gets the rectangular bounds of the specified text when rendered with the specified font.
// /// </summary>
// /// <param name="font">The <see cref="BitmapFont"/> used to render the text.</param>
// /// <param name="text">The text to measure.</param>
// /// <param name="location">The top-left corner of the bounding rectangle.</param>
// /// <returns>The rectangular bounds of the rendered text.</returns>
// public static RectangleF GetStringBounds(this BitmapFont font, string text, Vector2 location)
// {
// SizeF size = font.MeasureString(text);
// return new RectangleF(location, size);
// }
// /// <summary>
// /// Gets the rectangular bounds of the specified text when rendered with the specified font.
// /// </summary>
// /// <param name="font">The <see cref="BitmapFont"/> used to render the text.</param>
// /// <param name="text">The text to measure.</param>
// /// <param name="location">The top-left corner of the bounding rectangle.</param>
// /// <returns>The rectangular bounds of the rendered text.</returns>
// 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<char> 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;
// }
//}
+386 -330
View File
@@ -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<int, BitmapFontCharacter> _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<BitmapFontCharacter> characters)
{
private readonly Dictionary<int, BitmapFontRegion> _characterMap = new Dictionary<int, BitmapFontRegion>();
Face = face;
Size = size;
LineHeight = lineHeight;
_characters = new Dictionary<int, BitmapFontCharacter>();
public BitmapFont(string name, IEnumerable<BitmapFontRegion> 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<BitmapFontGlyph>
{
private readonly StringGlyphEnumerator _enumerator;
public StringGlyphEnumerable(BitmapFont font, string text, Vector2? position)
{
_enumerator = new StringGlyphEnumerator(font, text, position);
}
public StringGlyphEnumerator GetEnumerator()
{
return _enumerator;
}
IEnumerator<BitmapFontGlyph> IEnumerable<BitmapFontGlyph>.GetEnumerator()
{
return _enumerator;
}
IEnumerator IEnumerable.GetEnumerator()
{
return _enumerator;
}
}
public struct StringGlyphEnumerator : IEnumerator<BitmapFontGlyph>
{
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<BitmapFontGlyph>
{
private readonly StringBuilderGlyphEnumerator _enumerator;
public StringBuilderGlyphEnumerable(BitmapFont font, StringBuilder text, Vector2? position)
{
_enumerator = new StringBuilderGlyphEnumerator(font, text, position);
}
public StringBuilderGlyphEnumerator GetEnumerator()
{
return _enumerator;
}
IEnumerator<BitmapFontGlyph> IEnumerable<BitmapFontGlyph>.GetEnumerator()
{
return _enumerator;
}
IEnumerator IEnumerable.GetEnumerator()
{
return _enumerator;
}
}
public struct StringBuilderGlyphEnumerator : IEnumerator<BitmapFontGlyph>
{
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<BitmapFontGlyph>
{
private readonly StringGlyphEnumerator _enumerator;
public StringGlyphEnumerable(BitmapFont font, string text, Vector2? position)
{
_enumerator = new StringGlyphEnumerator(font, text, position);
}
public StringGlyphEnumerator GetEnumerator()
{
return _enumerator;
}
IEnumerator<BitmapFontGlyph> IEnumerable<BitmapFontGlyph>.GetEnumerator()
{
return _enumerator;
}
IEnumerator IEnumerable.GetEnumerator()
{
return _enumerator;
}
}
public struct StringGlyphEnumerator : IEnumerator<BitmapFontGlyph>
{
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<BitmapFontGlyph>
{
private readonly StringBuilderGlyphEnumerator _enumerator;
public StringBuilderGlyphEnumerable(BitmapFont font, StringBuilder text, Vector2? position)
{
_enumerator = new StringBuilderGlyphEnumerator(font, text, position);
}
public StringBuilderGlyphEnumerator GetEnumerator()
{
return _enumerator;
}
IEnumerator<BitmapFontGlyph> IEnumerable<BitmapFontGlyph>.GetEnumerator()
{
return _enumerator;
}
IEnumerator IEnumerable.GetEnumerator()
{
return _enumerator;
}
}
public struct StringBuilderGlyphEnumerator : IEnumerator<BitmapFontGlyph>
{
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;
}
}
/// <inheritdoc/>
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<string, Texture2D> pages = new Dictionary<string, Texture2D>();
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<int, BitmapFontCharacter> characters = new Dictionary<int, BitmapFontCharacter>();
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);
}
}
@@ -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;
/// <summary>
/// Represents a character in a bitmap font. This class cannot be inherited.
/// </summary>
public sealed class BitmapFontCharacter
{
/// <summary>
/// Gets the character code.
/// </summary>
public int Character { get; }
/// <summary>
/// Gets the texture region that contains the character's image.
/// </summary>
public TextureRegion2D TextureRegion { get; }
/// <summary>
/// Gets the horizontal offset for rendering the character.
/// </summary>
public int XOffset { get; }
/// <summary>
/// Gets the vertical offset for rendering the character.
/// </summary>
public int YOffset { get; }
/// <summary>
/// Gets the horizontal advance value for rendering the next character.
/// </summary>
public int XAdvance { get; }
/// <summary>
/// Gets the dictionary of kerning values for pairs of characters.
/// </summary>
public Dictionary<int, int> Kernings { get; }
/// <summary>
/// Initializes a new instance of the <see cref="BitmapFontCharacter"/> class.
/// </summary>
/// <param name="character">The character code.</param>
/// <param name="textureRegion">The texture region that contains the character's image.</param>
/// <param name="xOffset">The horizontal offset for rendering the character.</param>
/// <param name="yOffset">The vertical offset for rendering the character.</param>
/// <param name="xAdvance">The horizontal advance value for rendering the next character.</param>
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<int, int>();
}
}
@@ -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
/// <param name="clippingRectangle">Clips the boundaries of the text so that it's not drawn outside the clipping rectangle</param>
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
/// <param name="clippingRectangle">Clips the boundaries of the text so that it's not drawn outside the clipping rectangle</param>
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);
}
}
}
}
@@ -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<Texture2D>(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);
}
}
}
}
@@ -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<int, int>();
}
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<int, int> Kernings { get; }
public override string ToString()
{
return $"{Convert.ToChar(Character)} {TextureRegion}";
}
}
}
@@ -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;
}
@@ -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;
}
@@ -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<string> Pages = new List<string>();
public readonly List<BmfCharsBlock> Characters = new List<BmfCharsBlock>();
public readonly List<BmfKerningPairsBlock> Kernings = new List<BmfKerningPairsBlock>();
public static BmfFile FromStream(FileStream stream)
{
using BinaryReader reader = new BinaryReader(stream);
long position = stream.Position;
ReadOnlySpan<byte> binaryHeader = stackalloc byte[] { 66, 77, 70, 3 };
ReadOnlySpan<byte> xmlHeader = stackalloc byte[] { 60, 63, 120, 109 };
ReadOnlySpan<byte> textHeader = stackalloc byte[] { 105, 110, 102, 111 };
Span<byte> 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!");
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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<BmfCharsBlock>();
BmfKerningPairsBlock[] kernings = Array.Empty<BmfKerningPairsBlock>();
while (reader.BaseStream.Position < reader.BaseStream.Length)
{
byte blockType = reader.ReadByte();
int blockSize = reader.ReadInt32();
switch (blockType)
{
case 1:
bmfFile.Info = AsType<BmfInfoBlock>(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<BmfCommonBlock>(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<BmfCharsBlock>(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<BmfKerningPairsBlock>(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<BmfHeader>(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<T>(ReadOnlySpan<byte> buffer) where T : struct
{
T value;
try
{
unsafe
{
fixed (byte* ptr = buffer)
{
value = Marshal.PtrToStructure<T>((IntPtr)ptr);
}
}
return value;
}
catch
{
return default;
}
}
}
@@ -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<string> 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<string> 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<string> 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<string> 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<string> 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<string> 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<string> GetTokens(string line)
{
List<string> tokens = new List<string>();
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;
}
}
@@ -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);
}
}
}
@@ -1,11 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>It makes MonoGame more awesome.</Description>
<PackageTags>monogame extended pipeline bmfont tiled texture atlas input viewport fps shapes sprite</PackageTags>
</PropertyGroup>
<PropertyGroup>
<Description>It makes MonoGame more awesome.</Description>
<PackageTags>monogame extended pipeline bmfont tiled texture atlas input viewport fps shapes sprite</PackageTags>
</PropertyGroup>
<!-- Allow Test project to access internals -->
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>MonoGame.Extended.Tests</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>MonoGame.Extended.Content.Pipeline</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
</ItemGroup>
</Project>
@@ -0,0 +1,208 @@
using System;
using System.Globalization;
using System.Xml;
/// <summary>
/// Provides extension methods for the <see cref="XmlNode"/> class.
/// </summary>
public static class XmlNodeExtensions
{
/// <summary>
/// Retrieves the value of the specified attribute from the <see cref="XmlNode"/>.
/// </summary>
/// <param name="node">The XML node.</param>
/// <param name="attribute">The name of the attribute.</param>
/// <param name="value">
/// When this method returns, contains the value of the attribute, if found; otherwise, an empty string.
/// </param>
/// <returns><c>true</c> if the attribute is found and has a non-empty value; otherwise, <c>false</c>.</returns>
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);
}
/// <summary>
/// Retrieves the string value of the specified attribute.
/// </summary>
/// <param name="node">The XML node.</param>
/// <param name="attribute">The name of the attribute.</param>
/// <returns>
/// The string value of the attribute, or the default value if the attribute is not found or is empty.
/// </returns>
public static string GetStringAttribute(this XmlNode node, string attribute)
{
if (node.GetAttributeValue(attribute, out string value))
{
return value;
}
return default;
}
/// <summary>
/// Retrieves the byte value of the specified attribute.
/// </summary>
/// <param name="node">The XML node.</param>
/// <param name="attribute">The name of the attribute.</param>
/// <returns>
/// The byte value of the attribute, or the default value if the attribute is not found or is empty.
/// </returns>
public static byte GetByteAttribute(this XmlNode node, string attribute)
{
if (node.GetAttributeValue(attribute, out string value))
{
return Convert.ToByte(value, CultureInfo.InvariantCulture);
}
return default;
}
/// <summary>
/// Retrieves the ushort value of the specified attribute.
/// </summary>
/// <param name="node">The XML node.</param>
/// <param name="attribute">The name of the attribute.</param>
/// <returns>
/// The ushort value of the attribute, or the default value if the attribute is not found or is empty.
/// </returns>
public static ushort GetUInt16Attribute(this XmlNode node, string attribute)
{
if (node.GetAttributeValue(attribute, out string value))
{
return Convert.ToUInt16(value, CultureInfo.InvariantCulture);
}
return default;
}
/// <summary>
/// Retrieves the short value of the specified attribute.
/// </summary>
/// <param name="node">The XML node.</param>
/// <param name="attribute">The name of the attribute.</param>
/// <returns>
/// The short value of the attribute, or the default value if the attribute is not found or is empty.
/// </returns>
public static short GetInt16Attribute(this XmlNode node, string attribute)
{
if (node.GetAttributeValue(attribute, out string value))
{
return Convert.ToInt16(value, CultureInfo.InvariantCulture);
}
return default;
}
/// <summary>
/// Retrieves the uint value of the specified attribute.
/// </summary>
/// <param name="node">The XML node.</param>
/// <param name="attribute">The name of the attribute.</param>
/// <returns>
/// The uint value of the attribute, or the default value if the attribute is not found or is empty.
/// </returns>
public static uint GetUInt32Attribute(this XmlNode node, string attribute)
{
if (node.GetAttributeValue(attribute, out string value))
{
return Convert.ToUInt32(value, CultureInfo.InvariantCulture);
}
return default;
}
/// <summary>
/// Retrieves the int value of the specified attribute.
/// </summary>
/// <param name="node">The XML node.</param>
/// <param name="attribute">The name of the attribute.</param>
/// <returns>
/// The int value of the attribute, or the default value if the attribute is not found or is empty.
/// </returns>
public static int GetInt32Attribute(this XmlNode node, string attribute)
{
if (node.GetAttributeValue(attribute, out string value))
{
return Convert.ToInt32(value, CultureInfo.InvariantCulture);
}
return default;
}
/// <summary>
/// Retrieves the float value of the specified attribute.
/// </summary>
/// <param name="node">The XML node.</param>
/// <param name="attribute">The name of the attribute.</param>
/// <returns>
/// The float value of the attribute, or the default value if the attribute is not found or is empty.
/// </returns>
public static float GetSingleAttribute(this XmlNode node, string attribute)
{
if (node.GetAttributeValue(attribute, out string value))
{
return Convert.ToSingle(value, CultureInfo.InvariantCulture);
}
return default;
}
/// <summary>
/// Retrieves the double value of the specified attribute.
/// </summary>
/// <param name="node">The XML node.</param>
/// <param name="attribute">The name of the attribute.</param>
/// <returns>
/// The double value of the attribute, or the default value if the attribute is not found or is empty.
/// </returns>
public static double GetDoubleAttribute(this XmlNode node, string attribute)
{
if (node.GetAttributeValue(attribute, out string value))
{
return Convert.ToDouble(value, CultureInfo.InvariantCulture);
}
return default;
}
/// <summary>
/// Retrieves the boolean value of the specified attribute.
/// </summary>
/// <param name="node">The XML node.</param>
/// <param name="attribute">The name of the attribute.</param>
/// <returns>
/// The boolean value of the attribute, or the default value if the attribute is not found or is empty.
/// </returns>
public static bool GetBoolAttribute(this XmlNode node, string attribute)
{
if (node.GetAttributeValue(attribute, out string value))
{
return Convert.ToBoolean(value, CultureInfo.InvariantCulture);
}
return default;
}
/// <summary>
/// Retrieves the byte values from the specified delimited attribute and returns them in an array.
/// </summary>
/// <param name="node">The XML node.</param>
/// <param name="attribute">The name of the attribute.</param>
/// <param name="expectedCount">The expected number of byte values.</param>
/// <returns>
/// An array of byte values parsed from the attribute, or an array of default values if the attribute is not found
/// or is empty.
/// </returns>
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;
}
}