Browse Source

Refactored Read/Write contexts to simplify the default API

Vicente Penades 6 years ago
parent
commit
667d971271

+ 8 - 0
src/Shared/Guard.cs

@@ -51,6 +51,14 @@ namespace SharpGLTF
             throw new ArgumentException(message, parameterName);
         }
 
+        public static void DirectoryPathMustExist(string dirPath, string parameterName, string message = "")
+        {
+            if (System.IO.Directory.Exists(dirPath)) return;
+
+            if (string.IsNullOrWhiteSpace(message)) message = $"{dirPath} is invalid or does not exist.";
+            throw new ArgumentException(message, parameterName);
+        }
+
         #endregion
 
         #region null / empty

+ 284 - 0
src/SharpGLTF.Core/IO/ReadContext.cs

@@ -0,0 +1,284 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+using Newtonsoft.Json;
+using SharpGLTF.Schema2;
+
+using BYTES = System.ArraySegment<byte>;
+using SCHEMA2 = SharpGLTF.Schema2.ModelRoot;
+using VALIDATIONMODE = SharpGLTF.Validation.ValidationMode;
+
+namespace SharpGLTF.IO
+{
+    /// <summary>
+    /// Callback used for loading associated files of current model.
+    /// </summary>
+    /// <param name="assetName">the asset relative path.</param>
+    /// <returns>The file contents as a <see cref="byte"/> array.</returns>
+    public delegate BYTES FileReaderCallback(String assetName);
+
+    /// <summary>
+    /// Context for reading a <see cref="SCHEMA2"/>.
+    /// </summary>
+    public class ReadContext : Schema2.ReadSettings
+    {
+        #region lifecycle
+
+        public static ReadContext Create(FileReaderCallback callback)
+        {
+            Guard.NotNull(callback, nameof(callback));
+
+            return new ReadContext(callback);
+        }
+
+        public static ReadContext CreateFromFile(string filePath)
+        {
+            Guard.FilePathMustExist(filePath, nameof(filePath));
+
+            var dir = Path.GetDirectoryName(filePath);
+
+            return CreateFromDirectory(dir);
+        }
+
+        public static ReadContext CreateFromDirectory(string directoryPath)
+        {
+            return new ReadContext(assetFileName => new BYTES(File.ReadAllBytes(Path.Combine(directoryPath, assetFileName))));
+        }
+
+        public static ReadContext CreateFromDictionary(IReadOnlyDictionary<string, BYTES> dictionary)
+        {
+            return new ReadContext(fn => dictionary[fn]);
+        }
+
+        private ReadContext(FileReaderCallback reader)
+        {
+            _FileReader = reader;
+        }
+
+        internal ReadContext(ReadContext other)
+            : base(other)
+        {
+            this._FileReader = other._FileReader;
+            this.ImageReader = other.ImageReader;
+        }
+
+        #endregion
+
+        #region data
+
+        private FileReaderCallback _FileReader;
+
+        /// <summary>
+        /// When loading a GLB, this represents the internal binary data chunk.
+        /// </summary>
+        private Byte[] _BinaryChunk;
+
+        #endregion
+
+        #region API
+
+        public BYTES ReadAllBytesToEnd(string fileName)
+        {
+            if (_BinaryChunk != null)
+            {
+                if (string.IsNullOrEmpty(fileName)) return new BYTES(_BinaryChunk);
+            }
+
+            return _FileReader(fileName);
+        }
+
+        /// <summary>
+        /// Opens a file relative to this <see cref="ReadContext"/>.
+        /// </summary>
+        /// <param name="fileName">A relative file Name path.</param>
+        /// <returns>A <see cref="Stream"/>.</returns>
+        public Stream OpenFile(string fileName)
+        {
+            var content = _FileReader(fileName);
+
+            return new MemoryStream(content.Array, content.Offset, content.Count);
+        }
+
+        /// <summary>
+        /// Reads a <see cref="SCHEMA2"/> instance from a <see cref="Stream"/> containing a GLB or a GLTF file.
+        /// </summary>
+        /// <param name="stream">A <see cref="Stream"/> to read from.</param>
+        /// <returns>A <see cref="SCHEMA2"/> instance.</returns>
+        public SCHEMA2 ReadSchema2(Stream stream)
+        {
+            Guard.NotNull(stream, nameof(stream));
+
+            bool binaryFile = glb._Identify(stream);
+
+            return binaryFile ? ReadBinarySchema2(stream) : ReadTextSchema2(stream);
+        }
+
+        /// <summary>
+        /// Reads a <see cref="SCHEMA2"/> instance from a <see cref="Stream"/> containing a GLTF file.
+        /// </summary>
+        /// <param name="stream">A <see cref="Stream"/> to read from.</param>
+        /// <returns>A <see cref="SCHEMA2"/> instance.</returns>
+        public SCHEMA2 ReadTextSchema2(Stream stream)
+        {
+            Guard.NotNull(stream, nameof(stream));
+
+            string content = null;
+
+            using (var streamReader = new StreamReader(stream))
+            {
+                content = streamReader.ReadToEnd();
+            }
+
+            return ParseJson(content);
+        }
+
+        /// <summary>
+        /// Reads a <see cref="SCHEMA2"/> instance from a <see cref="Stream"/> containing a GLB file.
+        /// </summary>
+        /// <param name="stream">A <see cref="Stream"/> to read from.</param>
+        /// <returns>A <see cref="SCHEMA2"/> instance.</returns>
+        public SCHEMA2 ReadBinarySchema2(Stream stream)
+        {
+            Guard.NotNull(stream, nameof(stream));
+
+            var mv = _ReadGLB(stream);
+
+            if (mv.Validation.HasErrors) throw mv.Validation.Errors.FirstOrDefault();
+
+            return mv.Model;
+        }
+
+        /// <summary>
+        /// Parses a <see cref="SCHEMA2"/> instance from a <see cref="String"/> JSON content representing a GLTF file.
+        /// </summary>
+        /// <param name="jsonContent">A <see cref="String"/> JSON content representing a GLTF file.</param>
+        /// <returns>A <see cref="SCHEMA2"/> instance.</returns>
+        public SCHEMA2 ParseJson(String jsonContent)
+        {
+            var mv = _ParseGLTF(jsonContent);
+
+            if (mv.Validation.HasErrors) throw mv.Validation.Errors.FirstOrDefault();
+
+            return mv.Model;
+        }
+
+        public Validation.ValidationResult Validate(string filePath)
+        {
+            using (var stream = File.OpenRead(filePath))
+            {
+                bool isBinary = glb._Identify(stream);
+
+                if (isBinary) return _ReadGLB(stream).Validation;
+
+                string content = null;
+
+                using (var streamReader = new StreamReader(stream))
+                {
+                    content = streamReader.ReadToEnd();
+                }
+
+                return _ParseGLTF(content).Validation;
+            }
+        }
+
+        internal SCHEMA2 _ReadFromDictionary(string fileName)
+        {
+            using (var s = this.OpenFile(fileName))
+            {
+                using (var tr = new StreamReader(s))
+                {
+                    var mv = this._Read(tr);
+
+                    if (mv.Validation.HasErrors) throw mv.Validation.Errors.FirstOrDefault();
+
+                    return mv.Model;
+                }
+            }
+        }
+
+        #endregion
+
+        #region core
+
+        private (SCHEMA2 Model, Validation.ValidationResult Validation) _ReadGLB(Stream stream)
+        {
+            Guard.NotNull(stream, nameof(stream));
+
+            var chunks = glb.ReadBinaryFile(stream);
+
+            var dom = Encoding.UTF8.GetString(chunks[glb.CHUNKJSON]);
+
+            var context = this;
+
+            if (chunks.ContainsKey(glb.CHUNKBIN))
+            {
+                context = new ReadContext(context); // clone instance
+                context._BinaryChunk = chunks[glb.CHUNKBIN];
+            }
+
+            return context._ParseGLTF(dom);
+        }
+
+        private (SCHEMA2 Model, Validation.ValidationResult Validation) _ParseGLTF(String jsonContent)
+        {
+            Guard.NotNullOrEmpty(jsonContent, nameof(jsonContent));
+            using (var tr = new StringReader(jsonContent))
+            {
+                return _Read(tr);
+            }
+        }
+
+        private (SCHEMA2 Model, Validation.ValidationResult Validation) _Read(TextReader textReader)
+        {
+            Guard.NotNull(textReader, nameof(textReader));
+
+            var root = new SCHEMA2();
+            var vcontext = new Validation.ValidationResult(root, this.Validation);
+
+            using (var reader = new JsonTextReader(textReader))
+            {
+                if (!reader.Read())
+                {
+                    vcontext.AddError(new Validation.ModelException(root, "Json is empty"));
+                    return (null, vcontext);
+                }
+
+                try
+                {
+                    root.Deserialize(reader);
+                }
+                catch (JsonReaderException rex)
+                {
+                    vcontext.AddError(new Validation.SchemaException(root, rex));
+                    return (null, vcontext);
+                }
+            }
+
+            // schema validation
+
+            root.ValidateReferences(vcontext.GetContext());
+            var ex = vcontext.Errors.FirstOrDefault();
+            if (ex != null) return (null, vcontext);
+
+            // resolve external dependencies
+
+            root._ResolveSatelliteDependencies(this);
+
+            // full validation
+
+            if (this.Validation != VALIDATIONMODE.Skip)
+            {
+                root.Validate(vcontext.GetContext());
+                ex = vcontext.Errors.FirstOrDefault();
+                if (ex != null) return (null, vcontext);
+            }
+
+            return (root, vcontext);
+        }
+
+        #endregion
+    }
+}

