2
0
Эх сурвалжийг харах

Improving embedded images support

Vicente Penades 6 жил өмнө
parent
commit
a38dd09132

+ 1 - 0
src/SharpGLTF.Toolkit/Geometry/InterleavedMeshBuilder.cs

@@ -104,6 +104,7 @@ namespace SharpGLTF.Geometry
                 prim.Material = materialEvaluator(kvp.Key);
             }
 
+            root.MergeImages();
             root.MergeBuffers();
         }
 

+ 78 - 33
src/SharpGLTF/Schema2/glb.Images.cs

@@ -32,10 +32,10 @@ namespace SharpGLTF.Schema2
         /// </summary>
         /// <remarks>
         /// When a model is loaded, the image file is loaded into memory and assigned to this
-        /// field, and the <see cref="Image._uri"/> is nullified.
-        /// When writing a gltf file with external images, the <see cref="Image._uri"/> is
-        /// briefly reassigned so the JSON can be serialized correctly.
-        /// After serialization <see cref="Image._uri"/> is set back to null.
+        /// field, and the <see cref="Image._uri"/> and <see cref="Image._mimeType"/> fields are nullified.
+        /// When writing a gltf file with external images, the <see cref="Image._uri"/> and <see cref="Image._mimeType"/>
+        /// fields are briefly reassigned so the JSON can be serialized correctly.
+        /// After serialization <see cref="Image._uri"/> and <see cref="Image._mimeType"/> fields are set back to null.
         /// </remarks>
         private Byte[] _SatelliteImageContent;
 
@@ -56,12 +56,12 @@ namespace SharpGLTF.Schema2
         /// <summary>
         /// Gets a value indicating whether the contained image is a PNG image.
         /// </summary>
-        public bool IsPng => string.IsNullOrWhiteSpace(_mimeType) ? false : _mimeType.Contains("png");
+        public bool IsPng => _IsPng(GetImageContent());
 
         /// <summary>
         /// Gets a value indicating whether the contained image is a JPEG image.
         /// </summary>
-        public bool IsJpeg => string.IsNullOrWhiteSpace(_mimeType) ? false : _mimeType.Contains("jpg") | _mimeType.Contains("jpeg");
+        public bool IsJpeg => _IsJpeg(GetImageContent());
 
         #endregion
 
@@ -134,9 +134,10 @@ namespace SharpGLTF.Schema2
             if (_IsPng(content)) imageType = MIMEPNG; // these strings might be wrong
             if (_IsJpeg(content)) imageType = MIMEJPEG; // these strings might be wrong
 
-            this._mimeType = imageType ?? throw new ArgumentException($"{nameof(content)} must be a PNG or JPG image", nameof(content));
+            if (imageType == null) throw new ArgumentException($"{nameof(content)} must be a PNG or JPG image", nameof(content));
 
             this._uri = null;
+            this._mimeType = null;
             this._bufferView = null;
             this._SatelliteImageContent = content;
 
@@ -157,6 +158,7 @@ namespace SharpGLTF.Schema2
                 .LogicalIndex;
 
             this._uri = null;
+            this._mimeType = null;
             this._SatelliteImageContent = null;
         }
 
@@ -170,6 +172,7 @@ namespace SharpGLTF.Schema2
             {
                 _SatelliteImageContent = _LoadImageUnchecked(externalReferenceSolver, _uri);
                 _uri = null;
+                _mimeType = null;
             }
         }
 
@@ -187,30 +190,29 @@ namespace SharpGLTF.Schema2
         #region binary write
 
         /// <summary>
