using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using UnitTestSharp; using static Glass.BMFontDefinition; namespace Glass.UnitTests { public class BMFontDefinitionTests : TestFixture { public class TryReadStructTests : TestFixture { public static BinaryReader MakeReader(byte[] bytes) => new BinaryReader(new MemoryStream(bytes)); [StructLayout(LayoutKind.Sequential, Pack = 1)] struct ThreeBytes { public byte A; public byte B; public byte C; } [StructLayout(LayoutKind.Sequential, Pack = 1)] struct TwoShorts { public short X; public short Y; } public void ReturnsTrueAndPopulatesStruct() { var reader = MakeReader(new byte[] { 1, 2, 3, 4 }); var result = BMFontDefinition.TryReadStruct(reader, out var value); Check(result); CheckEqual(1, value.A); CheckEqual(2, value.B); CheckEqual(3, value.C); } public void ReturnsFalseWhenStreamIsEmpty() { var reader = MakeReader(Array.Empty()); var result = BMFontDefinition.TryReadStruct(reader, out _); CheckFalse(result); } public void ReturnsFalseWhenNotEnoughBytesRemain() { var reader = MakeReader(new byte[] { 1, 2 }); var result = BMFontDefinition.TryReadStruct(reader, out _); CheckFalse(result); } public void AdvancesStreamPosition() { var reader = MakeReader(new byte[] { 1, 2, 3, 99 }); BMFontDefinition.TryReadStruct(reader, out _); CheckEqual(3, reader.BaseStream.Position); } public void ReadsLittleEndianShorts() { var reader = MakeReader(new byte[] { 0x01, 0x00, 0x02, 0x00 }); BMFontDefinition.TryReadStruct(reader, out var value); CheckEqual(1, value.X); CheckEqual(2, value.Y); } public void ReadsFromCurrentPosition() { var stream = new MemoryStream(new byte[] { 99, 1, 2, 3 }); stream.Position = 1; var reader = new BinaryReader(stream); BMFontDefinition.TryReadStruct(reader, out var value); CheckEqual(1, value.A); CheckEqual(2, value.B); CheckEqual(3, value.C); } public void ExactlyEnoughBytesSucceeds() { var reader = MakeReader(new byte[] { 1, 2, 3 }); var result = BMFontDefinition.TryReadStruct(reader, out _); Check(result); } public void OutValueIsDefaultWhenReturnsFalse() { var reader = MakeReader(new byte[] { 1, 2 }); BMFontDefinition.TryReadStruct(reader, out var value); CheckEqual(0, value.A); CheckEqual(0, value.B); CheckEqual(0, value.C); } public void StreamPositionUnchangedWhenReturnsFalse() { var reader = MakeReader(new byte[] { 1, 2 }); BMFontDefinition.TryReadStruct(reader, out _); CheckEqual(0, reader.BaseStream.Position); } } public class ReadNullTerminatedStringTests : TestFixture { public static BinaryReader MakeReader(byte[] bytes) => new BinaryReader(new MemoryStream(bytes)); public static byte[] NullTerminated(string s) => Encoding.UTF8.GetBytes(s).Append((byte)0).ToArray(); public void BasicNullTerminatedString() { var bytes = NullTerminated("hello"); var reader = MakeReader(bytes); bool success = BMFontDefinition.ReadNullTerminatedString(reader, 256, out string result); Check(success); CheckEqual("hello", result); CheckEqual(bytes.Length, reader.BaseStream.Position); } public void EmptyString() { var reader = MakeReader(new byte[] { 0 }); bool success = BMFontDefinition.ReadNullTerminatedString(reader, 256, out string result); Check(success); CheckEqual("", result); CheckEqual(1, reader.BaseStream.Position); } public void EmptyStringWithMaxBytesOne() { var reader = MakeReader(new byte[] { 0 }); bool success = BMFontDefinition.ReadNullTerminatedString(reader, 1, out string result); Check(success); CheckEqual("", result); CheckEqual(1, reader.BaseStream.Position); } public void NoNullTerminatorReturnsWhatWasBufferedAndAdvancesStream() { var reader = MakeReader(Encoding.UTF8.GetBytes("hello")); bool success = BMFontDefinition.ReadNullTerminatedString(reader, 5, out string result); CheckFalse(success); CheckEqual("hello", result); CheckEqual(5, reader.BaseStream.Position); } public void MaxBytesTruncatesStringAndAdvancesStreamToMaxBytes() { var bytes = NullTerminated("hello"); var reader = MakeReader(bytes); bool success = BMFontDefinition.ReadNullTerminatedString(reader, 3, out string result); CheckFalse(success); CheckEqual("hel", result); CheckEqual(3, reader.BaseStream.Position); } public void InteriorNullTerminatesStringAndStreamAdvancesOnlyToStringLength() { var bytes = new byte[] { (byte)'h', (byte)'i', 0, (byte)'x', (byte)'y' }; var reader = MakeReader(bytes); bool success = BMFontDefinition.ReadNullTerminatedString(reader, 5, out string result); Check(success); CheckEqual("hi", result); CheckEqual(3, reader.BaseStream.Position); } public void MaxBytesLargerThanRemainingStreamClampsToAvailable() { var bytes = NullTerminated("hello"); var reader = MakeReader(bytes); bool success = BMFontDefinition.ReadNullTerminatedString(reader, 1000, out string result); Check(success); CheckEqual("hello", result); CheckEqual(bytes.Length, reader.BaseStream.Position); } public void Utf8MultiByteCharacters() { var text = "héllo"; var bytes = Encoding.UTF8.GetBytes(text).Append((byte)0).ToArray(); var reader = MakeReader(bytes); bool success = BMFontDefinition.ReadNullTerminatedString(reader, bytes.Length, out string result); Check(success); CheckEqual(text, result); CheckEqual(bytes.Length, reader.BaseStream.Position); } public void StringLongerThanMaxFontNameLengthTruncates() { var longName = new string('A', BMFontDefinition.MaxStringLength + 10); var bytes = Encoding.UTF8.GetBytes(longName).Append((byte)0).ToArray(); var reader = MakeReader(bytes); bool success = BMFontDefinition.ReadNullTerminatedString(reader, bytes.Length, out string result); Check(success); CheckEqual(new string('A', BMFontDefinition.MaxStringLength), result); CheckEqual(bytes.Length, reader.BaseStream.Position); } public void StringExactlyMaxFontNameLengthNotTruncated() { var name = new string('A', BMFontDefinition.MaxStringLength); var bytes = Encoding.UTF8.GetBytes(name).Append((byte)0).ToArray(); var reader = MakeReader(bytes); bool success = BMFontDefinition.ReadNullTerminatedString(reader, bytes.Length, out string result); Check(success); CheckEqual(name, result); CheckEqual(bytes.Length, reader.BaseStream.Position); } public void ReadsFromCurrentStreamPosition() { var bytes = new byte[] { 99 }.Concat(NullTerminated("hello")).ToArray(); var stream = new MemoryStream(bytes); stream.Position = 1; var reader = new BinaryReader(stream); bool success = BMFontDefinition.ReadNullTerminatedString(reader, 256, out string result); Check(success); CheckEqual("hello", result); CheckEqual(bytes.Length, reader.BaseStream.Position); } public void MaxBytesZeroReturnsEmptyString() { var reader = MakeReader(NullTerminated("hello")); bool success = BMFontDefinition.ReadNullTerminatedString(reader, 0, out string result); CheckFalse(success); CheckEqual("", result); CheckEqual(0, reader.BaseStream.Position); } } public class BMFontChannelToGlassChannelTests : TestFixture { public void BlueChannelReturnsTwo() { CheckEqual(2, BMFontDefinition.BMFontChannelToGlassChannel(1)); } public void GreenChannelReturnsOne() { CheckEqual(1, BMFontDefinition.BMFontChannelToGlassChannel(2)); } public void RedChannelReturnsZero() { CheckEqual(0, BMFontDefinition.BMFontChannelToGlassChannel(4)); } public void AlphaChannelReturnsThree() { CheckEqual(3, BMFontDefinition.BMFontChannelToGlassChannel(8)); } public void AllChannelsReturnsNull() { CheckNull(BMFontDefinition.BMFontChannelToGlassChannel(15)); } public void InvalidChannelThrowsArgumentException() { CheckThrow(typeof(ArgumentException)); BMFontDefinition.BMFontChannelToGlassChannel(99); } } public class ToGlyphDefinitionTests : TestFixture { public static BmfCharInfo MakeCharInfo() => new BmfCharInfo { X = 1, Y = 2, Width = 3, Height = 4, XOffset = 5, YOffset = 6, XAdvance = 7, Channel = 4, }; public void OriginMapsFromXAndY() { var result = BMFontDefinition.ToGlyphDefinition(MakeCharInfo()); CheckEqual(new Point(1, 2), result.Origin); } public void SizeMapsFromWidthAndHeight() { var result = BMFontDefinition.ToGlyphDefinition(MakeCharInfo()); CheckEqual(new Point(3, 4), result.Size); } public void OffsetMapsFromXOffsetAndYOffset() { var result = BMFontDefinition.ToGlyphDefinition(MakeCharInfo()); CheckEqual(new Point(5, 6), result.Offset); } public void XAdvanceMapsFromXAdvance() { var result = BMFontDefinition.ToGlyphDefinition(MakeCharInfo()); CheckEqual(7, result.XAdvance); } public void ChannelIsDelegatedToBMFontChannelToGlassChannel() { var result = BMFontDefinition.ToGlyphDefinition(MakeCharInfo()); CheckEqual(0, result.Channel); } public void ChannelIsNullWhenAllChannels() { var charInfo = MakeCharInfo(); charInfo.Channel = 15; var result = BMFontDefinition.ToGlyphDefinition(charInfo); CheckNull(result.Channel); } public void NegativeOffsetsRoundTripCorrectly() { var charInfo = MakeCharInfo(); charInfo.XOffset = -3; charInfo.YOffset = -7; var result = BMFontDefinition.ToGlyphDefinition(charInfo); CheckEqual(new Point(-3, -7), result.Offset); } public void NegativeXAdvanceRoundTripsCorrectly() { var charInfo = MakeCharInfo(); charInfo.XAdvance = -5; var result = BMFontDefinition.ToGlyphDefinition(charInfo); CheckEqual(-5, result.XAdvance); } } public class FromBinaryFileTests : TestFixture { static byte[] ValidHeader() => new byte[] { 66, 77, 70, // B, M, F 3, // version }; public static Stream MakeStream(byte[] bytes) => new MemoryStream(bytes); public void FileTooShortThrows() { CheckThrow(typeof(InvalidDataException)); BMFontDefinition.FromBinaryFile(MakeStream(new byte[] { 66, 77 })); } public void InvalidMagicBytesThrows() { CheckThrow(typeof(InvalidDataException)); BMFontDefinition.FromBinaryFile(MakeStream(new byte[] { 0, 0, 0, 3 })); } public void InvalidVersionThrows() { CheckThrow(typeof(InvalidDataException)); BMFontDefinition.FromBinaryFile(MakeStream(new byte[] { 66, 77, 70, 2 })); } public void ValidHeaderWithNoBlocksSucceeds() { var result = BMFontDefinition.FromBinaryFile(MakeStream(ValidHeader())); CheckNotNull(result); } public static byte[] WriteBlockHeader(byte blockType, int blockSize) { var bytes = new byte[5]; bytes[0] = blockType; BitConverter.GetBytes(blockSize).CopyTo(bytes, 1); return bytes; } public static byte[] Concat(params byte[][] arrays) => arrays.SelectMany(a => a).ToArray(); public void UnknownBlockTypeIsSkipped() { var unknownBlock = Concat( WriteBlockHeader(99, 3), new byte[] { 1, 2, 3 } ); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), unknownBlock))); CheckNotNull(result); } public void MultipleBlocksProcessedInSequence() { var block1 = Concat(WriteBlockHeader(99, 2), new byte[] { 1, 2 }); var block2 = Concat(WriteBlockHeader(99, 2), new byte[] { 3, 4 }); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block1, block2))); CheckNotNull(result); } public void BlockSizeLargerThanRemainingStreamThrows() { var block = Concat( WriteBlockHeader(99, 1000), // Claims 1000 bytes but stream has none. new byte[] { 1, 2, 3 } ); CheckThrow(typeof(InvalidDataException)); BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); } public void TruncatedBlockHeaderIsHandledGracefully() { var truncatedHeader = new byte[] { 99 }; // Block type but no block size. var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), truncatedHeader))); CheckNotNull(result); } public void EmptyBlockIsHandledGracefully() { var emptyBlock = WriteBlockHeader(99, 0); // BlockSize of 0, no following bytes. var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), emptyBlock))); CheckNotNull(result); } public void FileLengthThreeThrows() { CheckThrow(typeof(InvalidDataException)); BMFontDefinition.FromBinaryFile(MakeStream(new byte[] { 66, 77, 70 })); } public void KnownBlockFollowingUnknownBlockIsProcessedCorrectly() { // Verifies seeking works correctly across different block types. var unknownBlock = Concat(WriteBlockHeader(99, 2), new byte[] { 1, 2 }); var commonBlock = Concat( WriteBlockHeader(2, Marshal.SizeOf()), new byte[Marshal.SizeOf()] ); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), unknownBlock, commonBlock))); CheckNotNull(result); } public void BlockHeaderTruncatedAfterTwoByteThrows() { // Stream ends mid-block-size integer (2 bytes of 4 written). var truncatedHeader = new byte[] { 99, 0, 0 }; var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), truncatedHeader))); CheckNotNull(result); } public void BlockHeaderTruncatedAfterFourBytesHandledGracefully() { // Stream ends with block type + 3 bytes of block size written. var truncatedHeader = new byte[] { 99, 0, 0, 0 }; var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), truncatedHeader))); CheckNotNull(result); } public static byte[] WriteInfoBlock(string name) { var infoStruct = new byte[Marshal.SizeOf()]; var nameBytes = Encoding.UTF8.GetBytes(name).Append((byte)0).ToArray(); var blockData = Concat(infoStruct, nameBytes); return Concat(WriteBlockHeader(1, blockData.Length), blockData); } public static byte[] WriteInfoBlockNoName() { var infoStruct = new byte[Marshal.SizeOf()]; return Concat(WriteBlockHeader(1, infoStruct.Length), infoStruct); } public void InfoBlockNameIsRead() { var result = BMFontDefinition.FromBinaryFile( MakeStream(Concat(ValidHeader(), WriteInfoBlock("MyFont")))); CheckEqual("MyFont", result.Name); } public void InfoBlockEmptyNameIsRead() { var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), WriteInfoBlock("")))); CheckEqual("", result.Name); } public void InfoBlockWithNoNameBytesResultsInEmptyString() { var result = BMFontDefinition.FromBinaryFile( MakeStream(Concat(ValidHeader(), WriteInfoBlockNoName()))); CheckEqual("", result.Name); } public void InfoBlockTooSmallThrows() { var tooSmall = new byte[Marshal.SizeOf() - 1]; var block = Concat(WriteBlockHeader(1, tooSmall.Length), tooSmall); CheckThrow(typeof(InvalidDataException)); BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); } public void InfoBlockFollowedByAnotherBlockIsProcessedCorrectly() { var commonBlock = Concat( WriteBlockHeader(2, Marshal.SizeOf()), new byte[Marshal.SizeOf()] ); var result = BMFontDefinition.FromBinaryFile( MakeStream(Concat(ValidHeader(), WriteInfoBlock("MyFont"), commonBlock))); CheckEqual("MyFont", result.Name); CheckNotNull(result); } public static byte[] WriteCommonBlock(ushort lineHeight = 0, ushort baseline = 0) { int size = Marshal.SizeOf(); var bytes = new byte[size]; BitConverter.GetBytes(lineHeight).CopyTo(bytes, 0); BitConverter.GetBytes(baseline).CopyTo(bytes, 2); return Concat(WriteBlockHeader(2, size), bytes); } public void CommonBlockLineHeightIsRead() { var result = BMFontDefinition.FromBinaryFile( MakeStream(Concat(ValidHeader(), WriteCommonBlock(lineHeight: 32)))); CheckEqual(32, result.LineHeight); } public void CommonBlockBaselineIsRead() { var result = BMFontDefinition.FromBinaryFile( MakeStream(Concat(ValidHeader(), WriteCommonBlock(baseline: 24)))); CheckEqual(24, result.Baseline); } public void CommonBlockLineHeightAndBaselineAreIndependent() { var result = BMFontDefinition.FromBinaryFile( MakeStream(Concat(ValidHeader(), WriteCommonBlock(lineHeight: 32, baseline: 24)))); CheckEqual(32, result.LineHeight); CheckEqual(24, result.Baseline); } public void CommonBlockTooSmallThrows() { var tooSmall = new byte[Marshal.SizeOf() - 1]; var block = Concat(WriteBlockHeader(2, tooSmall.Length), tooSmall); CheckThrow(typeof(InvalidDataException)); BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); } public void CommonBlockFollowedByAnotherBlockIsProcessedCorrectly() { var unknownBlock = Concat(WriteBlockHeader(99, 2), new byte[] { 1, 2 }); var result = BMFontDefinition.FromBinaryFile( MakeStream(Concat(ValidHeader(), WriteCommonBlock(lineHeight: 32), unknownBlock))); CheckEqual(32, result.LineHeight); CheckNotNull(result); } public static byte[] WriteCharInfo(uint id, short xAdvance = 0, ushort x = 0, ushort y = 0, ushort width = 0, ushort height = 0, short xOffset = 0, short yOffset = 0, byte page = 0, byte channel = 15) { int size = Marshal.SizeOf(); var bytes = new byte[size]; BitConverter.GetBytes(id).CopyTo(bytes, 0); BitConverter.GetBytes(x).CopyTo(bytes, 4); BitConverter.GetBytes(y).CopyTo(bytes, 6); BitConverter.GetBytes(width).CopyTo(bytes, 8); BitConverter.GetBytes(height).CopyTo(bytes, 10); BitConverter.GetBytes(xOffset).CopyTo(bytes, 12); BitConverter.GetBytes(yOffset).CopyTo(bytes, 14); BitConverter.GetBytes(xAdvance).CopyTo(bytes, 16); bytes[18] = page; bytes[19] = channel; return bytes; } public static byte[] WriteCharsBlock(params byte[][] charInfos) { var blockData = Concat(charInfos); return Concat(WriteBlockHeader(4, blockData.Length), blockData); } public void SingleGlyphIsAddedToGlyphDefinitions() { var block = WriteCharsBlock(WriteCharInfo((uint)'A')); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); Check(result.GlyphDefinitions.ContainsKey('A')); } public void MultipleGlyphsAreAllAdded() { var block = WriteCharsBlock( WriteCharInfo((uint)'A'), WriteCharInfo((uint)'B'), WriteCharInfo((uint)'C') ); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); Check(result.GlyphDefinitions.ContainsKey('A')); Check(result.GlyphDefinitions.ContainsKey('B')); Check(result.GlyphDefinitions.ContainsKey('C')); } public void GlyphDefinitionIsWiredCorrectly() { var block = WriteCharsBlock(WriteCharInfo((uint)'A', xAdvance: 7)); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); CheckEqual(7, result.GlyphDefinitions['A'].XAdvance); } public void DuplicateNormalGlyphSilentlyOverwrites() { var block = WriteCharsBlock( WriteCharInfo((uint)'A', xAdvance: 5), WriteCharInfo((uint)'A', xAdvance: 9)); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); CheckEqual(9, result.GlyphDefinitions['A'].XAdvance); } public void InvalidGlyphIsAssignedToInvalidGlyph() { var block = WriteCharsBlock(WriteCharInfo(0xFFFFFFFF, xAdvance: 7)); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); CheckNotNull(result.InvalidGlyph); CheckEqual(7, result.InvalidGlyph.XAdvance); } public void DuplicateInvalidGlyphSilentlyOverwrites() { var block = WriteCharsBlock( WriteCharInfo(0xFFFFFFFF, xAdvance: 5), WriteCharInfo(0xFFFFFFFF, xAdvance: 9) ); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); CheckEqual(9, result.InvalidGlyph.XAdvance); } public void EmptyCharsBlockLeavesGlyphDefinitionsEmptyAndInvalidGlyphNull() { var block = WriteCharsBlock(); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); CheckEqual(0, result.GlyphDefinitions.Count); CheckNull(result.InvalidGlyph); } public void NullCharacterIsAddedToGlyphDefinitions() { var block = WriteCharsBlock(WriteCharInfo(0)); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); Check(result.GlyphDefinitions.ContainsKey('\0')); } public void MaxValidCharacterIdIsAddedToGlyphDefinitions() { var block = WriteCharsBlock(WriteCharInfo(0xFFFF)); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); Check(result.GlyphDefinitions.ContainsKey((char)0xFFFF)); } public void CharacterIdOver0xFFFFThrows() { var block = WriteCharsBlock(WriteCharInfo(0x00010000)); CheckThrow(typeof(InvalidDataException)); BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); } public void CharsBlockSizeNotDivisibleByCharSizeThrows() { var blockData = new byte[Marshal.SizeOf() + 1]; var block = Concat(WriteBlockHeader(4, blockData.Length), blockData); CheckThrow(typeof(InvalidDataException)); BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); } public void MixedNormalAndInvalidGlyphsAreAllProcessedCorrectly() { var block = WriteCharsBlock( WriteCharInfo((uint)'A', xAdvance: 5), WriteCharInfo(0xFFFFFFFF, xAdvance: 7), WriteCharInfo((uint)'B', xAdvance: 9) ); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); Check(result.GlyphDefinitions.ContainsKey('A')); CheckEqual(5, result.GlyphDefinitions['A'].XAdvance); CheckNotNull(result.InvalidGlyph); CheckEqual(7, result.InvalidGlyph.XAdvance); Check(result.GlyphDefinitions.ContainsKey('B')); CheckEqual(9, result.GlyphDefinitions['B'].XAdvance); } public void CharsBlockFollowedByAnotherBlockIsProcessedCorrectly() { var charsBlock = WriteCharsBlock(WriteCharInfo((uint)'A')); var unknownBlock = Concat(WriteBlockHeader(99, 2), new byte[] { 1, 2 }); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), charsBlock, unknownBlock))); Check(result.GlyphDefinitions.ContainsKey('A')); } public void TruncatedCharInfoThrows() { int charSize = Marshal.SizeOf(); var blockData = WriteCharInfo((uint)'A'); var block = Concat(WriteBlockHeader(4, charSize * 2), blockData); CheckThrow(typeof(InvalidDataException)); BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); } public static byte[] WriteKerningPair(uint first, uint second, short amount = 0) { int size = Marshal.SizeOf(); var bytes = new byte[size]; BitConverter.GetBytes(first).CopyTo(bytes, 0); BitConverter.GetBytes(second).CopyTo(bytes, 4); BitConverter.GetBytes(amount).CopyTo(bytes, 8); return bytes; } public static byte[] WriteKerningBlock(params byte[][] pairs) { var blockData = Concat(pairs); return Concat(WriteBlockHeader(5, blockData.Length), blockData); } public void SingleKerningPairIsAddedToKernings() { var block = WriteKerningBlock(WriteKerningPair((uint)'A', (uint)'V')); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); Check(result.Kernings.ContainsKey(((short)'A', (short)'V'))); } public void MultipleKerningPairsAreAllAdded() { var block = WriteKerningBlock( WriteKerningPair((uint)'A', (uint)'V'), WriteKerningPair((uint)'T', (uint)'o'), WriteKerningPair((uint)'W', (uint)'a') ); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); Check(result.Kernings.ContainsKey(((short)'A', (short)'V'))); Check(result.Kernings.ContainsKey(((short)'T', (short)'o'))); Check(result.Kernings.ContainsKey(((short)'W', (short)'a'))); } public void PositiveKerningAmountMapsCorrectly() { var block = WriteKerningBlock(WriteKerningPair((uint)'A', (uint)'V', amount: 3)); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); CheckEqual(3, result.Kernings[((short)'A', (short)'V')]); } public void NegativeKerningAmountRoundTripsCorrectly() { var block = WriteKerningBlock(WriteKerningPair((uint)'A', (uint)'V', amount: -10)); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); CheckEqual(-10, result.Kernings[((short)'A', (short)'V')]); } public void ZeroKerningAmountRoundTripsCorrectly() { var block = WriteKerningBlock(WriteKerningPair((uint)'A', (uint)'V', amount: 0)); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); CheckEqual(0, result.Kernings[((short)'A', (short)'V')]); } public void DuplicateKerningPairSilentlyOverwrites() { var block = WriteKerningBlock( WriteKerningPair((uint)'A', (uint)'V', amount: -3), WriteKerningPair((uint)'A', (uint)'V', amount: -7) ); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); CheckEqual(-7, result.Kernings[((short)'A', (short)'V')]); } public void KerningPairsAreStoredDirectionally() { var block = WriteKerningBlock( WriteKerningPair((uint)'A', (uint)'V', amount: -3), WriteKerningPair((uint)'V', (uint)'A', amount: -7) ); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); CheckEqual(-3, result.Kernings[((short)'A', (short)'V')]); CheckEqual(-7, result.Kernings[((short)'V', (short)'A')]); } public void FirstCharacterIdAtExactly0xFFFFSucceeds() { var block = WriteKerningBlock(WriteKerningPair(0xFFFF, (uint)'A')); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); Check(result.Kernings.ContainsKey((-1, (short)'A'))); } public void SecondCharacterIdAtExactly0xFFFFSucceeds() { var block = WriteKerningBlock(WriteKerningPair((uint)'A', 0xFFFF)); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); Check(result.Kernings.ContainsKey(((short)'A', -1))); } public void BothCharacterIdsAtExactly0xFFFFSucceeds() { var block = WriteKerningBlock(WriteKerningPair(0xFFFF, 0xFFFF)); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); Check(result.Kernings.ContainsKey((-1, -1))); } public void FirstCharacterIdOver0xFFFFThrows() { var block = WriteKerningBlock(WriteKerningPair(0x00010000, (uint)'A')); CheckThrow(typeof(InvalidDataException)); BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); } public void SecondCharacterIdOver0xFFFFThrows() { var block = WriteKerningBlock(WriteKerningPair((uint)'A', 0x00010000)); CheckThrow(typeof(InvalidDataException)); BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); } public void BothCharacterIdsOver0xFFFFThrows() { var block = WriteKerningBlock(WriteKerningPair(0x00010000, 0x00010000)); CheckThrow(typeof(InvalidDataException)); BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); } public void KerningBlockSizeNotDivisibleByPairSizeThrows() { var blockData = new byte[Marshal.SizeOf() + 1]; var block = Concat(WriteBlockHeader(5, blockData.Length), blockData); CheckThrow(typeof(InvalidDataException)); BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); } public void EmptyKerningBlockIsHandledGracefully() { var block = WriteKerningBlock(); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); CheckEqual(0, result.Kernings.Count); } public void TruncatedKerningPairThrows() { int pairSize = Marshal.SizeOf(); var blockData = WriteKerningPair((uint)'A', (uint)'V'); var block = Concat(WriteBlockHeader(5, pairSize * 2), blockData); // claims two pairs CheckThrow(typeof(InvalidDataException)); BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); } public void KerningBlockFollowedByAnotherBlockIsProcessedCorrectly() { var kerningBlock = WriteKerningBlock(WriteKerningPair((uint)'A', (uint)'V', amount: -3)); var unknownBlock = Concat(WriteBlockHeader(99, 2), new byte[] { 1, 2 }); var result = BMFontDefinition.FromBinaryFile(MakeStream( Concat(ValidHeader(), kerningBlock, unknownBlock))); CheckEqual(-3, result.Kernings[((short)'A', (short)'V')]); } public void ValidPairFollowedByInvalidPairThrows() { var block = WriteKerningBlock( WriteKerningPair((uint)'A', (uint)'V', amount: -3), WriteKerningPair(0x00010000, (uint)'A') ); CheckThrow(typeof(InvalidDataException)); BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); } public void KerningAndGlyphDataCoexistCorrectly() { var charsBlock = WriteCharsBlock(WriteCharInfo((uint)'A', xAdvance: 7)); var kerningBlock = WriteKerningBlock(WriteKerningPair((uint)'A', (uint)'V', amount: -3)); var result = BMFontDefinition.FromBinaryFile(MakeStream( Concat(ValidHeader(), charsBlock, kerningBlock))); Check(result.GlyphDefinitions.ContainsKey('A')); CheckEqual(7, result.GlyphDefinitions['A'].XAdvance); CheckEqual(-3, result.Kernings[((short)'A', (short)'V')]); } public static byte[] WritePagesBlock(params string[] pageNames) { if (pageNames.Length == 0) { return Concat(WriteBlockHeader(3, 0)); } var blockData = new List(); foreach (var name in pageNames) { var nameBytes = Encoding.UTF8.GetBytes(name); blockData.AddRange(nameBytes); blockData.Add(0); } return Concat(WriteBlockHeader(3, blockData.Count), blockData.ToArray()); } public void PagesPropertyIsNullWhenNoPagesBlockPresent() { var result = BMFontDefinition.FromBinaryFile(MakeStream(ValidHeader())); CheckNull(result.Pages); } public void SinglePageNameIsReadCorrectly() { var block = WritePagesBlock("page0.png"); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); CheckEqual(1, result.Pages.Count); CheckEqual("page0.png", result.Pages[0]); } public void MultiplePageNamesAreReadInCorrectOrder() { var block = WritePagesBlock("page0.png", "page1.png", "page2.png"); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); CheckEqual(3, result.Pages.Count); CheckEqual("page0.png", result.Pages[0]); CheckEqual("page1.png", result.Pages[1]); CheckEqual("page2.png", result.Pages[2]); } public void EmptyPagesBlockResultsInEmptyList() { var block = WritePagesBlock(); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); CheckEqual(0, result.Pages.Count); } public void EmptyPageNameIsHandledCorrectly() { var blockData = new byte[] { 0 }; var block = Concat(WriteBlockHeader(3, blockData.Length), blockData); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); CheckEqual(1, result.Pages.Count); CheckEqual("", result.Pages[0]); } public void PageNameWithNoNullTerminatorThrows() { var nameBytes = Encoding.UTF8.GetBytes("page0.png"); var block = Concat(WriteBlockHeader(3, nameBytes.Length), nameBytes); CheckThrow(typeof(InvalidDataException)); BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); } public void PageNameLongerThanMaxStringLengthThrows() { var longName = new string('A', BMFontDefinition.MaxStringLength + 1); var nameBytes = Encoding.UTF8.GetBytes(longName).Append((byte)0).ToArray(); var block = Concat(WriteBlockHeader(3, nameBytes.Length), nameBytes); CheckThrow(typeof(InvalidDataException)); BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); } public void PageNameExactlyMaxStringLengthMinusOneSucceeds() { var name = new string('A', BMFontDefinition.MaxStringLength - 1); var block = WritePagesBlock(name); var result = BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); CheckEqual(1, result.Pages.Count); CheckEqual(name, result.Pages[0]); } public void SecondPageNameShorterThanFirstThrows() { var blockData = Encoding.UTF8.GetBytes("page0.png").Append((byte)0) .Concat(Encoding.UTF8.GetBytes("page1").Append((byte)0)) .ToArray(); var block = Concat(WriteBlockHeader(3, blockData.Length), blockData); CheckThrow(typeof(InvalidDataException)); BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); } public void SecondPageNameLongerThanFirstThrows() { var blockData = Encoding.UTF8.GetBytes("page0").Append((byte)0) .Concat(Encoding.UTF8.GetBytes("page1.png").Append((byte)0)) .ToArray(); var block = Concat(WriteBlockHeader(3, blockData.Length), blockData); CheckThrow(typeof(InvalidDataException)); BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); } public void BlockSizeNotCleanMultipleOfSlotLengthThrows() { var firstName = Encoding.UTF8.GetBytes("page0.png").Append((byte)0).ToArray(); var partialSecond = Encoding.UTF8.GetBytes("page1.pn").ToArray(); // missing last char and null var blockData = Concat(firstName, partialSecond); var block = Concat(WriteBlockHeader(3, blockData.Length), blockData); CheckThrow(typeof(InvalidDataException)); BMFontDefinition.FromBinaryFile(MakeStream(Concat(ValidHeader(), block))); } public void PagesBlockFollowedByAnotherBlockIsProcessedCorrectly() { var pagesBlock = WritePagesBlock("page0.png"); var unknownBlock = Concat(WriteBlockHeader(99, 2), new byte[] { 1, 2 }); var result = BMFontDefinition.FromBinaryFile(MakeStream( Concat(ValidHeader(), pagesBlock, unknownBlock))); CheckEqual(1, result.Pages.Count); CheckEqual("page0.png", result.Pages[0]); } } } }