Serialization.WriteContext.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using SharpGLTF.Memory;
  6. using BYTES = System.ArraySegment<byte>;
  7. using MODEL = SharpGLTF.Schema2.ModelRoot;
  8. namespace SharpGLTF.Schema2
  9. {
  10. /// <summary>
  11. /// Callback used for saving associated files of the current model.
  12. /// </summary>
  13. /// <param name="assetName">The asset relative path.</param>
  14. /// <param name="assetData">The file contents as a <see cref="byte"/> array.</param>
  15. public delegate void FileWriterCallback(String assetName, BYTES assetData);
  16. /// <summary>
  17. /// Callback to control the image writing behavior.
  18. /// </summary>
  19. /// <param name="context">The current model writing context.</param>
  20. /// <param name="assetName">The default gltf URI used to reference the image.</param>
  21. /// <param name="image">The image to write.</param>
  22. /// <returns>The final glTF URI. If it didn't change, return the value of <para name="assetName"/>.</returns>
  23. public delegate string ImageWriterCallback(WriteContext context, String assetName, MemoryImage image);
  24. /// <summary>
  25. /// Configuration settings for writing model files.
  26. /// </summary>
  27. public class WriteContext : WriteSettings
  28. {
  29. #region lifecycle
  30. public static WriteContext Create(FileWriterCallback fileCallback)
  31. {
  32. Guard.NotNull(fileCallback, nameof(fileCallback));
  33. var context = new WriteContext(fileCallback)
  34. {
  35. _UpdateSupportedExtensions = true
  36. };
  37. return context;
  38. }
  39. public static WriteContext CreateFromFile(string filePath)
  40. {
  41. Guard.FilePathMustBeValid(filePath, nameof(filePath));
  42. if (!Path.IsPathRooted(filePath)) filePath = Path.GetFullPath(filePath);
  43. var dir = Path.GetDirectoryName(filePath);
  44. return CreateFromDirectory(dir);
  45. }
  46. public static WriteContext CreateFromDirectory(string directoryPath)
  47. {
  48. Guard.DirectoryPathMustExist(directoryPath, nameof(directoryPath));
  49. var dinfo = new DirectoryInfo(directoryPath);
  50. void _saveFile(string rawUri, BYTES data)
  51. {
  52. var path = Uri.UnescapeDataString(rawUri);
  53. path = Path.Combine(dinfo.FullName, path);
  54. File.WriteAllBytes(path, data.ToUnderlayingArray());
  55. }
  56. var context = Create(_saveFile);
  57. context.ImageWriting = ResourceWriteMode.SatelliteFile;
  58. context.JsonIndented = true;
  59. context.CurrentDirectory = dinfo;
  60. return context;
  61. }
  62. public static WriteContext CreateFromDictionary(IDictionary<string, BYTES> dict)
  63. {
  64. Guard.NotNull(dict, nameof(dict));
  65. var context = Create((rawUri, data) => dict[rawUri] = data);
  66. context.ImageWriting = ResourceWriteMode.SatelliteFile;
  67. context.MergeBuffers = false;
  68. context.JsonIndented = false;
  69. return context;
  70. }
  71. public static WriteContext CreateFromStream(Stream stream)
  72. {
  73. Guard.NotNull(stream, nameof(stream));
  74. Guard.IsTrue(stream.CanWrite, nameof(stream));
  75. var context = Create((fn, d) => stream.Write(d.Array, d.Offset, d.Count));
  76. context.ImageWriting = ResourceWriteMode.Embedded;
  77. context.MergeBuffers = true;
  78. context.JsonIndented = false;
  79. return context.WithBinarySettings();
  80. }
  81. public WriteContext WithBinarySettings()
  82. {
  83. ImageWriting = ResourceWriteMode.BufferView;
  84. MergeBuffers = true;
  85. JsonIndented = false;
  86. return this;
  87. }
  88. public WriteContext WithSettingsFrom(WriteSettings settings)
  89. {
  90. settings?.CopyTo(this);
  91. return this;
  92. }
  93. /// <summary>
  94. /// These settings are used exclusively by <see cref="MODEL.DeepClone"/>.
  95. /// </summary>
  96. /// <returns>A <see cref="WriteContext"/> instance to be used by <see cref="MODEL.DeepClone()"/></returns>
  97. internal WriteContext WithDeepCloneSettings()
  98. {
  99. _UpdateSupportedExtensions = false;
  100. _NoCloneWatchdog = true;
  101. MergeBuffers = false;
  102. return this;
  103. }
  104. private WriteContext(FileWriterCallback fileCallback)
  105. {
  106. _FileWriter = fileCallback;
  107. }
  108. #endregion
  109. #region data
  110. private readonly FileWriterCallback _FileWriter;
  111. #endregion
  112. #region properties
  113. public System.IO.DirectoryInfo CurrentDirectory { get; private set; }
  114. /// <summary>
  115. /// Gets a value indicating whether to scan the whole model for used extensions.
  116. /// </summary>
  117. internal Boolean _UpdateSupportedExtensions { get; private set; } = true;
  118. /// <summary>
  119. /// Gets a value indicating whether creating a defensive copy before serialization is not allowed.
  120. /// </summary>
  121. internal bool _NoCloneWatchdog { get; private set; } = false;
  122. #endregion
  123. #region API
  124. public void WriteAllBytesToEnd(string fileName, BYTES data)
  125. {
  126. this._FileWriter(fileName, data);
  127. }
  128. public string WriteImage(string assetName, MemoryImage image)
  129. {
  130. var callback = this.ImageWriteCallback;
  131. if (callback == null) callback = (ctx, apath, img) => { ctx.WriteAllBytesToEnd(apath, img._GetBuffer()); return apath; };
  132. return callback(this, assetName, image);
  133. }
  134. /// <summary>
  135. /// Writes <paramref name="model"/> to this context using the glTF json container.
  136. /// </summary>
  137. /// <param name="baseName">The base name to use for asset files, without extension.</param>
  138. /// <param name="model">The <see cref="MODEL"/> to write.</param>
  139. /// <remarks>
  140. /// If the model has associated resources like binary assets and textures,<br/>
  141. /// these additional resources will be also written as associated files using the pattern:<br/>
  142. /// <br/>
  143. /// "<paramref name="baseName"/>.{Number}.bin|png|jpg|dds"
  144. /// </remarks>
  145. public void WriteTextSchema2(string baseName, MODEL model)
  146. {
  147. Guard.NotNullOrEmpty(baseName, nameof(baseName));
  148. Guard.NotNull(model, nameof(model));
  149. model = this._PreprocessSchema2(model, this.ImageWriting == ResourceWriteMode.BufferView, this.MergeBuffers, this.BuffersMaxSize);
  150. Guard.NotNull(model, nameof(model));
  151. model._PrepareBuffersForSatelliteWriting(this, baseName);
  152. model._PrepareImagesForWriting(this, baseName, ResourceWriteMode.SatelliteFile);
  153. _ValidateBeforeWriting(model);
  154. using (var m = new MemoryStream())
  155. {
  156. model._WriteJSON(m, this.JsonOptions, this.JsonPostprocessor);
  157. WriteAllBytesToEnd($"{baseName}.gltf", m.ToArraySegment());
  158. }
  159. model._AfterWriting();
  160. }
  161. /// <summary>
  162. /// Writes <paramref name="model"/> to this context using the GLB binary container.
  163. /// </summary>
  164. /// <param name="baseName">The base name to use for asset files, without extension.</param>
  165. /// <param name="model">The <see cref="MODEL"/> to write.</param>
  166. public void WriteBinarySchema2(string baseName, MODEL model)
  167. {
  168. Guard.NotNullOrEmpty(baseName, nameof(baseName));
  169. Guard.NotNull(model, nameof(model));
  170. model = this._PreprocessSchema2(model, this.ImageWriting == ResourceWriteMode.BufferView, true, int.MaxValue);
  171. Guard.NotNull(model, nameof(model));
  172. var ex = _BinarySerialization.IsBinaryCompatible(model);
  173. if (ex != null) throw ex;
  174. model._PrepareBuffersForInternalWriting();
  175. model._PrepareImagesForWriting(this, baseName, ResourceWriteMode.Embedded);
  176. _ValidateBeforeWriting(model);
  177. using (var m = new MemoryStream())
  178. {
  179. using (var w = new BinaryWriter(m))
  180. {
  181. _BinarySerialization.WriteBinaryModel(w, model);
  182. }
  183. WriteAllBytesToEnd($"{baseName}.glb", m.ToArraySegment());
  184. }
  185. model._AfterWriting();
  186. }
  187. #endregion
  188. #region core
  189. /// <summary>
  190. /// This needs to be called immediately before writing to json,
  191. /// but immediately after preprocessing and buffer setup, so the model can be correctly validated.
  192. /// </summary>
  193. /// <param name="model">The model to validate.</param>
  194. private void _ValidateBeforeWriting(MODEL model)
  195. {
  196. if (_NoCloneWatchdog) return;
  197. if (this.Validation == SharpGLTF.Validation.ValidationMode.Skip) return;
  198. var vcontext = new Validation.ValidationResult(model, this.Validation);
  199. model.ValidateReferences(vcontext.GetContext());
  200. var ex = vcontext.Errors.FirstOrDefault();
  201. if (ex != null) throw ex;
  202. model.ValidateContent(vcontext.GetContext());
  203. ex = vcontext.Errors.FirstOrDefault();
  204. if (ex != null) throw ex;
  205. }
  206. /// <summary>
  207. /// Prepares the model for writing with the appropiate settings, creating a defensive copy if neccesary.
  208. /// </summary>
  209. /// <param name="model">The source <see cref="MODEL"/> instance.</param>
  210. /// <param name="imagesAsBufferViews">true if images should be stored as buffer views.</param>
  211. /// <param name="mergeBuffers">true if it's required the model must have a single buffer.</param>
  212. /// <param name="buffersMaxSize">When merging buffers, the max buffer size</param>
  213. /// <returns>The source <see cref="MODEL"/> instance, or a cloned and modified instance if current settings required it.</returns>
  214. private MODEL _PreprocessSchema2(MODEL model, bool imagesAsBufferViews, bool mergeBuffers, int buffersMaxSize)
  215. {
  216. Guard.NotNull(model, nameof(model));
  217. foreach (var img in model.LogicalImages) if (!img._HasContent) throw new Validation.DataException(img, "Image Content is missing.");
  218. // check if we need to modify the model before saving it,
  219. // in order to create a defensive copy.
  220. if (model.LogicalImages.Count == 0) imagesAsBufferViews = false;
  221. if (model.LogicalBuffers.Count <= 1 && !imagesAsBufferViews) mergeBuffers = false;
  222. if (mergeBuffers | imagesAsBufferViews)
  223. {
  224. // cloning check is done to prevent cloning from entering in an infinite loop where each clone attempt triggers another clone request.
  225. 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.");
  226. model = model.DeepClone();
  227. }
  228. if (imagesAsBufferViews) model.MergeImages();
  229. if (mergeBuffers)
  230. {
  231. if (buffersMaxSize == int.MaxValue) model.MergeBuffers();
  232. else model.MergeBuffers(buffersMaxSize);
  233. }
  234. if (this._UpdateSupportedExtensions) model.UpdateExtensionsSupport();
  235. return model;
  236. }
  237. #endregion
  238. }
  239. }