-        /// Called internally by the serializer when the buffer content is to be written internally.
+        /// Called internally by the serializer when the image content is to be embedded into the JSON document.
         /// </summary>
         internal void _WriteToInternal()
         {
-            if (_SatelliteImageContent != null)
+            if (_SatelliteImageContent == null) { _WriteAsBufferView(); return; }
+
+            var mimeContent = Convert.ToBase64String(_SatelliteImageContent, Base64FormattingOptions.None);
+
+            if (_IsPng(_SatelliteImageContent))
             {
-                var mimeContent = Convert.ToBase64String(_SatelliteImageContent, Base64FormattingOptions.None);
-
-                if (_IsPng(_SatelliteImageContent))
-                {
-                    _mimeType = MIMEPNG;
-                    _uri = EMBEDDEDPNGBUFFER + mimeContent;
-                    return;
-                }
-
-                if (_IsJpeg(_SatelliteImageContent))
-                {
-                    _mimeType = MIMEJPEG;
-                    _uri = EMBEDDEDJPEGBUFFER + mimeContent;
-                    return;
-                }
-
-                throw new NotImplementedException();
+                _mimeType = MIMEPNG;
+                _uri = EMBEDDEDPNGBUFFER + mimeContent;
+                return;
             }
+
+            if (_IsJpeg(_SatelliteImageContent))
+            {
+                _mimeType = MIMEJPEG;
+                _uri = EMBEDDEDJPEGBUFFER + mimeContent;
+                return;
+            }
+
+            throw new NotImplementedException();
         }
 
         /// <summary>
@@ -220,15 +222,36 @@ namespace SharpGLTF.Schema2
         /// <param name="satelliteUri">A local satellite URI</param>
         internal void _WriteToSatellite(AssetWriter writer, string satelliteUri)
         {
-            if (_SatelliteImageContent != null)
+            if (_SatelliteImageContent == null) { _WriteAsBufferView(); return; }
+
+            if (_IsPng(_SatelliteImageContent))
             {
-                if (this._mimeType.Contains("png")) satelliteUri += ".png";
-                if (this._mimeType.Contains("jpg")) satelliteUri += ".jpg";
-                if (this._mimeType.Contains("jpeg")) satelliteUri += ".jpg";
+                _mimeType = null;
+                _uri = satelliteUri += ".png";
+                writer(_uri, _SatelliteImageContent);
+                return;
+            }
 
-                this._uri = satelliteUri;
-                writer(satelliteUri, _SatelliteImageContent);
+            if (_IsJpeg(_SatelliteImageContent))
+            {
+                _mimeType = null;
+                _uri = satelliteUri += ".jpg";
+                writer(_uri, _SatelliteImageContent);
+                return;
             }
+
+            throw new NotImplementedException();
+
+        }
+
+        private void _WriteAsBufferView()
+        {
+            Guard.IsTrue(this._bufferView.HasValue, nameof(this._bufferView));
+
+            if (_IsPng(GetImageContent())) { _mimeType = MIMEPNG; return; }
+            if (_IsJpeg(GetImageContent())) { _mimeType = MIMEJPEG; return; }
+
+            throw new NotImplementedException();
         }
 
         /// <summary>
@@ -236,7 +259,11 @@ namespace SharpGLTF.Schema2
         /// calling <see cref="_WriteToSatellite(AssetWriter, string)"/>
         /// or <see cref="_WriteToInternal"/>
         /// </summary>
-        internal void _ClearAfterWrite() { this._uri = null; }
+        internal void _ClearAfterWrite()
+        {
+            this._mimeType = null;
+            this._uri = null;
+        }
 
         #endregion
     }
@@ -258,5 +285,23 @@ namespace SharpGLTF.Schema2
 
             return image;
         }
+
+        /// <summary>
+        /// Transfers all the <see cref="ModelRoot.LogicalImages"/> content into <see cref="BufferView"/> instances
+        /// </summary>
+        /// <remarks>
+        /// Images can be stored in three different ways:
+        /// - As satellite files.
+        /// - Embedded as MIME64 into the JSON document
+        /// - Referenced with <see cref="BufferView"/>
+        ///
+        /// This call ensures all images will be internalized as <see cref="BufferView"/> instances.
+        ///
+        /// This action cannot be reversed.
+        /// </remarks>
+        public void MergeImages()
+        {
+            foreach (var img in this._images) img.TransferToInternalBuffer();
+        }
     }
 }

+ 2 - 2
src/SharpGLTF/Schema2/glb.cs

@@ -111,7 +111,7 @@ namespace SharpGLTF.Schema2
             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.MergeBuffers)}");
