BinarySerialization.cs 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Text;
  5. namespace SharpGLTF.Schema2
  6. {
  7. using ROOT = ModelRoot;
  8. static class BinarySerialization
  9. {
  10. #region constants
  11. public const uint GLTFHEADER = 0x46546C67;
  12. public const uint GLTFVERSION2 = 2;
  13. public const uint CHUNKJSON = 0x4E4F534A;
  14. public const uint CHUNKBIN = 0x004E4942;
  15. #endregion
  16. #region read
  17. internal static bool _Identify(Stream stream)
  18. {
  19. Guard.NotNull(stream, nameof(stream));
  20. Guard.IsTrue(stream.CanSeek, nameof(stream), "A seekable stream is required for glTF/GLB format identification");
  21. var currPos = stream.Position;
  22. uint magic = 0;
  23. magic |= (uint)stream.ReadByte();
  24. magic |= (uint)stream.ReadByte() << 8;
  25. magic |= (uint)stream.ReadByte() << 16;
  26. magic |= (uint)stream.ReadByte() << 24;
  27. stream.Position = currPos; // restart read position
  28. return magic == GLTFHEADER;
  29. }
  30. public static IReadOnlyDictionary<UInt32, Byte[]> ReadBinaryFile(Stream stream)
  31. {
  32. Guard.NotNull(stream, nameof(stream));
  33. // WARNING: BinaryReader requires Encoding.ASCII because
  34. // the binaryReader.PeekChar() must read single bytes
  35. // in some cases, trying to read the end of the file will throw
  36. // an exception if encoding is UTF8 and there's just 1 byte left to read.
  37. using (var binaryReader = new BinaryReader(stream, Encoding.ASCII))
  38. {
  39. _ReadBinaryHeader(binaryReader);
  40. var chunks = new Dictionary<uint, Byte[]>();
  41. // keep reading until EndOfFile exception
  42. while (true)
  43. {
  44. if (binaryReader.PeekChar() < 0) break;
  45. uint chunkLength = binaryReader.ReadUInt32();
  46. if ((chunkLength & 3) != 0)
  47. {
  48. throw new InvalidDataException($"The chunk must be padded to 4 bytes: {chunkLength}");
  49. }
  50. uint chunkId = binaryReader.ReadUInt32();
  51. var data = binaryReader.ReadBytes((int)chunkLength);
  52. chunks[chunkId] = data;
  53. }
  54. return chunks;
  55. }
  56. }
  57. private static void _ReadBinaryHeader(BinaryReader binaryReader)
  58. {
  59. Guard.NotNull(binaryReader, nameof(binaryReader));
  60. uint magic = binaryReader.ReadUInt32();
  61. Guard.IsTrue(magic == GLTFHEADER, nameof(magic), $"Unexpected magic number: {magic}");
  62. uint version = binaryReader.ReadUInt32();
  63. Guard.IsTrue(version == GLTFVERSION2, nameof(version), $"Unknown version number: {version}");
  64. uint length = binaryReader.ReadUInt32();
  65. long fileLength = binaryReader.BaseStream.Length;
  66. Guard.IsTrue(length == fileLength, nameof(length), $"The specified length of the file ({length}) is not equal to the actual length of the file ({fileLength}).");
  67. }
  68. #endregion
  69. #region write
  70. /// <summary>
  71. /// Tells if a given model can be stored as Binary format.
  72. /// </summary>
  73. /// <param name="model">the model to test</param>
  74. /// <returns>null if it can be stored as binary, or an exception object if it can't</returns>
  75. /// <remarks>
  76. /// Due to the limitations of Binary Format, not all models can be saved as Binary.
  77. /// </remarks>
  78. public static Exception IsBinaryCompatible(ROOT model)
  79. {
  80. try
  81. {
  82. Guard.NotNull(model, nameof(model));
  83. Guard.IsTrue(model.LogicalBuffers.Count <= 1, nameof(model), $"GLB format only supports one binary buffer, {model.LogicalBuffers.Count} found. It can be solved by calling {nameof(ModelRoot.MergeImages)} and {nameof(ModelRoot.MergeBuffers)}");
  84. }
  85. catch (ArgumentException ex)
  86. {
  87. return ex;
  88. }
  89. // todo: buffer[0].Uri must be null
  90. return null;
  91. }
  92. /// <summary>
  93. /// Writes a <see cref="ROOT"/> instance into a <see cref="BinaryWriter"/>.
  94. /// </summary>
  95. /// <param name="binaryWriter">The destination <see cref="BinaryWriter"/> stream.</param>
  96. /// <param name="model">The source <see cref="ROOT"/> instance.</param>
  97. public static void WriteBinaryModel(this BinaryWriter binaryWriter, ROOT model)
  98. {
  99. var ex = IsBinaryCompatible(model); if (ex != null) throw ex;
  100. var jsonText = model.GetJSON(Newtonsoft.Json.Formatting.None);
  101. var jsonChunk = Encoding.UTF8.GetBytes(jsonText);
  102. var jsonPadding = jsonChunk.Length & 3; if (jsonPadding != 0) jsonPadding = 4 - jsonPadding;
  103. var buffer = model.LogicalBuffers.Count > 0 ? model.LogicalBuffers[0].Content : null;
  104. if (buffer != null && buffer.Length == 0) buffer = null;
  105. var binPadding = buffer == null ? 0 : buffer.Length & 3; if (binPadding != 0) binPadding = 4 - binPadding;
  106. int fullLength = 4 + 4 + 4;
  107. fullLength += 8 + jsonChunk.Length + jsonPadding;
  108. if (buffer != null) fullLength += 8 + buffer.Length + binPadding;
  109. binaryWriter.Write(GLTFHEADER);
  110. binaryWriter.Write(GLTFVERSION2);
  111. binaryWriter.Write(fullLength);
  112. binaryWriter.Write(jsonChunk.Length + jsonPadding);
  113. binaryWriter.Write(CHUNKJSON);
  114. binaryWriter.Write(jsonChunk);
  115. for (int i = 0; i < jsonPadding; ++i) binaryWriter.Write((Byte)0x20);
  116. if (buffer != null)
  117. {
  118. binaryWriter.Write(buffer.Length + binPadding);
  119. binaryWriter.Write(CHUNKBIN);
  120. binaryWriter.Write(buffer);
  121. for (int i = 0; i < binPadding; ++i) binaryWriter.Write((Byte)0);
  122. }
  123. }
  124. #endregion
  125. }
  126. }