Serialization.Binary.cs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Text;
  5. using MODEL = SharpGLTF.Schema2.ModelRoot;
  6. namespace SharpGLTF.Schema2
  7. {
  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. public static Memory<Byte> ReadBytesToEnd(this Stream s)
  18. {
  19. using (var m = new MemoryStream())
  20. {
  21. s.CopyTo(m);
  22. if (m.TryGetBuffer(out ArraySegment<Byte> segment)) return segment;
  23. return m.ToArray();
  24. }
  25. }
  26. internal static bool _TryReadUInt32(this System.IO.BinaryReader r, out UInt32 result)
  27. {
  28. try
  29. {
  30. result = r.ReadUInt32();
  31. return true;
  32. }
  33. catch(System.IO.EndOfStreamException)
  34. {
  35. result = 0;
  36. return false;
  37. }
  38. }
  39. internal static bool _Identify(Stream stream)
  40. {
  41. Guard.NotNull(stream, nameof(stream));
  42. Guard.IsTrue(stream.CanSeek, nameof(stream), "A seekable stream is required for glTF/GLB format identification");
  43. var currPos = stream.Position;
  44. var a = stream.ReadByte();
  45. var b = stream.ReadByte();
  46. var c = stream.ReadByte();
  47. var d = stream.ReadByte();
  48. stream.Position = currPos; // restart read position
  49. return IsBinaryHeader((Byte)a, (Byte)b, (Byte)c, (Byte)d);
  50. }
  51. internal static bool IsBinaryHeader(ReadOnlySpan<Byte> span)
  52. {
  53. if (span.Length < 4) return false;
  54. return IsBinaryHeader(span[0], span[1], span[2], span[3]);
  55. }
  56. public static bool IsBinaryHeader(Byte a, Byte b, Byte c, Byte d)
  57. {
  58. uint magic = 0;
  59. magic |= (uint)a;
  60. magic |= (uint)b << 8;
  61. magic |= (uint)c << 16;
  62. magic |= (uint)d << 24;
  63. return magic == GLTFHEADER;
  64. }
  65. public static IReadOnlyDictionary<UInt32, Byte[]> ReadBinaryFile(Stream stream)
  66. {
  67. Guard.NotNull(stream, nameof(stream));
  68. // WARNING: BinaryReader requires Encoding.ASCII because
  69. // the binaryReader.PeekChar() must read single bytes
  70. // in some cases, trying to read the end of the file will throw
  71. // an exception if encoding is UTF8 and there's just 1 byte left to read.
  72. using (var binaryReader = new BinaryReader(stream, Encoding.ASCII))
  73. {
  74. // body length can actually be smaller than the stream length,
  75. // in which case, the data afterwards is considered "extra data".
  76. var remaining = _ReadBinaryHeader(binaryReader);
  77. void _checkCanRead(long count)
  78. {
  79. if (count < 0) throw new ArgumentOutOfRangeException(nameof(count));
  80. if (count > int.MaxValue) throw new Validation.SchemaException(null, $"{count} bytes to read exceeds maximum capacity.");
  81. if (remaining < count) throw new Validation.SchemaException(null, "unexpected End of GLB block.");
  82. remaining -= count;
  83. }
  84. var chunks = new Dictionary<uint, Byte[]>();
  85. while (remaining >= 4)
  86. {
  87. remaining -= 4;
  88. if (!binaryReader._TryReadUInt32(out var chunkLength)) break;
  89. if (chunkLength == 0)
  90. {
  91. throw new Validation.SchemaException(null, $"The chunk must non zero size.");
  92. }
  93. if ((chunkLength & 3) != 0)
  94. {
  95. throw new Validation.SchemaException(null, $"The chunk must be padded to 4 bytes: {chunkLength}");
  96. }
  97. _checkCanRead(4);
  98. uint chunkId = binaryReader.ReadUInt32();
  99. if (chunks.ContainsKey(chunkId))
  100. {
  101. throw new Validation.SchemaException(null, $"Duplicated chunk found {chunkId}");
  102. }
  103. _checkCanRead(chunkLength);
  104. var data = binaryReader.ReadBytes((int)chunkLength);
  105. chunks[chunkId] = data;
  106. }
  107. // finish reading remainder
  108. // if (remaining > 0) { binaryReader.ReadBytes((int)remaining); }
  109. if (!chunks.ContainsKey(CHUNKJSON)) throw new Validation.SchemaException(null, "JSON Chunk chunk not found");
  110. // warnings
  111. // if (!chunks.ContainsKey(CHUNKBIN)) throw new Validation.SchemaException(null, "BIN Chunk chunk not found");
  112. // if (remaining > 0) throw new Validation.SchemaException(null, "Extra bytes found");
  113. return chunks;
  114. }
  115. }
  116. private static long _ReadBinaryHeader(BinaryReader binaryReader)
  117. {
  118. Guard.NotNull(binaryReader, nameof(binaryReader));
  119. uint magic = binaryReader.ReadUInt32();
  120. if (magic != GLTFHEADER) throw new Validation.SchemaException(null, $"Unexpected magic number: {magic}");
  121. uint version = binaryReader.ReadUInt32();
  122. if (version != GLTFVERSION2) throw new Validation.SchemaException(null, $"Unknown version number: {version}");
  123. uint bodyLength = binaryReader.ReadUInt32(); // length of the actual glb body
  124. try // check stream is large enough
  125. {
  126. if (binaryReader.BaseStream.CanSeek)
  127. {
  128. var fileLength = (uint)binaryReader.BaseStream.Length;
  129. if (bodyLength > fileLength)
  130. {
  131. throw new Validation.SchemaException(null, $"The specified length of the file ({bodyLength}) is not equal to the actual length of the file ({fileLength}).");
  132. }
  133. }
  134. }
  135. catch (System.NotSupportedException)
  136. {
  137. // Some streams like Android assets don't support getting the length
  138. // https://github.com/vpenades/SharpGLTF/issues/178
  139. }
  140. return bodyLength - 12;
  141. }
  142. #endregion
  143. #region write
  144. /// <summary>
  145. /// Tells if a given model can be stored as Binary format.
  146. /// </summary>
  147. /// <param name="model">the model to test</param>
  148. /// <returns>null if it can be stored as binary, or an exception object if it can't</returns>
  149. /// <remarks>
  150. /// Due to the limitations of Binary Format, not all models can be saved as Binary.
  151. /// </remarks>
  152. public static Exception IsBinaryCompatible(MODEL model)
  153. {
  154. try
  155. {
  156. Guard.NotNull(model, nameof(model));
  157. 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)}");
  158. }
  159. catch (ArgumentException ex)
  160. {
  161. return ex;
  162. }
  163. // todo: buffer[0].Uri must be null
  164. return null;
  165. }
  166. /// <summary>
  167. /// Writes a <see cref="MODEL"/> instance into a <see cref="BinaryWriter"/>.
  168. /// </summary>
  169. /// <param name="binaryWriter">The destination <see cref="BinaryWriter"/> stream.</param>
  170. /// <param name="model">The source <see cref="MODEL"/> instance.</param>
  171. public static void WriteBinaryModel(this BinaryWriter binaryWriter, MODEL model)
  172. {
  173. var ex = IsBinaryCompatible(model); if (ex != null) throw ex;
  174. var jsonText = model._GetJSON(false);
  175. var jsonChunk = Encoding.UTF8.GetBytes(jsonText);
  176. var jsonPadding = jsonChunk.Length & 3; if (jsonPadding != 0) jsonPadding = 4 - jsonPadding;
  177. var buffer = model.LogicalBuffers.Count > 0 ? model.LogicalBuffers[0].Content : null;
  178. if (buffer != null && buffer.Length == 0) buffer = null;
  179. var binPadding = buffer == null ? 0 : buffer.Length & 3; if (binPadding != 0) binPadding = 4 - binPadding;
  180. int fullLength = 4 + 4 + 4;
  181. fullLength += 8 + jsonChunk.Length + jsonPadding;
  182. if (buffer != null) fullLength += 8 + buffer.Length + binPadding;
  183. binaryWriter.Write(GLTFHEADER);
  184. binaryWriter.Write(GLTFVERSION2);
  185. binaryWriter.Write(fullLength);
  186. binaryWriter.Write(jsonChunk.Length + jsonPadding);
  187. binaryWriter.Write(CHUNKJSON);
  188. binaryWriter.Write(jsonChunk);
  189. for (int i = 0; i < jsonPadding; ++i) binaryWriter.Write((Byte)0x20);
  190. if (buffer != null)
  191. {
  192. binaryWriter.Write(buffer.Length + binPadding);
  193. binaryWriter.Write(CHUNKBIN);
  194. binaryWriter.Write(buffer);
  195. for (int i = 0; i < binPadding; ++i) binaryWriter.Write((Byte)0);
  196. }
  197. }
  198. #endregion
  199. }
  200. }