+                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 (Exception ex)
             {
@@ -132,7 +132,7 @@ namespace SharpGLTF.Schema2
         {
             var ex = IsBinaryCompatible(model); if (ex != null) throw ex;
 
-            var jsonText = model.GetJSON(Newtonsoft.Json.Formatting.None);
+            var jsonText = model.WriteJSON(Newtonsoft.Json.Formatting.None);
             var jsonChunk = Encoding.UTF8.GetBytes(jsonText);
             var jsonPadding = jsonChunk.Length & 3; if (jsonPadding != 0) jsonPadding = 4 - jsonPadding;
 

+ 4 - 0
src/SharpGLTF/Schema2/gltf.Buffer.cs

@@ -157,6 +157,10 @@ namespace SharpGLTF.Schema2
         /// </summary>
         /// <remarks>
         /// When merging the buffers, it also adjusts the BufferView offsets so the data they point to remains the same.
+        ///
+        /// If images are required to be included in the binary, call <see cref="ModelRoot.MergeImages"/> before calling <see cref="MergeBuffers"/>
+        ///
+        /// This action cannot be reversed.
         /// </remarks>
         public void MergeBuffers()
         {

+ 3 - 3
src/SharpGLTF/Schema2/gltf.Root.cs

@@ -49,12 +49,12 @@ namespace SharpGLTF.Schema2
         /// <returns>A new <see cref="ModelRoot"/> instance.</returns>
         /// <remarks>
         /// Deep cloning is performed as a brute force operation; by serializing
-        /// the whole model to GLB into memory, and then deserializing it back.
+        /// the whole model to GLTF into memory, and then deserializing it back.
         /// </remarks>
         public ModelRoot DeepClone()
         {
-            var document = this.GetGLB();
-            return ModelRoot.ParseGLB(document);
+            var dictionary = this.WriteToDictionary("deepclone");
+            return ModelRoot.ReadFromDictionary(dictionary, "deepclone.gltf");
         }
 
         #endregion

+ 100 - 52
src/SharpGLTF/Schema2/gltf.Serialization.cs

@@ -33,15 +33,23 @@ namespace SharpGLTF.Schema2
         {
         }
 
+        internal ReadSettings(AssetReader reader)
+        {
+            FileReader = reader;
+        }
+
         internal ReadSettings(string filePath)
         {
             Guard.FilePathMustExist(filePath, nameof(filePath));
 
             var dir = Path.GetDirectoryName(filePath);
 
-            FileReader = asset => File.ReadAllBytes(Path.Combine(dir, asset));
+            FileReader = assetFileName => File.ReadAllBytes(Path.Combine(dir, assetFileName));
         }
 
+        /// <summary>
+        /// Gets or sets the <see cref="AssetReader"/> delegate used to read satellite files.
+        /// </summary>
         public AssetReader FileReader { get; set; }
     }
 
@@ -61,6 +69,11 @@ namespace SharpGLTF.Schema2
             this.FileWriter = (fn, d) => File.WriteAllBytes(Path.Combine(dir, fn), d);
         }
 
+        internal WriteSettings(AssetWriter fileWriter)
+        {
+            this.FileWriter = fileWriter;
+        }
+
         internal WriteSettings(Func<string, Stream> fileWriter)
         {
             void assetWriter(string n, byte[] d)
@@ -93,6 +106,9 @@ namespace SharpGLTF.Schema2
 
         public Formatting JSonFormatting { get; set; }
 
+        /// <summary>
+        /// Gets or sets the <see cref="AssetWriter"/> delegate used to write satellite files.
+        /// </summary>
         public AssetWriter FileWriter { get; set; }
 
         #endregion
@@ -124,11 +140,11 @@ namespace SharpGLTF.Schema2
         /// </summary>
         /// <param name="glb">A <see cref="byte"/> array representing a GLB file</param>
         /// <returns>A <see cref="MODEL"/> instance.</returns>
-        public static MODEL ParseGLB(Byte[] glb)
+        public static MODEL ParseGLB(ArraySegment<Byte> glb)
         {
             Guard.NotNull(glb, nameof(glb));
 
-            using (var m = new MemoryStream(glb))
+            using (var m = new MemoryStream(glb.Array, glb.Offset, glb.Count, false))
             {
                 return ReadGLB(m, new ReadSettings());
             }
@@ -209,7 +225,22 @@ namespace SharpGLTF.Schema2
             }
         }
 
-        private static MODEL _Read(StringReader textReader, ReadSettings settings)
+        public static MODEL ReadFromDictionary(Dictionary<string, Byte[]> files, string fileName)
+        {
+            var jsonBytes = files[fileName];
+
+            var settings = new ReadSettings(fn => files[fn]);
+
+            using (var m = new MemoryStream(jsonBytes))
+            {
+                using (var s = new StreamReader(m))
+                {
+                    return _Read(s, settings);
+                }
+            }
+        }
+
+        private static MODEL _Read(TextReader textReader, ReadSettings settings)
         {
             Guard.NotNull(textReader, nameof(textReader));
             Guard.NotNull(settings, nameof(settings));
@@ -288,6 +319,71 @@ namespace SharpGLTF.Schema2
             Write(settings, name);
         }
 
+        /// <summary>
+        /// Writes this <see cref="MODEL"/> to a dictionary where every key is an individual file
+        /// </summary>
+        /// <param name="fileName">the base name to use for the dictionary keys</param>
+        /// <returns>a dictionary instance.</returns>
+        public Dictionary<String, Byte[]> WriteToDictionary(string fileName)
+        {
+            var dict = new Dictionary<string, Byte[]>();
+
+            var settings = new WriteSettings((fn, buff) => dict[fn] = buff);
+
+            this.Write(settings, fileName);
+
+            return dict;
+        }
+
+        /// <summary>
+        /// Writes this <see cref="MODEL"/> JSON document to a <see cref="TextWriter"/>.
+        /// </summary>
+        /// <param name="sw">The target <see cref="TextWriter"/>.</param>
+        /// <param name="fmt">The formatting of the JSON document.</param>
+        public void WriteJSON(TextWriter sw, Formatting fmt)
+        {
+            using (var writer = new JsonTextWriter(sw))
+            {
+                writer.Formatting = fmt;
+
+                this.Serialize(writer);
+            }
+        }
+
+        /// <summary>
+        /// Gets the JSON document of this <see cref="MODEL"/>.
+        /// </summary>
+        /// <param name="fmt">The formatting of the JSON document.</param>
+        /// <returns>A JSON content.</returns>
+        public string WriteJSON(Formatting fmt)
+        {
+            using (var ss = new StringWriter())
+            {
+                WriteJSON(ss, fmt);
+                return ss.ToString();
+            }
+        }
+
+        /// <summary>
+        /// Writes this <see cref="MODEL"/> to a <see cref="byte"/> array in GLB format.
+        /// </summary>
+        /// <returns>A <see cref="byte"/> array containing a GLB file.</returns>
+        public ArraySegment<Byte> WriteGLB()
+        {
+            using (var m = new MemoryStream())
+            {
+                var settings = new WriteSettings(m);
+
+                // ensure that images are embedded.
+                settings.EmbedImages = true;
+
+                Write(settings, "model");
+
+                if (m.TryGetBuffer(out ArraySegment<Byte> buffer)) return buffer;
+                return new ArraySegment<byte>(m.ToArray());
+            }
+        }
+
         /// <summary>
         /// Writes this <see cref="MODEL"/> to the asset writer in <see cref="WriteSettings"/> configuration.
         /// </summary>
@@ -352,54 +448,6 @@ namespace SharpGLTF.Schema2
             foreach (var i in this._images) i._ClearAfterWrite();
         }
 