+ 245 - 0
src/SharpGLTF.Core/IO/WriteContext.cs

@@ -0,0 +1,245 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+using Newtonsoft.Json;
+using SharpGLTF.Schema2;
+
+using BYTES = System.ArraySegment<byte>;
+using SCHEMA2 = SharpGLTF.Schema2.ModelRoot;
+
+namespace SharpGLTF.IO
+{
+    /// <summary>
+    /// Callback used for saving associated files of the current model.
+    /// </summary>
+    /// <param name="assetName">The asset relative path.</param>
+    /// <param name="assetData">The file contents as a <see cref="byte"/> array.</param>
+    public delegate void FileWriterCallback(String assetName, BYTES assetData);
+
+    /// <summary>
+    /// Configuration settings for writing model files.
+    /// </summary>
+    public class WriteContext : WriteSettings
+    {
+        #region lifecycle
+
+        public static WriteContext Create(FileWriterCallback callback)
+        {
+            Guard.NotNull(callback, nameof(callback));
+
+            var context = new WriteContext(callback)
+            {
+                _UpdateSupportedExtensions = true
+            };
+
+            return context;
+        }
+
+        public static WriteContext CreateFromFile(string filePath)
+        {
+            Guard.FilePathMustBeValid(filePath, nameof(filePath));
+
+            var dir = Path.GetDirectoryName(filePath);
+
+            return CreateFromDirectory(dir);
+        }
+
+        public static WriteContext CreateFromDirectory(string dirPath)
+        {
+            Guard.DirectoryPathMustExist(dirPath, nameof(dirPath));
+
+            var context = Create((fn, d) => File.WriteAllBytes(Path.Combine(dirPath, fn), d.ToArray()));
+            context.ImageWriting = ResourceWriteMode.SatelliteFile;
+            context.JsonFormatting = Formatting.Indented;
+            return context;
+        }
+
+        public static WriteContext CreateFromDictionary(IDictionary<string, BYTES> dict)
+        {
+            Guard.NotNull(dict, nameof(dict));
+
+            var context = Create((fn, buff) => dict[fn] = buff);
+            context.ImageWriting = ResourceWriteMode.SatelliteFile;
+            context.MergeBuffers = false;
+            context.JsonFormatting = Formatting.None;
+
+            return context;
+        }
+
+        public static WriteContext CreateFromStream(Stream stream)
+        {
+            Guard.NotNull(stream, nameof(stream));
+            Guard.IsTrue(stream.CanWrite, nameof(stream));
+
+            var context = Create((fn, d) => stream.Write(d.Array, d.Offset, d.Count));
+            context.ImageWriting = ResourceWriteMode.Embedded;
+            context.MergeBuffers = true;
+            context.JsonFormatting = Formatting.None;
+
+            return context.WithBinarySettings();
+        }
+
+        public WriteContext WithBinarySettings()
+        {
+            ImageWriting = ResourceWriteMode.BufferView;
+            MergeBuffers = true;
+            JsonFormatting = Formatting.None;
+
+            return this;
+        }
+
+        /// <summary>
+        /// These settings are used exclusively by <see cref="SCHEMA2.DeepClone"/>.
+        /// </summary>
+        /// <returns>A <see cref="WriteContext"/> instance to be used by <see cref="SCHEMA2.DeepClone()"/></returns>
+        internal WriteContext WithDeepCloneSettings()
+        {
+            _UpdateSupportedExtensions = false;
+            _NoCloneWatchdog = true;
+            MergeBuffers = false;
+
+            return this;
+        }
+
+        private WriteContext(FileWriterCallback callback)
+        {
+            _FileWriter = callback;
+        }
+
+        #endregion
+
+        #region data
+
+        private readonly FileWriterCallback _FileWriter;
+
+        #endregion
+
+        #region properties
+
+        /// <summary>
+        /// Gets a value indicating whether to scan the whole model for used extensions.
+        /// </summary>
+        internal Boolean _UpdateSupportedExtensions { get; private set; } = true;
+
+        /// <summary>
+        /// Gets a value indicating whether creating a defensive copy before serialization is not allowed.
+        /// </summary>
+        internal bool _NoCloneWatchdog { get; private set; } = false;
+
+        #endregion
+
+        #region API
+
+        public void WriteAllBytesToEnd(string fileName, BYTES data)
+        {
+            this._FileWriter(fileName, data);
+        }
+
+        /// <summary>
+        /// Writes <paramref name="model"/> to this context.
+        /// </summary>
+        /// <param name="baseName">The base name to use for asset files, without extension.</param>
+        /// <param name="model">The <see cref="SCHEMA2"/> to write.</param>
+        public void WriteTextSchema2(string baseName, SCHEMA2 model)
+        {
+            Guard.NotNullOrEmpty(baseName, nameof(baseName));
+            Guard.NotNull(model, nameof(model));
+
+            model = this._PreprocessSchema2(model, this.ImageWriting == ResourceWriteMode.BufferView, this.MergeBuffers);
+            Guard.NotNull(model, nameof(model));
+
+            model._PrepareBuffersForSatelliteWriting(this, baseName);
+
+            model._PrepareImagesForWriting(this, baseName, ResourceWriteMode.SatelliteFile);
+
+            using (var m = new MemoryStream())
+            {
+                using (var w = new StreamWriter(m))
+                {
+                    model._WriteJSON(w, this.JsonFormatting);
+                }
+
+                WriteAllBytesToEnd($"{baseName}.gltf", m.ToArraySegment());
+            }
+
+            model._AfterWriting();
+        }
+
+        /// <summary>
+        /// Writes <paramref name="model"/> to this context.
+        /// </summary>
+        /// <param name="baseName">The base name to use for asset files, without extension.</param>
+        /// <param name="model">The <see cref="SCHEMA2"/> to write.</param>
+        public void WriteBinarySchema2(string baseName, SCHEMA2 model)
+        {
+            Guard.NotNullOrEmpty(baseName, nameof(baseName));
+            Guard.NotNull(model, nameof(model));
+
+            model = this._PreprocessSchema2(model, this.ImageWriting == ResourceWriteMode.BufferView, true);
+            Guard.NotNull(model, nameof(model));
+
+            var ex = glb.IsBinaryCompatible(model);
+            if (ex != null) throw ex;
+
+            model._PrepareBuffersForInternalWriting();
+
+            model._PrepareImagesForWriting(this, baseName, ResourceWriteMode.Embedded);
+
+            using (var m = new MemoryStream())
+            {
+                using (var w = new BinaryWriter(m))
+                {
+                    glb.WriteBinaryModel(w, model);
+                }
+
+                WriteAllBytesToEnd($"{baseName}.glb", m.ToArraySegment());
+            }
+
+            model._AfterWriting();
+        }
+
+        #endregion
+
+        #region core
+
+        /// <summary>
+        /// Prepares the model for writing with the appropiate settings, creating a defensive copy if neccesary.
+        /// </summary>
+        /// <param name="model">The source <see cref="SCHEMA2"/> instance.</param>
+        /// <param name="imagesAsBufferViews">true if images should be stored as buffer views.</param>
+        /// <param name="mergeBuffers">true if it's required the model must have a single buffer.</param>
+        /// <returns>The source <see cref="SCHEMA2"/> instance, or a cloned and modified instance if current settings required it.</returns>
+        private SCHEMA2 _PreprocessSchema2(SCHEMA2 model, bool imagesAsBufferViews, bool mergeBuffers)
+        {
+            Guard.NotNull(model, nameof(model));
+
+            foreach (var img in model.LogicalImages) if (!img._HasContent) throw new Validation.DataException(img, "Image Content is missing.");
+
+            // check if we need to modify the model before saving it,
+            // in order to create a defensive copy.
+
+            if (model.LogicalImages.Count == 0) imagesAsBufferViews = false;
+            if (model.LogicalBuffers.Count <= 1 && !imagesAsBufferViews) mergeBuffers = false;
+
+            if (mergeBuffers | imagesAsBufferViews)
+            {
+                // cloning check is done to prevent cloning from entering in an infinite loop where each clone attempt triggers another clone request.
+                if (_NoCloneWatchdog) throw new InvalidOperationException($"Current settings require creating a densive copy before model modification, but calling {nameof(SCHEMA2.DeepClone)} is not allowed with the current settings.");
+
+                model = model.DeepClone();
+            }
+
+            if (imagesAsBufferViews) model.MergeImages();
+            if (mergeBuffers) model.MergeBuffers();
+
+            if (this._UpdateSupportedExtensions) model.UpdateExtensionsSupport();
+
+            return model;
+        }
+
+        #endregion
+    }
+}

