Browse Source

Improving KTX2 image validation (still WIP)
Added some JSON methods to help parse unknown json data (WIP)
+bugfixes

Vicente Penades 5 years ago
parent
commit
6f850b2d11

+ 12 - 0
src/SharpGLTF.Core/IO/BinarySerialization.cs

@@ -20,6 +20,18 @@ namespace SharpGLTF.Schema2
 
         #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();
+            }
+        }
+
         public static bool IsBinaryHeader(Byte a, Byte b, Byte c, Byte d)
         {
             uint magic = 0;

+ 136 - 21
src/SharpGLTF.Core/IO/JsonCollections.cs

@@ -4,51 +4,166 @@ using System.Linq;
 using System.Text;
 using System.Text.Json;
 
+using JSONELEMENT = System.Text.Json.JsonElement;
+
 namespace SharpGLTF.IO
 {
-    static class JsonUtils
+    static class JsonValue
     {
-        public static Memory<Byte> ReadBytesToEnd(this System.IO.Stream s)
+        public static bool IsJsonSerializable(Object value, out Object invalidValue)
         {
-            using (var m = new System.IO.MemoryStream())
-            {
-                s.CopyTo(m);
-
-                if (m.TryGetBuffer(out ArraySegment<Byte> segment)) return segment;
-
-                return m.ToArray();
-            }
-        }
+            invalidValue = null;
 
-        public static bool IsJsonSerializable(Object value)
-        {
             if (value == null) return false;
 
             if (value is IConvertible cvt)
             {
                 var t = cvt.GetTypeCode();
-                if (t == TypeCode.Empty) return false;
-                if (t == TypeCode.DBNull) return false;
-                if (t == TypeCode.Object) return false;
-                if (t == TypeCode.DateTime) return false;
+                if (t == TypeCode.Empty) { invalidValue = value; return false; }
+                if (t == TypeCode.DBNull) { invalidValue = value; return false; }
+                if (t == TypeCode.Object) { invalidValue = value; return false; }
+                if (t == TypeCode.DateTime) { invalidValue = value; return false; }
                 return true;
             }
 
             if (value is JsonList list)
             {
-                return list.All(item => IsJsonSerializable(item));
+                foreach (var item in list)
+                {
+                    if (!IsJsonSerializable(item, out invalidValue)) return false;
+                }
+                return true;
             }
 
             if (value is JsonDictionary dict)
             {
-                return dict.Values.All(item => IsJsonSerializable(item));
+                foreach (var item in dict.Values)
+                {
+                    if (!IsJsonSerializable(item, out invalidValue)) return false;
+                }
+                return true;
             }
 
+            invalidValue = value;
             return false;
         }
+
+        public static bool IsJsonSerializable(Object value) { return IsJsonSerializable(value, out _); }
+
+        public static string SerializeToJson(Object value, System.Text.Json.JsonSerializerOptions options)
+        {
+            if (!IsJsonSerializable(value, out Object invalidValue)) throw new ArgumentException($"Found {invalidValue}, Expected Values, JsonList and JsonDictionary types allowed.", nameof(value));
+
+            if (options == null)
+            {
+                options = new System.Text.Json.JsonSerializerOptions
+                {
+                    PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
+                    IgnoreNullValues = true,
+                    WriteIndented = true
+                };
+            }
+
+            return System.Text.Json.JsonSerializer.Serialize(value, value.GetType(), options);
+        }
+
+        /// <summary>
+        /// Clones a json hierarchical object.
+        /// </summary>
+        /// <param name="value">An Iconvertible object, a List, or a Dictionary</param>
+        /// <returns>A cloned object</returns>
+        public static Object DeepClone(Object value)
+        {
+            if (value == null) throw new ArgumentNullException(nameof(value));
+
+            if (value is IConvertible cvt)
+            {
+                var t = cvt.GetTypeCode();
+                if (t == TypeCode.Empty) throw new ArgumentException($"Unexpected type {t}", nameof(value));
+                if (t == TypeCode.DBNull) throw new ArgumentException($"Unexpected type {t}", nameof(value));
+                if (t == TypeCode.Object) throw new ArgumentException($"Unexpected type {t}", nameof(value));
+                if (t == TypeCode.DateTime) throw new ArgumentException($"Unexpected type {t}", nameof(value));
+                return value;
+            }
+
+            if (value is IDictionary<string, Object> wadict) return new JsonDictionary(wadict);
+            if (value is IReadOnlyDictionary<string, Object> rodict) return new JsonDictionary(rodict);
+
+            if (value is IList<Object> walist) return new JsonList(walist);
+            if (value is IReadOnlyList<Object> rolist) return new JsonList(rolist);
+
+            throw new ArgumentException($"Unexpected type {value.GetType().Name}", nameof(value));
+        }
+
+        public static Object DeepParse(string json, JsonDocumentOptions options = default)
+        {
+            using (var doc = System.Text.Json.JsonDocument.Parse(json, options))
+            {
+                return DeepClone(doc);
+            }
+        }
+
+        public static Object DeepClone(System.Text.Json.JsonDocument doc)
+        {
+            return DeepClone(doc.RootElement);
+        }
+
+        public static Object DeepClone(JSONELEMENT element)
+        {
+            if (element.ValueKind == JsonValueKind.Null) return null;
+            if (element.ValueKind == JsonValueKind.False) return false;
+            if (element.ValueKind == JsonValueKind.True) return true;
+            if (element.ValueKind == JsonValueKind.String) return element.GetString();
+            if (element.ValueKind == JsonValueKind.Number) return element.GetRawText(); // use IConvertible interface when needed.
+            if (element.ValueKind == JsonValueKind.Array) return new JsonList(element);
+            if (element.ValueKind == JsonValueKind.Object) return new JsonDictionary(element);
+
+            throw new NotImplementedException();
+        }
     }
 
-    public class JsonList : List<Object> { }
+    public class JsonList : List<Object>
+    {
+        public JsonList() { }
+
+        internal JsonList(IEnumerable<Object> list)
+            : base(list) { }
+
+        internal JsonList(JSONELEMENT element)
+        {
+            if (element.ValueKind != JsonValueKind.Array) throw new ArgumentException("Must be JsonValueKind.Array", nameof(element));
 
-    public class JsonDictionary : Dictionary<String, Object> { }
+            foreach (var item in element.EnumerateArray())
+            {
+                var xitem = JsonValue.DeepClone(item);
+                this.Add(xitem);
+            }
+        }
+    }
+
+    public class JsonDictionary : Dictionary<String, Object>
+    {
+        public JsonDictionary() { }
+
+        internal JsonDictionary(IDictionary<String, Object> dict)
+            : base(dict) { }
+
+        internal JsonDictionary(IReadOnlyDictionary<String, Object> dict)
+        {
+            foreach (var kvp in dict)
+            {
+                this[kvp.Key] = kvp.Value;
+            }
+        }
+
+        internal JsonDictionary(JSONELEMENT element)
+        {
+            if (element.ValueKind != JsonValueKind.Object) throw new ArgumentException("Must be JsonValueKind.Object", nameof(element));
+
+            foreach (var item in element.EnumerateObject())
+            {
+                this[item.Name] = JsonValue.DeepClone(item.Value);
+            }
+        }
+    }
 }

+ 68 - 19
src/SharpGLTF.Core/Memory/MemoryImage.cs

@@ -240,7 +240,7 @@ namespace SharpGLTF.Memory
                 if (!string.IsNullOrWhiteSpace(_SourcePathHint)) return System.IO.Path.GetFileName(_SourcePathHint);
 
                 if (IsEmpty) return "Empty";
-                if (!IsValid) return $"Unknown {_Image.Count}ᴮʸᵗᵉˢ";
+                if (!_IsImage(_Image)) return $"Unknown {_Image.Count}ᴮʸᵗᵉˢ";
                 if (IsJpg) return $"JPG {_Image.Count}ᴮʸᵗᵉˢ";
                 if (IsPng) return $"PNG {_Image.Count}ᴮʸᵗᵉˢ";
                 if (IsDds) return $"DDS {_Image.Count}ᴮʸᵗᵉˢ";
@@ -254,6 +254,13 @@ namespace SharpGLTF.Memory
 
         #region API
 
+        public static void Verify(MemoryImage image, string paramName)
+        {
+            Guard.IsTrue(_IsImage(image._Image), paramName, $"{paramName} must be a valid image byte stream.");
+
+            if (image.IsKtx2) Ktx2Header.Verify(image._Image, paramName);
+        }
+
         /// <summary>
         /// Opens the image file for reading its contents
         /// </summary>
@@ -295,7 +302,7 @@ namespace SharpGLTF.Memory
         /// <returns>A mime64 string.</returns>
         internal string ToMime64(bool withPrefix = true)
         {
-            if (!this.IsValid) return null;
+            if (!_IsImage(_Image)) return null;
 
             var mimeContent = string.Empty;
             if (withPrefix)
@@ -343,7 +350,7 @@ namespace SharpGLTF.Memory
         {
             Guard.NotNullOrEmpty(format, nameof(format));
 
-            if (!IsValid) return false;
+            if (!_IsImage(_Image)) return false;
 
             if (format.EndsWith("png", StringComparison.OrdinalIgnoreCase)) return IsPng;
             if (format.EndsWith("jpg", StringComparison.OrdinalIgnoreCase)) return IsJpg;
@@ -405,22 +412,8 @@ namespace SharpGLTF.Memory
 
         private static bool _IsKtx2Image(IReadOnlyList<Byte> data)
         {
-            if (data[0] != 0xAB) return false;
-            if (data[1] != 0x4B) return false;
-            if (data[2] != 0x54) return false;
-            if (data[3] != 0x58) return false;
-
-            if (data[4] != 0x20) return false;
-            if (data[5] != 0x32) return false;
-            if (data[6] != 0x30) return false;
-            if (data[7] != 0xBB) return false;
-
-            if (data[8] != 0x0D) return false;
-            if (data[9] != 0x0A) return false;
-            if (data[10] != 0x1A) return false;
-            if (data[11] != 0x0A) return false;
-
-            return true;
+            if (!Ktx2Header.TryGetHeader(data, out Ktx2Header header)) return false;
+            return header.IsValidHeader;
         }
 
         private static bool _IsImage(IReadOnlyList<Byte> data)
@@ -439,4 +432,60 @@ namespace SharpGLTF.Memory
 
         #endregion
     }
+
+    readonly struct Ktx2Header
+    {
+        // http://github.khronos.org/KTX-Specification/
+
+        public readonly UInt64 Header0;
+        public readonly UInt32 Header1;
+
+        public readonly UInt32 vkFormat;
+        public readonly UInt32 typeSize;
+        public readonly UInt32 pixelWidth;
+        public readonly UInt32 pixelHeight;
+        public readonly UInt32 pixelDepth;
+        public readonly UInt32 layerCount;
+        public readonly UInt32 faceCount;
+        public readonly UInt32 levelCount;
+        public readonly UInt32 supercompressionScheme;
+
+        public static bool TryGetHeader(IReadOnlyList<Byte> data, out Ktx2Header header)
+        {
+            if (data.Count < 12) { header = default; return false; }
+            header = System.Runtime.InteropServices.MemoryMarshal.Cast<Byte, Ktx2Header>(data.ToArray())[0];
+            return true;
+        }
+
+        public bool IsValidHeader
+        {
+            get
+            {
+                if (Header0 != 0xbb30322058544BAb) return false;
+                if (Header1 != 0x0A1A0A0D) return false;
+                return true;
+            }
+        }
+
+        public static void Verify(IReadOnlyList<Byte> data, string paramName)
+        {
+            // https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_basisu#ktx-v2-images-with-basis-universal-supercompression
+
+            Guard.IsTrue(TryGetHeader(data, out Ktx2Header header), paramName);
+
+            // header must be valid
+            Guard.IsTrue(header.IsValidHeader, paramName + ".Header");
+
+            // pixelWidth and pixelHeight MUST be multiples of 4. 
+            Guard.MustBePositiveAndMultipleOf((int)header.pixelWidth, 4, $"{paramName}.{nameof(pixelWidth)}");
+            Guard.MustBePositiveAndMultipleOf((int)header.pixelHeight, 4, $"{paramName}.{nameof(pixelHeight)}");
+
+            // For 2D and cubemap textures, pixelDepth must be 0.
+            Guard.MustBeEqualTo((int)header.pixelDepth, 0, $"{paramName}.{nameof(pixelDepth)}");
+
+            Guard.MustBeLessThan((int)header.supercompressionScheme, 3, $"{paramName}.{nameof(supercompressionScheme)}");
+
+            // TODO: more checks required
+        }
+    }
 }

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

@@ -107,7 +107,7 @@ namespace SharpGLTF.Schema2
         /// <param name="content">A <see cref="Byte"/> array containing a PNG or JPEG image.</param>
         private void SetSatelliteContent(Memory.MemoryImage content)
         {
-            if (!content.IsValid) throw new ArgumentException($"{nameof(content)} must be a PNG, JPG, DDS, WEBP or KTX2 image", nameof(content));
+            Memory.MemoryImage.Verify(content, nameof(content));
 
             _DiscardContent();
 
@@ -187,7 +187,7 @@ namespace SharpGLTF.Schema2
             if (!_SatelliteContent.HasValue) { _WriteAsBufferView(); return; }
 
             var imimg = _SatelliteContent.Value;
-            if (!imimg.IsValid) throw new InvalidOperationException();
+            Memory.MemoryImage.Verify(imimg, nameof(imimg));
 
             _uri = imimg.ToMime64();
             _mimeType = imimg.MimeType;
@@ -207,7 +207,7 @@ namespace SharpGLTF.Schema2
             }
 
             var imimg = _SatelliteContent.Value;
-            if (!imimg.IsValid) throw new InvalidOperationException();
+            Memory.MemoryImage.Verify(imimg, nameof(imimg));
 
             satelliteUri = System.IO.Path.ChangeExtension(satelliteUri, imimg.FileExtension);
 
@@ -223,7 +223,7 @@ namespace SharpGLTF.Schema2
             Guard.IsTrue(_bufferView.HasValue, nameof(_bufferView));
 
             var imimg = this.Content;
-            if (!imimg.IsValid) throw new InvalidOperationException();
+            Memory.MemoryImage.Verify(imimg, nameof(imimg));
 
             _uri = null;
             _mimeType = imimg.MimeType;
@@ -262,7 +262,7 @@ namespace SharpGLTF.Schema2
                 validate.IsTrue("BufferView", bv.IsDataBuffer, "is a GPU target.");
             }
 