-        /// <summary>
-        /// Writes this <see cref="MODEL"/> JSON document to a <see cref="TextWriter"/>.
-        /// </summary>
-        /// <param name="sw">The target <see cref="TextWriter"/>.</param>
-        /// <param name="fmt">The formatting of the JSON document.</param>
-        public void WriteJSON(TextWriter sw, Formatting fmt)
-        {
-            using (var writer = new JsonTextWriter(sw))
-            {
-                writer.Formatting = fmt;
-
-                this.Serialize(writer);
-            }
-        }
-
-        /// <summary>
-        /// Gets the JSON document of this <see cref="MODEL"/>.
-        /// </summary>
-        /// <param name="fmt">The formatting of the JSON document.</param>
-        /// <returns>A JSON content.</returns>
-        public string GetJSON(Formatting fmt)
-        {
-            using (var ss = new StringWriter())
-            {
-                WriteJSON(ss, fmt);
-                return ss.ToString();
-            }
-        }
-
-        /// <summary>
-        /// Writes this <see cref="MODEL"/> to a <see cref="byte"/> array in GLB format.
-        /// </summary>
-        /// <returns>A <see cref="byte"/> array containing a GLB file.</returns>
-        public Byte[] GetGLB()
-        {
-            using (var m = new MemoryStream())
-            {
-                var settings = new WriteSettings(m);
-
-                // ensure that images are embedded.
-                settings.EmbedImages = true;
-
-                Write(settings, "model");
-
-                return m.ToArray();
-            }
-        }
-
         #endregion
     }
 }

