| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Text;
- using MODEL = SharpGLTF.Schema2.ModelRoot;
- namespace SharpGLTF.Schema2
- {
- static class _BinarySerialization
- {
- #region constants
- public const uint GLTFHEADER = 0x46546C67;
- public const uint GLTFVERSION2 = 2;
- public const uint CHUNKJSON = 0x4E4F534A;
- public const uint CHUNKBIN = 0x004E4942;
- #endregion
- #region read
- public static Memory<Byte> ReadBytesToEnd(this Stream s)
- {
- using (var m = new MemoryStream())
- {
- s.CopyTo(m);
- if (m.TryGetBuffer(out ArraySegment<Byte> segment)) return segment;
- return m.ToArray();
- }
- }
- internal static bool _TryReadUInt32(this System.IO.BinaryReader r, out UInt32 result)
- {
- try
- {
- result = r.ReadUInt32();
- return true;
- }
- catch(System.IO.EndOfStreamException)
- {
- result = 0;
- return false;
- }
- }
- internal static bool _Identify(Stream stream)
- {
- Guard.NotNull(stream, nameof(stream));
- Guard.IsTrue(stream.CanSeek, nameof(stream), "A seekable stream is required for glTF/GLB format identification");
- var currPos = stream.Position;
- var a = stream.ReadByte();
- var b = stream.ReadByte();
- var c = stream.ReadByte();
- var d = stream.ReadByte();
- stream.Position = currPos; // restart read position
- return IsBinaryHeader((Byte)a, (Byte)b, (Byte)c, (Byte)d);
- }
- internal static bool IsBinaryHeader(ReadOnlySpan<Byte> span)
- {
- if (span.Length < 4) return false;
- return IsBinaryHeader(span[0], span[1], span[2], span[3]);
- }
- public static bool IsBinaryHeader(Byte a, Byte b, Byte c, Byte d)
- {
- uint magic = 0;
- magic |= (uint)a;
- magic |= (uint)b << 8;
- magic |= (uint)c << 16;
- magic |= (uint)d << 24;
- return magic == GLTFHEADER;
- }
- public static IReadOnlyDictionary<UInt32, Byte[]> ReadBinaryFile(Stream stream)
- {
- Guard.NotNull(stream, nameof(stream));
- // WARNING: BinaryReader requires Encoding.ASCII because
- // the binaryReader.PeekChar() must read single bytes
- // in some cases, trying to read the end of the file will throw
- // an exception if encoding is UTF8 and there's just 1 byte left to read.
- using (var binaryReader = new BinaryReader(stream, Encoding.ASCII))
- {
- // body length can actually be smaller than the stream length,
- // in which case, the data afterwards is considered "extra data".
- var remaining = _ReadBinaryHeader(binaryReader);
-
- void _checkCanRead(long count)
- {
- if (count < 0) throw new ArgumentOutOfRangeException(nameof(count));
- if (count > int.MaxValue) throw new Validation.SchemaException(null, $"{count} bytes to read exceeds maximum capacity.");
- if (remaining < count) throw new Validation.SchemaException(null, "unexpected End of GLB block.");
- remaining -= count;
- }
- var chunks = new Dictionary<uint, Byte[]>();
- while (remaining >= 4)
- {
- remaining -= 4;
- if (!binaryReader._TryReadUInt32(out var chunkLength)) break;
- if (chunkLength == 0)
- {
- throw new Validation.SchemaException(null, $"The chunk must non zero size.");
- }
- if ((chunkLength & 3) != 0)
- {
- throw new Validation.SchemaException(null, $"The chunk must be padded to 4 bytes: {chunkLength}");
- }
- _checkCanRead(4);
- uint chunkId = binaryReader.ReadUInt32();
- if (chunks.ContainsKey(chunkId))
- {
- throw new Validation.SchemaException(null, $"Duplicated chunk found {chunkId}");
- }
-
- _checkCanRead(chunkLength);
- var data = binaryReader.ReadBytes((int)chunkLength);
- chunks[chunkId] = data;
- }
- // finish reading remainder
- // if (remaining > 0) { binaryReader.ReadBytes((int)remaining); }
- if (!chunks.ContainsKey(CHUNKJSON)) throw new Validation.SchemaException(null, "JSON Chunk chunk not found");
- // warnings
- // if (!chunks.ContainsKey(CHUNKBIN)) throw new Validation.SchemaException(null, "BIN Chunk chunk not found");
- // if (remaining > 0) throw new Validation.SchemaException(null, "Extra bytes found");
- return chunks;
- }
- }
- private static long _ReadBinaryHeader(BinaryReader binaryReader)
- {
- Guard.NotNull(binaryReader, nameof(binaryReader));
- uint magic = binaryReader.ReadUInt32();
- if (magic != GLTFHEADER) throw new Validation.SchemaException(null, $"Unexpected magic number: {magic}");
- uint version = binaryReader.ReadUInt32();
- if (version != GLTFVERSION2) throw new Validation.SchemaException(null, $"Unknown version number: {version}");
- uint bodyLength = binaryReader.ReadUInt32(); // length of the actual glb body
- try // check stream is large enough
- {
- if (binaryReader.BaseStream.CanSeek)
- {
- var fileLength = (uint)binaryReader.BaseStream.Length;
- if (bodyLength > fileLength)
- {
- throw new Validation.SchemaException(null, $"The specified length of the file ({bodyLength}) is not equal to the actual length of the file ({fileLength}).");
- }
- }
- }
- catch (System.NotSupportedException)
- {
- // Some streams like Android assets don't support getting the length
- // https://github.com/vpenades/SharpGLTF/issues/178
- }
- return bodyLength - 12;
- }
- #endregion
- #region write
- /// <summary>
- /// Tells if a given model can be stored as Binary format.
- /// </summary>
- /// <param name="model">the model to test</param>
- /// <returns>null if it can be stored as binary, or an exception object if it can't</returns>
- /// <remarks>
- /// Due to the limitations of Binary Format, not all models can be saved as Binary.
- /// </remarks>
- public static Exception IsBinaryCompatible(MODEL model)
- {
- try
- {
- Guard.NotNull(model, nameof(model));
- 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)}");
- }
- catch (ArgumentException ex)
- {
- return ex;
- }
- // todo: buffer[0].Uri must be null
- return null;
- }
- /// <summary>
- /// Writes a <see cref="MODEL"/> instance into a <see cref="BinaryWriter"/>.
- /// </summary>
- /// <param name="binaryWriter">The destination <see cref="BinaryWriter"/> stream.</param>
- /// <param name="model">The source <see cref="MODEL"/> instance.</param>
- public static void WriteBinaryModel(this BinaryWriter binaryWriter, MODEL model)
- {
- var ex = IsBinaryCompatible(model); if (ex != null) throw ex;
- var jsonText = model._GetJSON(false);
- var jsonChunk = Encoding.UTF8.GetBytes(jsonText);
- var jsonPadding = jsonChunk.Length & 3; if (jsonPadding != 0) jsonPadding = 4 - jsonPadding;
- var buffer = model.LogicalBuffers.Count > 0 ? model.LogicalBuffers[0].Content : null;
- if (buffer != null && buffer.Length == 0) buffer = null;
- var binPadding = buffer == null ? 0 : buffer.Length & 3; if (binPadding != 0) binPadding = 4 - binPadding;
- int fullLength = 4 + 4 + 4;
- fullLength += 8 + jsonChunk.Length + jsonPadding;
- if (buffer != null) fullLength += 8 + buffer.Length + binPadding;
- binaryWriter.Write(GLTFHEADER);
- binaryWriter.Write(GLTFVERSION2);
- binaryWriter.Write(fullLength);
- binaryWriter.Write(jsonChunk.Length + jsonPadding);
- binaryWriter.Write(CHUNKJSON);
- binaryWriter.Write(jsonChunk);
- for (int i = 0; i < jsonPadding; ++i) binaryWriter.Write((Byte)0x20);
- if (buffer != null)
- {
- binaryWriter.Write(buffer.Length + binPadding);
- binaryWriter.Write(CHUNKBIN);
- binaryWriter.Write(buffer);
- for (int i = 0; i < binPadding; ++i) binaryWriter.Write((Byte)0);
- }
- }
- #endregion
- }
- }
|