+ 6 - 6
src/SharpGLTF.Core/Schema2/gltf.Buffer.cs

@@ -44,18 +44,18 @@ namespace SharpGLTF.Schema2
         const string EMBEDDEDOCTETSTREAM = "data:application/octet-stream;base64,";
         const string EMBEDDEDGLTFBUFFER = "data:application/gltf-buffer;base64,";
 
-        internal void _ResolveUri(ReadContext context)
+        internal void _ResolveUri(IO.ReadContext context)
         {
             _Content = _LoadBinaryBufferUnchecked(_uri, context);
 
             _uri = null; // When _Data is not empty, clear URI
         }
 
-        private static Byte[] _LoadBinaryBufferUnchecked(string uri, ReadContext context)
+        private static Byte[] _LoadBinaryBufferUnchecked(string uri, IO.ReadContext context)
         {
             return uri._TryParseBase64Unchecked(EMBEDDEDGLTFBUFFER)
                 ?? uri._TryParseBase64Unchecked(EMBEDDEDOCTETSTREAM)
-                ?? context.ReadBytes(uri).ToArray();
+                ?? context.ReadAllBytesToEnd(uri).ToArray();
         }
 
         #endregion
@@ -67,12 +67,12 @@ namespace SharpGLTF.Schema2
         /// </summary>
         /// <param name="writer">The satellite asset writer</param>
         /// <param name="satelliteUri">A local satellite URI</param>
-        internal void _WriteToSatellite(AssetWriter writer, string satelliteUri)
+        internal void _WriteToSatellite(IO.WriteContext writer, string satelliteUri)
         {
             this._uri = satelliteUri;
             this._byteLength = _Content.Length;
 
-            writer(satelliteUri, new ArraySegment<byte>(_Content.GetPaddedContent()) );
+            writer.WriteAllBytesToEnd(satelliteUri, new ArraySegment<byte>(_Content.GetPaddedContent()) );
         }
 
         /// <summary>
@@ -86,7 +86,7 @@ namespace SharpGLTF.Schema2
 
         /// <summary>
         /// Called by the serializer immediatelly after
-        /// calling <see cref="_WriteToSatellite(AssetWriter, string)"/>
+        /// calling <see cref="_WriteToSatellite(WriteContext, string)"/>
         /// or <see cref="_WriteToInternal"/>
         /// </summary>
         internal void _ClearAfterWrite()

+ 9 - 9
src/SharpGLTF.Core/Schema2/gltf.Images.cs

@@ -205,7 +205,7 @@ namespace SharpGLTF.Schema2
 
         #region binary read
 
-        internal void _ResolveUri(ReadContext context)
+        internal void _ResolveUri(IO.ReadContext context)
         {
             if (!String.IsNullOrWhiteSpace(_uri))
             {
@@ -217,14 +217,14 @@ namespace SharpGLTF.Schema2
             }
         }
 