+ 5 - 5
tests/SharpGLTF.Tests/Schema2/Authoring/CreateModelTests.cs

@@ -45,8 +45,8 @@ namespace SharpGLTF.Schema2.Authoring
                 ["D"] = new Dictionary<String, Object> { ["S"]= 1, ["T"] = 2 }
             };
 
-            var json = root.GetJSON(Newtonsoft.Json.Formatting.Indented);
-            var bytes = root.GetGLB();
+            var json = root.WriteJSON(Newtonsoft.Json.Formatting.Indented);
+            var bytes = root.WriteGLB();
 
             var rootBis = ModelRoot.ParseGLB(bytes);
 
@@ -210,9 +210,9 @@ namespace SharpGLTF.Schema2.Authoring
 
             // fill our node with the mesh
             meshBuilder.CopyToNode(rnode, createMaterialForColor);
-            
-            model.AttachToCurrentTest("result.glb");
-            model.AttachToCurrentTest("result.gltf");
+
+            model.DeepClone().AttachToCurrentTest("result.gltf");
+            model.DeepClone().AttachToCurrentTest("result.glb");            
         }
 
 

+ 1 - 0
tests/SharpGLTF.Tests/Schema2/Authoring/SimpleMeshBuilder.cs

@@ -98,6 +98,7 @@ namespace SharpGLTF.Schema2.Authoring
                 prim.Material = materialEvaluator(kvp.Key);
             }
 
+            root.MergeImages();
             root.MergeBuffers();            
         }
 

+ 7 - 3
tests/SharpGLTF.Tests/Schema2/LoadAndSave/LoadModelTests.cs

@@ -86,14 +86,18 @@ namespace SharpGLTF.Schema2.LoadAndSave
                 if (!f.Contains(section)) continue;
 
                 var model = GltfUtils.LoadModel(f);
-                Assert.NotNull(model);                
+                Assert.NotNull(model);
 
                 // evaluate and save all the triangles to a Wavefront Object
                 model.AttachToCurrentTest(System.IO.Path.ChangeExtension(System.IO.Path.GetFileName(f), ".obj"));
 
-                // do a model roundtrip                
+                // attempt clone
+                var xclone = model.DeepClone();
+
+                // do a model roundtrip
+                model.MergeImages();
                 model.MergeBuffers();
-                var bytes = model.GetGLB();
+                var bytes = model.WriteGLB();
 
                 var modelBis = ModelRoot.ParseGLB(bytes);
             }

+ 1 - 0
tests/SharpGLTF.Tests/TestUtils.cs

@@ -47,6 +47,7 @@ namespace SharpGLTF
             if (fileName.ToLower().EndsWith(".glb"))
             {
                 // ensure the model has just one buffer
+                model.MergeImages();
                 model.MergeBuffers();
                 model.SaveGLB(fileName);
             }