BitmapFontFileReader.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. // Copyright (c) Craftwork Games. All rights reserved.
  2. // Licensed under the MIT license.
  3. // See LICENSE file in the project root for full license information.
  4. using System;
  5. using System.Collections.Generic;
  6. using System.Globalization;
  7. using System.IO;
  8. using System.Runtime.InteropServices;
  9. using System.Text;
  10. using System.Xml;
  11. namespace MonoGame.Extended.Content.BitmapFonts;
  12. /// <summary>
  13. /// A utility class for reading the contents of a font file in the AngleCode BMFont file spec.
  14. /// </summary>
  15. public static class BitmapFontFileReader
  16. {
  17. /// <summary>
  18. /// Reads the content of the font file at the path specified.
  19. /// </summary>
  20. /// <param name="path">The path to the font file to read.</param>
  21. /// <returns>A <see cref="BitmapFontFileContent"/> instance containing the results of the read operation.</returns>
  22. /// <exception cref="InvalidOperationException">
  23. /// Thrown if the header for the file contents does not match a known header format.
  24. /// </exception>
  25. public static BitmapFontFileContent Read(string path)
  26. {
  27. using var stream = File.OpenRead(path);
  28. return Read(stream, path);
  29. }
  30. /// <summary>
  31. /// Reads the content of the font file at the path specified.
  32. /// </summary>
  33. /// <param name="stream">A <see cref="Stream"/> containing the font file contents to read.</param>
  34. /// <returns>A <see cref="BitmapFontFileContent"/> instance containing the results of the read operation.</returns>
  35. /// <exception cref="InvalidOperationException">
  36. /// Thrown if the header for the file contents does not match a known header format.
  37. /// </exception>
  38. [Obsolete("Use the overload that takes an explicit name parameter.")]
  39. public static BitmapFontFileContent Read(FileStream stream)
  40. {
  41. return Read(stream, stream.Name);
  42. }
  43. /// <summary>
  44. /// Reads the content of the font file at the path specified.
  45. /// </summary>
  46. /// <param name="stream">A <see cref="Stream"/> containing the font file contents to read.</param>
  47. /// <param name="name">The name or path that uniquely identifies this <see cref="BitmapFontFileContent"/>.</param>
  48. /// <returns>A <see cref="BitmapFontFileContent"/> instance containing the results of the read operation.</returns>
  49. /// <exception cref="InvalidOperationException">
  50. /// Thrown if the header for the file contents does not match a known header format.
  51. /// </exception>
  52. public static BitmapFontFileContent Read(Stream stream, string name)
  53. {
  54. long position = stream.Position;
  55. var sig = stream.ReadByte();
  56. stream.Position = position;
  57. var bmfFile = sig switch
  58. {
  59. // Binary header begins with [66, 77, 70, 3]
  60. 66 => ReadBinary(stream),
  61. // XML format begins with [60, 63, 120, 109]
  62. 60 => ReadXml(stream),
  63. // Text format begins with [105, 110, 102, 111]
  64. 105 => ReadText(stream),
  65. // Unknown format
  66. _ => throw new InvalidOperationException("This does not appear to be a valid BMFont file!")
  67. };
  68. bmfFile.Path = name;
  69. return bmfFile;
  70. }
  71. #region ------------------------ Read BMFFont Binary Formatted File -----------------------------------------------
  72. private static BitmapFontFileContent ReadBinary(Stream stream)
  73. {
  74. using BinaryReader reader = new BinaryReader(stream);
  75. return ReadBinary(reader);
  76. }
  77. private static BitmapFontFileContent ReadBinary(BinaryReader reader)
  78. {
  79. BitmapFontFileContent bmfFile = new BitmapFontFileContent();
  80. bmfFile.Header = AsType<BitmapFontFileContent.HeaderBlock>(reader.ReadBytes(BitmapFontFileContent.HeaderBlock.StructSize));
  81. if (!bmfFile.Header.IsValid)
  82. {
  83. throw new InvalidOperationException($"The BMFFont file header is invalid, this does not appear to be a valid BMFont file");
  84. }
  85. while (reader.BaseStream.Position < reader.BaseStream.Length)
  86. {
  87. byte blockType = reader.ReadByte();
  88. int blockSize = reader.ReadInt32();
  89. switch (blockType)
  90. {
  91. case 1:
  92. bmfFile.Info = AsType<BitmapFontFileContent.InfoBlock>(reader.ReadBytes(BitmapFontFileContent.InfoBlock.StructSize));
  93. int stringLen = blockSize - BitmapFontFileContent.InfoBlock.StructSize;
  94. bmfFile.FontName = Encoding.UTF8.GetString(reader.ReadBytes(stringLen)).Replace("\0", string.Empty);
  95. break;
  96. case 2:
  97. bmfFile.Common = AsType<BitmapFontFileContent.CommonBlock>(reader.ReadBytes(BitmapFontFileContent.CommonBlock.StructSize));
  98. break;
  99. case 3:
  100. string[] pages = Encoding.UTF8.GetString(reader.ReadBytes(blockSize)).Split('\0', StringSplitOptions.RemoveEmptyEntries);
  101. bmfFile.Pages.AddRange(pages);
  102. break;
  103. case 4:
  104. int characterCount = blockSize / BitmapFontFileContent.CharacterBlock.StructSize;
  105. for (int c = 0; c < characterCount; c++)
  106. {
  107. BitmapFontFileContent.CharacterBlock character = AsType<BitmapFontFileContent.CharacterBlock>(reader.ReadBytes(BitmapFontFileContent.CharacterBlock.StructSize));
  108. bmfFile.Characters.Add(character);
  109. }
  110. break;
  111. case 5:
  112. int kerningCount = blockSize / BitmapFontFileContent.KerningPairsBlock.StructSize;
  113. for (int k = 0; k < kerningCount; k++)
  114. {
  115. BitmapFontFileContent.KerningPairsBlock kerning = AsType<BitmapFontFileContent.KerningPairsBlock>(reader.ReadBytes(BitmapFontFileContent.KerningPairsBlock.StructSize));
  116. bmfFile.Kernings.Add(kerning);
  117. }
  118. break;
  119. default:
  120. reader.BaseStream.Seek(blockSize, SeekOrigin.Current);
  121. break;
  122. }
  123. }
  124. return bmfFile;
  125. }
  126. private static T AsType<T>(ReadOnlySpan<byte> buffer) where T : struct
  127. {
  128. T value;
  129. try
  130. {
  131. unsafe
  132. {
  133. fixed (byte* ptr = buffer)
  134. {
  135. value = Marshal.PtrToStructure<T>((IntPtr)ptr);
  136. }
  137. }
  138. return value;
  139. }
  140. catch
  141. {
  142. return default;
  143. }
  144. }
  145. #endregion --------------------- Read BMFFont Binary Formatted File -----------------------------------------------
  146. #region ------------------------ Read BMFFont Xml Formatted File --------------------------------------------------
  147. private static BitmapFontFileContent ReadXml(Stream stream)
  148. {
  149. BitmapFontFileContent bmfFile = new BitmapFontFileContent();
  150. // XML does not contain the header like binary so we manually create it
  151. bmfFile.Header = new() { B = (byte)'B', M = (byte)'M', F = (byte)'F', Version = 3 };
  152. var document = new XmlDocument();
  153. document.Load(stream);
  154. var root = document.DocumentElement;
  155. ReadInfoNode(bmfFile, root);
  156. ReadCommonNode(bmfFile, root);
  157. ReadPageNodes(bmfFile, root);
  158. ReadCharacterNodes(bmfFile, root);
  159. ReadKerningNodes(bmfFile, root);
  160. return bmfFile;
  161. }
  162. private static void ReadInfoNode(BitmapFontFileContent bmfFile, XmlNode root)
  163. {
  164. var node = root.SelectSingleNode("info");
  165. bmfFile.FontName = node.GetStringAttribute("face");
  166. bmfFile.Info.FontSize = node.GetInt16Attribute("size");
  167. var smooth = node.GetByteAttribute("smooth");
  168. var unicode = node.GetByteAttribute("unicode");
  169. var italic = node.GetByteAttribute("italic");
  170. var bold = node.GetByteAttribute("bold");
  171. var fixedHeight = node.GetByteAttribute("fixedHeight");
  172. bmfFile.Info.BitField = (byte)(smooth << 7 | unicode << 6 | italic << 5 | bold << 4 | fixedHeight << 3);
  173. bmfFile.Info.CharSet = node.GetByteAttribute("charSet");
  174. bmfFile.Info.StretchH = node.GetUInt16Attribute("stretchH");
  175. bmfFile.Info.AA = node.GetByteAttribute("aa");
  176. var paddingValues = node.GetByteDelimitedAttribute("padding", 4);
  177. bmfFile.Info.PaddingUp = paddingValues[0];
  178. bmfFile.Info.PaddingRight = paddingValues[1];
  179. bmfFile.Info.PaddingDown = paddingValues[2];
  180. bmfFile.Info.PaddingLeft = paddingValues[3];
  181. var spacingValues = node.GetByteDelimitedAttribute("spacing", 2);
  182. bmfFile.Info.SpacingHoriz = spacingValues[0];
  183. bmfFile.Info.SpacingVert = spacingValues[1];
  184. bmfFile.Info.Outline = node.GetByteAttribute("outline");
  185. }
  186. private static void ReadCommonNode(BitmapFontFileContent bmfFile, XmlNode root)
  187. {
  188. var node = root.SelectSingleNode("common");
  189. bmfFile.Common.LineHeight = node.GetUInt16Attribute("lineHeight");
  190. bmfFile.Common.Base = node.GetUInt16Attribute("base");
  191. bmfFile.Common.ScaleW = node.GetUInt16Attribute("scaleW");
  192. bmfFile.Common.ScaleH = node.GetUInt16Attribute("scaleH");
  193. bmfFile.Common.Pages = node.GetUInt16Attribute("pages");
  194. var packed = node.GetByteAttribute("packed");
  195. bmfFile.Common.BitField = (byte)(packed << 7);
  196. bmfFile.Common.AlphaChnl = node.GetByteAttribute("alphaChnl");
  197. bmfFile.Common.RedChnl = node.GetByteAttribute("redChnl");
  198. bmfFile.Common.GreenChnl = node.GetByteAttribute("greenChnl");
  199. bmfFile.Common.BlueChnl = node.GetByteAttribute("blueChnl");
  200. }
  201. private static void ReadPageNodes(BitmapFontFileContent bmfFile, XmlNode root)
  202. {
  203. var nodes = root.SelectNodes("pages/page");
  204. foreach (XmlNode node in nodes)
  205. {
  206. string file = node.GetStringAttribute("file");
  207. bmfFile.Pages.Add(file);
  208. }
  209. }
  210. private static void ReadCharacterNodes(BitmapFontFileContent bmfFile, XmlNode root)
  211. {
  212. var nodes = root.SelectNodes("chars/char");
  213. foreach (XmlNode node in nodes)
  214. {
  215. var character = new BitmapFontFileContent.CharacterBlock
  216. {
  217. ID = node.GetUInt32Attribute("id"),
  218. X = node.GetUInt16Attribute("x"),
  219. Y = node.GetUInt16Attribute("y"),
  220. Width = node.GetUInt16Attribute("width"),
  221. Height = node.GetUInt16Attribute("height"),
  222. XOffset = node.GetInt16Attribute("xoffset"),
  223. YOffset = node.GetInt16Attribute("yoffset"),
  224. XAdvance = node.GetInt16Attribute("xadvance"),
  225. Page = node.GetByteAttribute("page"),
  226. Chnl = node.GetByteAttribute("chnl"),
  227. };
  228. bmfFile.Characters.Add(character);
  229. }
  230. }
  231. private static void ReadKerningNodes(BitmapFontFileContent bmfFile, XmlNode root)
  232. {
  233. var nodes = root.SelectNodes("kernings/kerning");
  234. foreach (XmlNode node in nodes)
  235. {
  236. var kerning = new BitmapFontFileContent.KerningPairsBlock
  237. {
  238. First = node.GetUInt32Attribute("first"),
  239. Second = node.GetUInt32Attribute("second"),
  240. Amount = node.GetInt16Attribute("amount"),
  241. };
  242. bmfFile.Kernings.Add(kerning);
  243. }
  244. }
  245. #endregion --------------------- Read BMFFont Xml Formatted File --------------------------------------------------
  246. #region ------------------------ Read BMFFont Text Formatted File -------------------------------------------------
  247. private static BitmapFontFileContent ReadText(Stream stream)
  248. {
  249. var bmfFile = new BitmapFontFileContent();
  250. // Text does not contain the header like binary so we manually create it
  251. bmfFile.Header = new() { B = (byte)'B', M = (byte)'M', F = (byte)'F', Version = 3 };
  252. using var reader = new StreamReader(stream);
  253. string line = default;
  254. while ((line = reader.ReadLine()) != null)
  255. {
  256. var tokens = GetTokens(line);
  257. if (tokens.Count == 0)
  258. {
  259. continue;
  260. }
  261. switch (tokens[0])
  262. {
  263. case "info":
  264. ReadInfoTokens(bmfFile, CollectionsMarshal.AsSpan(tokens)[1..]);
  265. break;
  266. case "common":
  267. ReadCommonTokens(bmfFile, CollectionsMarshal.AsSpan(tokens)[1..]);
  268. break;
  269. case "page":
  270. ReadPageTokens(bmfFile, CollectionsMarshal.AsSpan(tokens)[1..]);
  271. break;
  272. case "char":
  273. ReadCharacterTokens(bmfFile, CollectionsMarshal.AsSpan(tokens)[1..]);
  274. break;
  275. case "kerning":
  276. ReadKerningTokens(bmfFile, CollectionsMarshal.AsSpan(tokens)[1..]);
  277. break;
  278. }
  279. }
  280. return bmfFile;
  281. }
  282. private static void ReadInfoTokens(BitmapFontFileContent bmfFile, ReadOnlySpan<string> tokens)
  283. {
  284. for (int i = 0; i < tokens.Length; ++i)
  285. {
  286. var split = tokens[i].Split('=');
  287. if (split.Length != 2)
  288. {
  289. continue;
  290. }
  291. switch (split[0])
  292. {
  293. case "face":
  294. bmfFile.FontName = split[1].Replace("\"", string.Empty);
  295. break;
  296. case "size":
  297. bmfFile.Info.FontSize = Convert.ToInt16(split[1], CultureInfo.InvariantCulture);
  298. break;
  299. case "smooth":
  300. var smooth = Convert.ToByte(split[1], CultureInfo.InvariantCulture);
  301. bmfFile.Info.BitField |= (byte)(smooth << 7);
  302. break;
  303. case "unicode":
  304. var unicode = Convert.ToByte(split[1], CultureInfo.InvariantCulture);
  305. bmfFile.Info.BitField |= (byte)(unicode << 6);
  306. break;
  307. case "italic":
  308. var italic = Convert.ToByte(split[1], CultureInfo.InvariantCulture);
  309. bmfFile.Info.BitField |= (byte)(italic << 5);
  310. break;
  311. case "bold":
  312. var bold = Convert.ToByte(split[1], CultureInfo.InvariantCulture);
  313. bmfFile.Info.BitField |= (byte)(bold << 4);
  314. break;
  315. case "fixedHeight":
  316. byte fixedHeight = Convert.ToByte(split[1], CultureInfo.InvariantCulture);
  317. bmfFile.Info.BitField |= (byte)(fixedHeight << 3);
  318. break;
  319. case "stretchH":
  320. bmfFile.Info.StretchH = Convert.ToUInt16(split[1], CultureInfo.InvariantCulture);
  321. break;
  322. case "aa":
  323. bmfFile.Info.AA = Convert.ToByte(split[1], CultureInfo.InvariantCulture);
  324. break;
  325. case "padding":
  326. var paddingValues = split[1].Split(',');
  327. if (paddingValues.Length == 4)
  328. {
  329. bmfFile.Info.PaddingUp = Convert.ToByte(paddingValues[0], CultureInfo.InvariantCulture);
  330. bmfFile.Info.PaddingRight = Convert.ToByte(paddingValues[1], CultureInfo.InvariantCulture);
  331. bmfFile.Info.PaddingDown = Convert.ToByte(paddingValues[2], CultureInfo.InvariantCulture);
  332. bmfFile.Info.PaddingLeft = Convert.ToByte(paddingValues[3], CultureInfo.InvariantCulture);
  333. }
  334. break;
  335. case "spacing":
  336. var spacingValues = split[1].Split(',');
  337. if (spacingValues.Length == 2)
  338. {
  339. bmfFile.Info.SpacingHoriz = Convert.ToByte(spacingValues[0], CultureInfo.InvariantCulture);
  340. bmfFile.Info.SpacingVert = Convert.ToByte(spacingValues[1], CultureInfo.InvariantCulture);
  341. }
  342. break;
  343. case "outline":
  344. bmfFile.Info.Outline = Convert.ToByte(split[1], CultureInfo.InvariantCulture);
  345. break;
  346. }
  347. }
  348. }
  349. private static void ReadCommonTokens(BitmapFontFileContent bmfFile, ReadOnlySpan<string> tokens)
  350. {
  351. for (int i = 0; i < tokens.Length; ++i)
  352. {
  353. var split = tokens[i].Split('=');
  354. if (split.Length != 2)
  355. {
  356. continue;
  357. }
  358. switch (split[0])
  359. {
  360. case "lineHeight":
  361. bmfFile.Common.LineHeight = Convert.ToUInt16(split[1], CultureInfo.InvariantCulture);
  362. break;
  363. case "base":
  364. bmfFile.Common.Base = Convert.ToUInt16(split[1], CultureInfo.InvariantCulture);
  365. break;
  366. case "scaleW":
  367. bmfFile.Common.ScaleW = Convert.ToUInt16(split[1], CultureInfo.InvariantCulture);
  368. break;
  369. case "scaleH":
  370. bmfFile.Common.ScaleH = Convert.ToUInt16(split[1], CultureInfo.InvariantCulture);
  371. break;
  372. case "pages":
  373. bmfFile.Common.Pages = Convert.ToUInt16(split[1], CultureInfo.InvariantCulture);
  374. break;
  375. case "packed":
  376. var packed = Convert.ToByte(split[1], CultureInfo.InvariantCulture);
  377. bmfFile.Common.BitField |= (byte)(packed << 7);
  378. break;
  379. case "alphaChnl":
  380. bmfFile.Common.AlphaChnl = Convert.ToByte(split[1], CultureInfo.InvariantCulture);
  381. break;
  382. case "redChnl":
  383. bmfFile.Common.RedChnl = Convert.ToByte(split[1], CultureInfo.InvariantCulture);
  384. break;
  385. case "greenChnl":
  386. bmfFile.Common.GreenChnl = Convert.ToByte(split[1], CultureInfo.InvariantCulture);
  387. break;
  388. case "blueChnl":
  389. bmfFile.Common.BlueChnl = Convert.ToByte(split[1], CultureInfo.InvariantCulture);
  390. break;
  391. }
  392. }
  393. }
  394. private static void ReadPageTokens(BitmapFontFileContent bmfFile, ReadOnlySpan<string> tokens)
  395. {
  396. for (var i = 0; i < tokens.Length; ++i)
  397. {
  398. var split = tokens[i].Split('=');
  399. if (split.Length != 2)
  400. {
  401. continue;
  402. }
  403. if (split[0] == "file")
  404. {
  405. var page = split[1].Replace("\"", string.Empty);
  406. bmfFile.Pages.Add(page);
  407. }
  408. }
  409. }
  410. private static void ReadCharacterTokens(BitmapFontFileContent bmfFile, ReadOnlySpan<string> tokens)
  411. {
  412. var character = default(BitmapFontFileContent.CharacterBlock);
  413. for (var i = 0; i < tokens.Length; ++i)
  414. {
  415. var split = tokens[i].Split('=');
  416. if (split.Length != 2)
  417. {
  418. continue;
  419. }
  420. switch (split[0])
  421. {
  422. case "id":
  423. character.ID = Convert.ToUInt32(split[1], CultureInfo.InvariantCulture);
  424. break;
  425. case "x":
  426. character.X = Convert.ToUInt16(split[1], CultureInfo.InvariantCulture);
  427. break;
  428. case "y":
  429. character.Y = Convert.ToUInt16(split[1], CultureInfo.InvariantCulture);
  430. break;
  431. case "width":
  432. character.Width = Convert.ToUInt16(split[1], CultureInfo.InvariantCulture);
  433. break;
  434. case "height":
  435. character.Height = Convert.ToUInt16(split[1], CultureInfo.InvariantCulture);
  436. break;
  437. case "xoffset":
  438. character.XOffset = Convert.ToInt16(split[1], CultureInfo.InvariantCulture);
  439. break;
  440. case "yoffset":
  441. character.YOffset = Convert.ToInt16(split[1], CultureInfo.InvariantCulture);
  442. break;
  443. case "xadvance":
  444. character.XAdvance = Convert.ToInt16(split[1], CultureInfo.InvariantCulture);
  445. break;
  446. case "page":
  447. character.Page = Convert.ToByte(split[1], CultureInfo.InvariantCulture);
  448. break;
  449. case "chnl":
  450. character.Chnl = Convert.ToByte(split[1], CultureInfo.InvariantCulture);
  451. break;
  452. }
  453. }
  454. bmfFile.Characters.Add(character);
  455. }
  456. private static void ReadKerningTokens(BitmapFontFileContent bmfFile, ReadOnlySpan<string> tokens)
  457. {
  458. var kerning = default(BitmapFontFileContent.KerningPairsBlock);
  459. for (var i = 0; i < tokens.Length; ++i)
  460. {
  461. var split = tokens[i].Split('=');
  462. if (split.Length != 2)
  463. {
  464. continue;
  465. }
  466. switch (split[0])
  467. {
  468. case "first":
  469. kerning.First = Convert.ToUInt32(split[1], CultureInfo.InvariantCulture);
  470. break;
  471. case "second":
  472. kerning.Second = Convert.ToUInt32(split[1], CultureInfo.InvariantCulture);
  473. break;
  474. case "amount":
  475. kerning.Amount = Convert.ToInt16(split[1], CultureInfo.InvariantCulture);
  476. break;
  477. }
  478. }
  479. bmfFile.Kernings.Add(kerning);
  480. }
  481. private static List<string> GetTokens(ReadOnlySpan<char> line)
  482. {
  483. var tokens = new List<string>();
  484. var currentToken = new StringBuilder();
  485. var inQuotes = false;
  486. for (int i = 0; i < line.Length; i++)
  487. {
  488. var c = line[i];
  489. if (c == ' ' && !inQuotes)
  490. {
  491. if (currentToken.Length > 0)
  492. {
  493. tokens.Add(currentToken.ToString());
  494. currentToken.Clear();
  495. }
  496. }
  497. else if (c == '"')
  498. {
  499. inQuotes = !inQuotes;
  500. }
  501. else
  502. {
  503. currentToken.Append(c);
  504. }
  505. }
  506. if (currentToken.Length > 0)
  507. {
  508. tokens.Add(currentToken.ToString());
  509. }
  510. return tokens;
  511. }
  512. #endregion --------------------- Read BMFFont Text Formatted File -------------------------------------------------
  513. }