-        private static Byte[] _LoadImageUnchecked(ReadContext context, string uri)
+        private static Byte[] _LoadImageUnchecked(IO.ReadContext context, string uri)
         {
             return uri._TryParseBase64Unchecked(EMBEDDED_GLTF_BUFFER)
                 ?? uri._TryParseBase64Unchecked(EMBEDDED_OCTET_STREAM)
                 ?? uri._TryParseBase64Unchecked(EMBEDDED_JPEG_BUFFER)
                 ?? uri._TryParseBase64Unchecked(EMBEDDED_PNG_BUFFER)
                 ?? uri._TryParseBase64Unchecked(EMBEDDED_DDS_BUFFER)
-                ?? context.ReadBytes(uri).ToArray();
+                ?? context.ReadAllBytesToEnd(uri).ToArray();
         }
 
         internal void _DiscardContent()
@@ -284,7 +284,7 @@ namespace SharpGLTF.Schema2
         /// </summary>
         /// <param name="writer">The satellite asset writer</param>
         /// <param name="satelliteUri">A local satellite URI</param>
-        internal void _WriteToSatellite(AssetWriter writer, string satelliteUri)
+        internal void _WriteToSatellite(IO.WriteContext writer, string satelliteUri)
         {
             if (_SatelliteImageContent == null)
             {
@@ -296,7 +296,7 @@ namespace SharpGLTF.Schema2
             {
                 _mimeType = null;
                 _uri = satelliteUri += ".png";
-                writer(_uri, _SatelliteImageContent.Slice(0) );
+                writer.WriteAllBytesToEnd(_uri, _SatelliteImageContent.Slice(0) );
                 return;
             }
 
@@ -304,7 +304,7 @@ namespace SharpGLTF.Schema2
             {
                 _mimeType = null;
                 _uri = satelliteUri += ".jpg";
-                writer(_uri, _SatelliteImageContent.Slice(0) );
+                writer.WriteAllBytesToEnd(_uri, _SatelliteImageContent.Slice(0) );
                 return;
             }
 
@@ -312,7 +312,7 @@ namespace SharpGLTF.Schema2
             {
                 _mimeType = null;
                 _uri = satelliteUri += ".dds";
-                writer(_uri, _SatelliteImageContent.Slice(0) );
+                writer.WriteAllBytesToEnd(_uri, _SatelliteImageContent.Slice(0) );
                 return;
             }
 
@@ -320,7 +320,7 @@ namespace SharpGLTF.Schema2
             {
                 _mimeType = null;
                 _uri = satelliteUri += ".webp";
-                writer(_uri, _SatelliteImageContent.Slice(0));
+                writer.WriteAllBytesToEnd(_uri, _SatelliteImageContent.Slice(0));
                 return;
             }
 
@@ -343,7 +343,7 @@ namespace SharpGLTF.Schema2
 
         /// <summary>
         /// Called by the serializer immediatelly after
-        /// calling <see cref="_WriteToSatellite(AssetWriter, string)"/>
+        /// calling <see cref="_WriteToSatellite(FileWriterCallback, string)"/>
         /// or <see cref="_WriteToInternal"/>
         /// </summary>
         internal void _ClearAfterWrite()

+ 8 - 6
src/SharpGLTF.Core/Schema2/gltf.Root.cs

@@ -54,15 +54,17 @@ namespace SharpGLTF.Schema2
         public ModelRoot DeepClone()
         {
             var dict = new Dictionary<string, ArraySegment<Byte>>();
-            var settings = WriteContext.ForDeepClone(dict);
+            var wcontext = IO.WriteContext
+                .CreateFromDictionary(dict)
+                .WithDeepCloneSettings();
 
-            System.Diagnostics.Debug.Assert(settings._NoCloneWatchdog, "invalid clone settings");
+            System.Diagnostics.Debug.Assert(wcontext._NoCloneWatchdog, "invalid clone settings");
 
-            this.Write(settings, "deepclone");
+            wcontext.WriteTextSchema2("deepclone", this);
 
-            var context = ReadContext.CreateFromDictionary(dict);
-            context.Validation = Validation.ValidationMode.Strict;
-            return context._ReadFromDictionary("deepclone.gltf");
+            var rcontext = IO.ReadContext.CreateFromDictionary(dict);
+            rcontext.Validation = Validation.ValidationMode.Strict;
+            return rcontext._ReadFromDictionary("deepclone.gltf");
         }
 
         #endregion

+ 45 - 272
src/SharpGLTF.Core/Schema2/gltf.Serialization.Read.cs

@@ -13,27 +13,29 @@ namespace SharpGLTF.Schema2
     using MODEL = ModelRoot;
     using VALIDATIONMODE = Validation.ValidationMode;
 
-    /// <summary>
-    /// Callback used for loading associated files of current model.
-    /// </summary>
-    /// <param name="assetName">the asset relative path.</param>
-    /// <returns>The file contents as a <see cref="byte"/> array.</returns>
-    public delegate BYTES FileReaderCallback(String assetName);
-
     public delegate Boolean ImageReaderCallback(Image image);
 
     /// <summary>
-    /// Configuration settings for reading model files.
+    /// Settings to customize how <see cref="MODEL"/> files are read.
     /// </summary>
     public class ReadSettings
     {
         #region lifecycle
 
+        public static implicit operator ReadSettings(VALIDATIONMODE vmode)
+        {
+            return new ReadSettings
+            {
+                Validation = vmode
+            };
+        }
+
         public ReadSettings() { }
 
         public ReadSettings(ReadSettings other)
         {
-            this.Validation = other.Validation;
+            Guard.NotNull(other, nameof(other));
+            other.CopyTo(this);
         }
 
         #endregion
@@ -45,264 +47,17 @@ namespace SharpGLTF.Schema2
         /// </summary>
         public VALIDATIONMODE Validation { get; set; } = VALIDATIONMODE.Strict;
 
-        #endregion
-    }
-
-    public class ReadContext : ReadSettings
-    {
-        #region lifecycle
-
-        public static ReadContext Create(FileReaderCallback callback)
-        {
-            Guard.NotNull(callback, nameof(callback));
-
-            return new ReadContext(callback);
-        }
-
-        public static ReadContext CreateFromFile(string filePath)
-        {
-            Guard.FilePathMustExist(filePath, nameof(filePath));
-
-            var dir = Path.GetDirectoryName(filePath);
-
-            return CreateFromDirectory(dir);
-        }
-
-        public static ReadContext CreateFromDirectory(string directoryPath)
-        {
-            return new ReadContext(assetFileName => new BYTES(File.ReadAllBytes(Path.Combine(directoryPath, assetFileName))));
-        }
-
-        public static ReadContext CreateFromDictionary(IReadOnlyDictionary<string, BYTES> dictionary)
-        {
-            return new ReadContext(fn => dictionary[fn]);
-        }
-
-        private ReadContext(FileReaderCallback reader)
-        {
-            _FileReader = reader;
-        }
-
-        internal ReadContext(ReadContext other)
-            : base(other)
-        {
-            this._FileReader = other._FileReader;
-            this.ImageReader = other.ImageReader;
-        }
-
-        #endregion
-
-        #region data
-
-        private FileReaderCallback _FileReader;
-
-        /// <summary>
-        /// When loading GLB, this represents the internal binary data chunk.
-        /// </summary>
-        private Byte[] _BinaryChunk;
-
-        #endregion
-
-        #region callbacks
-
         public ImageReaderCallback ImageReader { get; set; }
 
         #endregion
 
         #region API
 
-        public BYTES ReadBytes(string fileName)
-        {
-            if (_BinaryChunk != null)
-            {
-                if (string.IsNullOrEmpty(fileName)) return new BYTES(_BinaryChunk);
-            }
-
-            return _FileReader(fileName);
-        }
-
-        public Stream OpenFile(string fileName)
-        {
-            var content = _FileReader(fileName);
-
-            return new MemoryStream(content.Array, content.Offset, content.Count);
-        }
-
-        /// <summary>
-        /// Reads a <see cref="MODEL"/> instance from a <see cref="Stream"/> containing a GLB or a GLTF file.
-        /// </summary>
-        /// <param name="stream">A <see cref="Stream"/> to read from.</param>
-        /// <returns>A <see cref="MODEL"/> instance.</returns>
-        public MODEL Read(Stream stream)
-        {
-            Guard.NotNull(stream, nameof(stream));
-
-            bool binaryFile = glb._Identify(stream);
-
-            return binaryFile ? ReadGLB(stream) : ReadGLTF(stream);
-        }
-
-        /// <summary>
-        /// Reads a <see cref="MODEL"/> instance from a <see cref="Stream"/> containing a GLTF file.
-        /// </summary>
-        /// <param name="stream">A <see cref="Stream"/> to read from.</param>
-        /// <returns>A <see cref="MODEL"/> instance.</returns>
-        public MODEL ReadGLTF(Stream stream)
-        {
-            Guard.NotNull(stream, nameof(stream));
-
-            string content = null;
-
-            using (var streamReader = new StreamReader(stream))
-            {
-                content = streamReader.ReadToEnd();
-            }
-
-            return ParseGLTF(content);
-        }
-
-        /// <summary>
-        /// Reads a <see cref="MODEL"/> instance from a <see cref="Stream"/> containing a GLB file.
-        /// </summary>
-        /// <param name="stream">A <see cref="Stream"/> to read from.</param>
-        /// <returns>A <see cref="MODEL"/> instance.</returns>
-        public MODEL ReadGLB(Stream stream)
-        {
-            Guard.NotNull(stream, nameof(stream));
-
-            var mv = _ReadGLB(stream);
-
-            if (mv.Validation.HasErrors) throw mv.Validation.Errors.FirstOrDefault();
-
-            return mv.Model;
-        }
-
-        /// <summary>
-        /// Parses a <see cref="MODEL"/> instance from a <see cref="String"/> JSON content representing a GLTF file.
-        /// </summary>
-        /// <param name="jsonContent">A <see cref="String"/> JSON content representing a GLTF file.</param>
-        /// <returns>A <see cref="MODEL"/> instance.</returns>
-        public MODEL ParseGLTF(String jsonContent)
-        {
-            var mv = _ParseGLTF(jsonContent);
-
-            if (mv.Validation.HasErrors) throw mv.Validation.Errors.FirstOrDefault();
-
-            return mv.Model;
-        }
-
-        public Validation.ValidationResult Validate(string filePath)
-        {
-            using (var stream = File.OpenRead(filePath))
-            {
-                bool binaryFile = glb._Identify(stream);
-
-                if (binaryFile) return _ReadGLB(stream).Validation;
-
-                string content = null;
-
-                using (var streamReader = new StreamReader(stream))
-                {
-                    content = streamReader.ReadToEnd();
-                }
-
-                return _ParseGLTF(content).Validation;
-            }
-        }
-
-        internal MODEL _ReadFromDictionary(string fileName)
-        {
-            using (var s = this.OpenFile(fileName))
-            {
-                using (var tr = new StreamReader(s))
-                {
-                    var mv = this._Read(tr);
-
-                    if (mv.Validation.HasErrors) throw mv.Validation.Errors.FirstOrDefault();
-
-                    return mv.Model;
-                }
-            }
-        }
-
-        #endregion
-
-        #region core
-
-        private (MODEL Model, Validation.ValidationResult Validation) _ReadGLB(Stream stream)
-        {
-            Guard.NotNull(stream, nameof(stream));
-
-            var chunks = glb.ReadBinaryFile(stream);
-
-            var dom = Encoding.UTF8.GetString(chunks[glb.CHUNKJSON]);
-
-            var context = this;
-
-            if (chunks.ContainsKey(glb.CHUNKBIN))
-            {
-                context = new ReadContext(context); // clone instance
-                context._BinaryChunk = chunks[glb.CHUNKBIN];
-            }
-
-            return context._ParseGLTF(dom);
-        }
-
-        private (MODEL Model, Validation.ValidationResult Validation) _ParseGLTF(String jsonContent)
+        public void CopyTo(ReadSettings other)
         {
-            Guard.NotNullOrEmpty(jsonContent, nameof(jsonContent));
-            using (var tr = new StringReader(jsonContent))
-            {
-                return _Read(tr);
-            }
-        }
-
-        private (MODEL Model, Validation.ValidationResult Validation) _Read(TextReader textReader)
-        {
-            Guard.NotNull(textReader, nameof(textReader));
-
-            var root = new MODEL();
-            var vcontext = new Validation.ValidationResult(root, this.Validation);
-
-            using (var reader = new JsonTextReader(textReader))
-            {
-                if (!reader.Read())
-                {
-                    vcontext.AddError(new Validation.ModelException(root, "Json is empty"));
-                    return (null, vcontext);
-                }
-
-                try
-                {
-                    root.Deserialize(reader);
-                }
-                catch (JsonReaderException rex)
-                {
-                    vcontext.AddError(new Validation.SchemaException(root, rex));
-                    return (null, vcontext);
-                }
-            }
-
-            // schema validation
-
-            root.ValidateReferences(vcontext.GetContext());
-            var ex = vcontext.Errors.FirstOrDefault();
-            if (ex != null) return (null, vcontext);
-
-            // resolve external dependencies
-
-            root._ResolveSatelliteDependencies(this);
-
-            // full validation
-
-            if (this.Validation != VALIDATIONMODE.Skip)
-            {
-                root.Validate(vcontext.GetContext());
-                ex = vcontext.Errors.FirstOrDefault();
-                if (ex != null) return (null, vcontext);
-            }
-
-            return (root, vcontext);
+            Guard.NotNull(other, nameof(other));
+            other.Validation = this.Validation;
+            other.ImageReader = this.ImageReader;
         }
 
         #endregion
@@ -316,7 +71,7 @@ namespace SharpGLTF.Schema2
         {
             Guard.FilePathMustExist(filePath, nameof(filePath));
 
-            var context = ReadContext.CreateFromFile(filePath);
+            var context = IO.ReadContext.CreateFromFile(filePath);
 
             return context.Validate(filePath);
         }
@@ -329,19 +84,19 @@ namespace SharpGLTF.Schema2
         /// Reads a <see cref="MODEL"/> instance from a path pointing to a GLB or a GLTF file
         /// </summary>
         /// <param name="filePath">A valid file path.</param>
-        /// <param name="vmode">Defines the file validation level.</param>
+        /// <param name="settings">Optional settings.</param>
         /// <returns>A <see cref="MODEL"/> instance.</returns>
-        public static MODEL Load(string filePath, VALIDATIONMODE vmode = VALIDATIONMODE.Strict)
+        public static MODEL Load(string filePath, ReadSettings settings = null)
         {
             Guard.FilePathMustExist(filePath, nameof(filePath));
 
-            var context = ReadContext.CreateFromFile(filePath);
+            var context = IO.ReadContext.CreateFromFile(filePath);
 
-            context.Validation = vmode;
+            if (settings != null) settings.CopyTo(context);
 
             using (var s = File.OpenRead(filePath))
             {
-                return context.Read(s);
+                return context.ReadSchema2(s);
             }
         }
 
@@ -349,24 +104,42 @@ namespace SharpGLTF.Schema2
         /// Parses a <see cref="MODEL"/> instance from a <see cref="byte"/> array representing a GLB file
         /// </summary>
         /// <param name="glb">A <see cref="byte"/> array representing a GLB file</param>
+        /// <param name="settings">Optional settings.</param>
         /// <returns>A <see cref="MODEL"/> instance.</returns>
-        public static MODEL ParseGLB(BYTES glb)
+        public static MODEL ParseGLB(BYTES glb, ReadSettings settings = null)
         {
             Guard.NotNull(glb, nameof(glb));
 
-            var context = ReadContext.Create(f => throw new NotSupportedException());
-
             using (var m = new MemoryStream(glb.Array, glb.Offset, glb.Count, false))
             {
-                return context.ReadGLB(m);
+                return ReadGLB(m, settings);
             }
         }
 
+        /// <summary>
+        /// Reads a <see cref="MODEL"/> instance from a <see cref="Stream"/> representing a GLB file
+        /// </summary>
+        /// <param name="stream">The source <see cref="Stream"/>.</param>
+        /// <param name="settings">Optional settings.</param>
+        /// <returns>A <see cref="MODEL"/> instance.</returns>
+        public static MODEL ReadGLB(Stream stream, ReadSettings settings = null)
+        {
+            Guard.NotNull(stream, nameof(stream));
+            Guard.IsTrue(stream.CanRead, nameof(stream));
+
+            var context = IO.ReadContext
+                .Create(f => throw new NotSupportedException());
+
+            if (settings != null) settings.CopyTo(context);
+
+            return context.ReadBinarySchema2(stream);
+        }
+
         #endregion
 
-        #region externals resolver
+        #region externals dependencies resolver
 
-        internal void _ResolveSatelliteDependencies(ReadContext context)
+        internal void _ResolveSatelliteDependencies(IO.ReadContext context)
         {
             // resolve satellite buffers
 

+ 92 - 258
src/SharpGLTF.Core/Schema2/gltf.Serialization.Write.cs

@@ -17,6 +17,11 @@ namespace SharpGLTF.Schema2
     /// </summary>
     public enum ResourceWriteMode
     {
+        /// <summary>
+        /// Use the most appropiate mode.
+        /// </summary>
+        Default,
+
         /// <summary>
         /// Resources will be stored as external satellite files.
         /// </summary>
@@ -33,191 +38,55 @@ namespace SharpGLTF.Schema2
         BufferView
     }
 
-    /// <summary>
-    /// Callback used for saving associated files of the current model.
-    /// </summary>
-    /// <param name="assetName">The asset relative path.</param>
-    /// <param name="assetData">The file contents as a <see cref="byte"/> array.</param>
-    public delegate void AssetWriter(String assetName, BYTES assetData);
-
-    /// <summary>
-    /// Configuration settings for writing model files.
-    /// </summary>
-    public class WriteContext
+    public class WriteSettings
     {
         #region lifecycle
 
-        /// <summary>
-        /// These settings are used exclusively by <see cref="MODEL.DeepClone"/>.
-        /// </summary>
-        /// <param name="dict">The dictionary where the model will be stored</param>
-        /// <returns>The settings to use with <see cref="MODEL.Write(WriteContext, string)"/></returns>
-        internal static WriteContext ForDeepClone(Dictionary<string, BYTES> dict)
+        public static implicit operator WriteSettings(Formatting jsonfmt)
         {
-            var settings = new WriteContext()
+            return new WriteSettings
             {
-                BinaryMode = false,
-                ImageWriting = ResourceWriteMode.SatelliteFile,
-                MergeBuffers = false,
-                JsonFormatting = Formatting.None,
-                _UpdateSupportedExtensions = false,
-                _NoCloneWatchdog = true,
-
-                FileWriter = (fn, buff) => dict[fn] = buff
+                JsonFormatting = jsonfmt
             };
-
-            return settings;
         }
 
-        internal static WriteContext ForText(string filePath)
-        {
-            Guard.FilePathMustBeValid(filePath, nameof(filePath));
-
-            var dir = Path.GetDirectoryName(filePath);
-
-            var settings = new WriteContext
-            {
-                BinaryMode = false,
-                ImageWriting = ResourceWriteMode.SatelliteFile,
-                MergeBuffers = true,
-                JsonFormatting = Formatting.Indented,
-                _UpdateSupportedExtensions = true,
+        public WriteSettings() { }
 
-                FileWriter = (fn, d) => File.WriteAllBytes(Path.Combine(dir, fn), d.ToArray())
-            };
-
-            return settings;
-        }
-
-        internal static WriteContext ForText(Dictionary<string, BYTES> dict)
+        public WriteSettings(WriteSettings other)
         {
-            var settings = new WriteContext()
-            {
-                BinaryMode = false,
-                ImageWriting = ResourceWriteMode.SatelliteFile,
-                MergeBuffers = false,
-                JsonFormatting = Formatting.None,
-                _UpdateSupportedExtensions = true,
-
-                FileWriter = (fn, buff) => dict[fn] = buff
-            };
-
-            return settings;
+            Guard.NotNull(other, nameof(other));
+            other.CopyTo(this);
         }
 
-        internal static WriteContext ForBinary(string filePath)
-        {
-            Guard.FilePathMustBeValid(filePath, nameof(filePath));
-
-            var dir = Path.GetDirectoryName(filePath);
-
-            var settings = new WriteContext
-            {
-                BinaryMode = true,
-                ImageWriting = ResourceWriteMode.BufferView,
-                MergeBuffers = true,
-                JsonFormatting = Formatting.None,
-                _UpdateSupportedExtensions = true,
-
-                FileWriter = (fn, d) => File.WriteAllBytes(Path.Combine(dir, fn), d.ToArray())
-            };
-
-            return settings;
-        }
-
-        internal static WriteContext ForBinary(Stream stream)
-        {
-            Guard.NotNull(stream, nameof(stream));
-            Guard.IsTrue(stream.CanWrite, nameof(stream));
-
-            var settings = new WriteContext
-            {
-                BinaryMode = true,
-                ImageWriting = ResourceWriteMode.BufferView,
-                MergeBuffers = true,
-                JsonFormatting = Formatting.None,
-                _UpdateSupportedExtensions = true,
-
-                FileWriter = (fn, d) => stream.Write(d.Array, d.Offset, d.Count)
-            };
-
-            return settings;
-        }
-        
         #endregion
 
-        #region data
-
-        /// <summary>
-        /// Gets or sets a value indicating whether to write a GLTF or a GLB file.
-        /// </summary>
-        public Boolean BinaryMode { get; set; }
+        #region properties
 
         /// <summary>
         /// Gets or sets a value indicating how to write the images of the model.
         /// </summary>
-        public ResourceWriteMode ImageWriting { get; set; }
+        public ResourceWriteMode ImageWriting { get; set; } = ResourceWriteMode.Default;
 
         /// <summary>
         /// Gets or sets a value indicating whether to merge all the buffers in <see cref="MODEL.LogicalBuffers"/> into a single buffer.
         /// </summary>
-        public Boolean MergeBuffers { get; set; }
+        public Boolean MergeBuffers { get; set; } = true;
 
         /// <summary>
         /// Gets or sets a value indicating how to format the JSON document of the glTF.
         /// </summary>
-        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; }
-
-        /// <summary>
-        /// Gets a value indicating whether to scan the whole model for used extensions.
-        /// </summary>
-        internal Boolean _UpdateSupportedExtensions { get; private set; } = true;
-
-        /// <summary>
-        /// Gets a value indicating whether creating a defensive copy before serialization is not allowed.
-        /// </summary>
-        internal bool _NoCloneWatchdog { get; private set; } = false;
+        public Formatting JsonFormatting { get; set; } = Formatting.None;
 
         #endregion
 
         #region API
 
-        /// <summary>
-        /// Prepares the model for writing with the appropiate settings, creating a defensive copy if neccesary.
-        /// </summary>
-        /// <param name="model">The source <see cref="MODEL"/> instance.</param>
-        /// <returns>The source <see cref="MODEL"/> instance, or a cloned and modified instance if current settings required it.</returns>
-        internal MODEL FilterModel(MODEL model)
+        public void CopyTo(WriteSettings other)
         {
-            Guard.NotNull(model, nameof(model));
-
-            // check if we need to modify the model before saving it,
-            // in order to create a defensive copy.
-
-            var needsMergeBuffers = (this.MergeBuffers | this.BinaryMode) && model.LogicalBuffers.Count > 1;
-
-            var imagesAsBufferViews = model.LogicalImages.Count > 0 && this.ImageWriting == ResourceWriteMode.BufferView;
-
-            if (needsMergeBuffers | imagesAsBufferViews)
-            {
-                if (_NoCloneWatchdog) throw new InvalidOperationException($"Current settings require creating a densive copy before model modification, but calling {nameof(MODEL.DeepClone)} is not allowed with the current settings.");
-                model = model.DeepClone();
-            }
-
-            if (ImageWriting == ResourceWriteMode.BufferView)
-            {
-                model.MergeImages();
-                needsMergeBuffers |= this.MergeBuffers | this.BinaryMode;
-            }
-
-            if (needsMergeBuffers) model.MergeBuffers();
-
-            return model;
+            Guard.NotNull(other, nameof(other));
+            this.ImageWriting = other.ImageWriting;
+            this.MergeBuffers = other.MergeBuffers;
+            this.JsonFormatting = other.JsonFormatting;
         }
 
         #endregion
@@ -225,72 +94,65 @@ namespace SharpGLTF.Schema2
 
     partial class ModelRoot
     {
+        #region save / write methods
+
         /// <summary>
         /// Writes this <see cref="MODEL"/> to a file in GLTF or GLB based on the extension of <paramref name="filePath"/>.
         /// </summary>
         /// <param name="filePath">A valid file path to write to.</param>
-        public void Save(string filePath)
+        /// <param name="settings">Optional settings.</param>
+        public void Save(string filePath, WriteSettings settings = null)
         {
-            Guard.FilePathMustBeValid(filePath, nameof(filePath));
+            Guard.NotNullOrEmpty(filePath, nameof(filePath));
 
             bool isGltfExtension = filePath
                 .ToLower(System.Globalization.CultureInfo.InvariantCulture)
                 .EndsWith(".gltf", StringComparison.OrdinalIgnoreCase);
 
-            if (isGltfExtension) SaveGLTF(filePath);
-            else SaveGLB(filePath);
+            if (isGltfExtension) SaveGLTF(filePath, settings);
+            else SaveGLB(filePath, settings);
         }
 
         /// <summary>
         /// Writes this <see cref="MODEL"/> to a file in GLB format.
         /// </summary>
         /// <param name="filePath">A valid file path to write to.</param>
-        public void SaveGLB(string filePath)
+        /// <param name="settings">Optional settings.</param>
+        public void SaveGLB(string filePath, WriteSettings settings = null)
         {
             Guard.FilePathMustBeValid(filePath, nameof(filePath));
 
-            var settings = WriteContext.ForBinary(filePath);
+            var context = IO.WriteContext
+                .CreateFromFile(filePath)
+                .WithBinarySettings();
+
+            if (settings != null) settings.CopyTo(context);
 
             var name = Path.GetFileNameWithoutExtension(filePath);
 
-            _Write(settings, name, this);
+            context.WriteBinarySchema2(name, this);
         }
 
         /// <summary>
         /// Writes this <see cref="MODEL"/> to a file in GLTF format.
         /// </summary>
         /// <param name="filePath">A valid file path to write to.</param>
-        /// <param name="fmt">The formatting of the JSON document.</param>
+        /// <param name="settings">Optional settings.</param>
         /// <remarks>
         /// Satellite files like buffers and images are also saved with the file name formatted as "FILE_{Index}.EXT".
         /// </remarks>
-        public void SaveGLTF(string filePath, Formatting fmt = Formatting.None)
+        public void SaveGLTF(string filePath, WriteSettings settings = null)
         {
             Guard.FilePathMustBeValid(filePath, nameof(filePath));
 
-            var settings = WriteContext.ForText(filePath);
+            var context = IO.WriteContext
+                .CreateFromFile(filePath);
 
-            settings.JsonFormatting = fmt;
+            if (settings != null) settings.CopyTo(context);
 
             var name = Path.GetFileNameWithoutExtension(filePath);
 
-            _Write(settings, name, this);
-        }
-
-        /// <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, BYTES> WriteToDictionary(string fileName)
-        {
-            var dict = new Dictionary<string, BYTES>();
-
-            var settings = WriteContext.ForText(dict);
-
-            _Write(settings, fileName, this);
-
-            return dict;
+            context.WriteTextSchema2(name, this);
         }
 
         /// <summary>
@@ -310,12 +172,13 @@ namespace SharpGLTF.Schema2
         /// <summary>
         /// Writes this <see cref="MODEL"/> to a <see cref="byte"/> array in GLB format.
         /// </summary>
+        /// <param name="settings">Optional settings.</param>
         /// <returns>A <see cref="byte"/> array containing a GLB file.</returns>
-        public BYTES WriteGLB()
+        public BYTES WriteGLB(WriteSettings settings = null)
         {
             using (var m = new MemoryStream())
             {
-                WriteGLB(m);
+                WriteGLB(m, settings);
 
                 return m.ToArraySegment();
             }
@@ -325,31 +188,29 @@ namespace SharpGLTF.Schema2
         /// Writes this <see cref="MODEL"/> to a <see cref="Stream"/> in GLB format.
         /// </summary>
         /// <param name="stream">A <see cref="Stream"/> open for writing.</param>
-        public void WriteGLB(Stream stream)
+        /// <param name="settings">Optional settings.</param>
+        public void WriteGLB(Stream stream, WriteSettings settings = null)
         {
             Guard.NotNull(stream, nameof(stream));
             Guard.IsTrue(stream.CanWrite, nameof(stream));
 
-            var settings = WriteContext.ForBinary(stream);
+            var context = IO.WriteContext.CreateFromStream(stream);
 
-            _Write(settings, "model", this);
-        }
+            if (settings != null)
+            {
+                settings.CopyTo(context);
+                context.MergeBuffers = true;
+                context.ImageWriting = ResourceWriteMode.Default;
+            }
 
-        /// <summary>
-        /// Writes this <see cref="MODEL"/> to the asset writer in <see cref="WriteContext"/> configuration.
-        /// </summary>
-        /// <param name="settings">A <see cref="WriteContext"/> to use to write the files.</param>
-        /// <param name="baseName">The base name to use for asset files.</param>
-        /// <remarks>
-        /// Satellite files like buffers and images are also written with the file name formatted as "FILE_{Index}.EXT".
-        /// </remarks>
-        public void Write(WriteContext settings, string baseName)
-        {
-            Guard.NotNull(settings, nameof(settings));
-            _Write(settings, baseName, this);
+            context.WriteBinarySchema2("model", this);
         }
 
-        private void _WriteJSON(TextWriter sw, Formatting fmt)
+        #endregion
+
+        #region core
+
+        internal void _WriteJSON(TextWriter sw, Formatting fmt)
         {
             using (var writer = new JsonTextWriter(sw))
             {
@@ -359,81 +220,54 @@ namespace SharpGLTF.Schema2
             }
         }
 
-        private static void _Write(WriteContext settings, string baseName, MODEL model)
+        internal void _PrepareBuffersForSatelliteWriting(IO.WriteContext context, string baseName)
         {
-            Guard.NotNull(settings, nameof(settings));
-            Guard.NotNullOrEmpty(baseName, nameof(baseName));
-            Guard.NotNull(model, nameof(model));
-
-            model = settings.FilterModel(model);
-
-            foreach (var img in model._images) if (!img._HasContent) throw new Validation.DataException(img, "Content is missing.");
-
-            if (settings._UpdateSupportedExtensions) model.UpdateExtensionsSupport();
-
-            if (settings.BinaryMode)
+            // setup all buffers to be written as satellite files
+            for (int i = 0; i < this._buffers.Count; ++i)
             {
-                var ex = glb.IsBinaryCompatible(model);
-                if (ex != null) throw ex;
-
-                // setup all buffers to be written internally
-                for (int i = 0; i < model._buffers.Count; ++i)
-                {
-                    var buffer = model._buffers[i];
-                    buffer._WriteToInternal();
-                }
+                var buffer = this._buffers[i];
+                var bname = this._buffers.Count != 1 ? $"{baseName}_{i}.bin" : $"{baseName}.bin";
+                buffer._WriteToSatellite(context, bname);
             }
-            else
+        }
+
+        internal void _PrepareBuffersForInternalWriting()
+        {
+            // setup all buffers to be written internally
+            for (int i = 0; i < this._buffers.Count; ++i)
             {
-                // setup all buffers to be written as satellite files
-                for (int i = 0; i < model._buffers.Count; ++i)
-                {
-                    var buffer = model._buffers[i];
-                    var bname = model._buffers.Count != 1 ? $"{baseName}_{i}.bin" : $"{baseName}.bin";
-                    buffer._WriteToSatellite(settings.FileWriter, bname);
-                }
+                var buffer = this._buffers[i];
+                buffer._WriteToInternal();
             }
+        }
+
+        internal void _PrepareImagesForWriting(IO.WriteContext context, string baseName, ResourceWriteMode rmode)
+        {
+            if (context.ImageWriting != ResourceWriteMode.Default) rmode = context.ImageWriting;
 
             // setup all images to be written to the appropiate location.
-            for (int i = 0; i < model._images.Count; ++i)
+            for (int i = 0; i < this._images.Count; ++i)
             {
-                var image = model._images[i];
+                var image = this._images[i];
 
-                if (settings.ImageWriting != ResourceWriteMode.SatelliteFile)
+                if (rmode != ResourceWriteMode.SatelliteFile)
                 {
                     image._WriteToInternal();
                 }
                 else
                 {
-                    var iname = model._images.Count != 1 ? $"{baseName}_{i}" : $"{baseName}";
-                    image._WriteToSatellite(settings.FileWriter, iname);
-                }
-            }
-
-            using (var m = new MemoryStream())
-            {
-                if (settings.BinaryMode)
-                {
-                    using (var w = new BinaryWriter(m))
-                    {
-                        glb.WriteBinaryModel(w, model);
-                    }
-
-                    settings.FileWriter($"{baseName}.glb", m.ToArraySegment());
-                }
-                else
-                {
-                    using (var w = new StreamWriter(m))
-                    {
-                        model._WriteJSON(w, settings.JsonFormatting);
-                    }
-
-                    settings.FileWriter($"{baseName}.gltf", m.ToArraySegment());
+                    var iname = this._images.Count != 1 ? $"{baseName}_{i}" : $"{baseName}";
+                    image._WriteToSatellite(context, iname);
                 }
             }
+        }
 
-            foreach (var b in model._buffers) b._ClearAfterWrite();
-            foreach (var i in model._images) i._ClearAfterWrite();
+        internal void _AfterWriting()
+        {
+            foreach (var b in this._buffers) b._ClearAfterWrite();
+            foreach (var i in this._images) i._ClearAfterWrite();
         }
+
+        #endregion
     }
 }

+ 1 - 1
tests/SharpGLTF.Tests/Utils.cs

@@ -124,7 +124,7 @@ namespace SharpGLTF
             }
             else if (fileName.ToLower().EndsWith(".gltf"))
             {
-                model.SaveGLTF(fileName, Newtonsoft.Json.Formatting.Indented);
+                model.Save(fileName, Newtonsoft.Json.Formatting.Indented);
             }
             else if (fileName.ToLower().EndsWith(".obj"))
             {