-            validate.IsTrue("MemoryImage", Content.IsValid, "Invalid image");
+            Memory.MemoryImage.Verify(Content, nameof(Content));
         }
 
         #endregion
@@ -293,7 +293,7 @@ namespace SharpGLTF.Schema2
         /// <returns>A <see cref="Image"/> instance.</returns>
         public Image UseImage(Memory.MemoryImage imageContent)
         {
-            Guard.IsTrue(imageContent.IsValid, nameof(imageContent), $"{nameof(imageContent)} must be a valid image byte stream.");
+            Memory.MemoryImage.Verify(imageContent, nameof(imageContent));
 
             // If we find an image with the same content, let's reuse it.
             foreach (var img in this.LogicalImages)

+ 2 - 1
src/SharpGLTF.Core/Schema2/gltf.MeshPrimitive.cs

@@ -383,7 +383,8 @@ namespace SharpGLTF.Schema2
                 .Where(item => item != null)
                 .Select(item => item.JointsCount);
 
-            var maxJoints = skins.Any() ? skins.Max() : 0;
+            // if no skins found, use a max joint value that essentially skips max joint validation
+            var maxJoints = skins.Any() ? skins.Max() : int.MaxValue;
 
             Accessor.ValidateVertexAttributes(validate, this.VertexAccessors, maxJoints);
         }

+ 6 - 0
src/SharpGLTF.Core/SharpGLTF.Core.csproj

@@ -7,6 +7,12 @@
     <LangVersion>7.3</LangVersion>    
   </PropertyGroup>
 
+  <PropertyGroup>
+    <!--
+    <DefineConstants>TRACE;SUPRESSTRYCATCH</DefineConstants>
+    -->
+  </PropertyGroup>
+
   <Import Project="..\PackageInfo.props" />
   <Import Project="..\Version.props" />
   <Import Project="..\Analyzers.props" />

+ 4 - 10
src/SharpGLTF.Core/Transforms/MeshTransforms.cs

@@ -305,6 +305,10 @@ namespace SharpGLTF.Transforms
 
         #region properties
 
+        public bool Visible => true;
+
+        public bool FlipFaces => false;
+
         /// <summary>
         /// Gets the collection of the current, final matrices to use for skinning
         /// </summary>
@@ -341,14 +345,8 @@ namespace SharpGLTF.Transforms
             }
         }
 
-        public bool Visible => true;
-
-        public bool FlipFaces => false;
-
         public V3 TransformPosition(V3 localPosition, IReadOnlyList<V3> morphTargets, in SparseWeight8 skinWeights)
         {
-            Guard.NotNull(skinWeights, nameof(skinWeights));
-
             localPosition = MorphVectors(localPosition, morphTargets);
 
             var worldPosition = V3.Zero;
@@ -365,8 +363,6 @@ namespace SharpGLTF.Transforms
 
         public V3 TransformNormal(V3 localNormal, IReadOnlyList<V3> morphTargets, in SparseWeight8 skinWeights)
         {
-            Guard.NotNull(skinWeights, nameof(skinWeights));
-
             localNormal = MorphVectors(localNormal, morphTargets);
 
             var worldNormal = V3.Zero;
@@ -381,8 +377,6 @@ namespace SharpGLTF.Transforms
 
         public V4 TransformTangent(V4 localTangent, IReadOnlyList<V3> morphTargets, in SparseWeight8 skinWeights)
         {
-            Guard.NotNull(skinWeights, nameof(skinWeights));
-
             var localTangentV = MorphVectors(new V3(localTangent.X, localTangent.Y, localTangent.Z), morphTargets);
 
             var worldTangent = V3.Zero;

+ 1 - 1
src/SharpGLTF.Core/Validation/ValidationContext.Guards.cs

@@ -144,7 +144,7 @@ namespace SharpGLTF.Validation
 
         public OUTTYPE IsJsonSerializable(PARAMNAME parameterName, Object value)
         {
-            if (!IO.JsonUtils.IsJsonSerializable(value)) _SchemaThrow(parameterName, "cannot be serialized to Json");
+            if (!IO.JsonValue.IsJsonSerializable(value)) _SchemaThrow(parameterName, "cannot be serialized to Json");
             return this;
         }
 

BIN
tests/Assets/FlightHelmet_baseColor_basis.ktx2


+ 1 - 1
tests/SharpGLTF.Tests/Schema2/Authoring/ExtensionsCreationTests.cs

@@ -164,7 +164,7 @@ namespace SharpGLTF.Schema2.Authoring
 
         [TestCase("shannon-dxt5.dds")]
         [TestCase("shannon.webp")]
-        [TestCase("CesiumLogoFlat.ktx2")]
+        [TestCase("FlightHelmet_baseColor_basis.ktx2")]
         public void CreateSceneWithTextureImageExtension(string textureFileName)
         {
             TestContext.CurrentContext.AttachShowDirLink();