Skip to content

Commit 85cc836

Browse files
committed
More robust JSON processing, add tests, fix error handling
1 parent 98cabce commit 85cc836

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2802
-347
lines changed

Source/.runsettings

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<RunSettings>
3+
<RunConfiguration>
4+
<ResultsDirectory>..\Build\Tests\Results</ResultsDirectory>
5+
</RunConfiguration>
6+
</RunSettings>
File renamed without changes.

Source/Core/Core.csproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<RootNamespace>glTF</RootNamespace>
8+
</PropertyGroup>
9+
10+
</Project>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Nodes;
3+
4+
namespace glTF
5+
{
6+
internal static class JsonExtensions
7+
{
8+
public static JsonArray? GetArray(this JsonNode node, string propertyName, JsonArray? defaultValue = null)
9+
{
10+
var propertyNode = node[propertyName];
11+
if (propertyNode == null || propertyNode.GetValueKind() != JsonValueKind.Array)
12+
{
13+
return defaultValue;
14+
}
15+
16+
return propertyNode.AsArray();
17+
}
18+
19+
public static int GetInt(this JsonNode node, string propertyName, int defaultValue = -1)
20+
{
21+
var propertyNode = node[propertyName];
22+
if (propertyNode == null || propertyNode.GetValueKind() != JsonValueKind.Number)
23+
{
24+
return defaultValue;
25+
}
26+
27+
return propertyNode.GetValue<int>();
28+
}
29+
30+
public static string? GetString(this JsonNode node, string propertyName, string? defaultValue = null)
31+
{
32+
var propertyNode = node[propertyName];
33+
if (propertyNode == null || propertyNode.GetValueKind() != JsonValueKind.String)
34+
{
35+
return defaultValue;
36+
}
37+
38+
return propertyNode.GetValue<string>();
39+
}
40+
41+
public static string? GetLocalPath(this JsonNode node, string propertyName, Uri baseUri, string? defaultValue = null)
42+
{
43+
var uriString = node.GetString(propertyName);
44+
if (uriString == null)
45+
{
46+
return defaultValue;
47+
}
48+
49+
if (!Uri.TryCreate(baseUri, uriString, out var uri) || !uri.IsFile)
50+
{
51+
return defaultValue;
52+
}
53+
54+
return uri.LocalPath;
55+
}
56+
57+
public static void Set<T>(this JsonNode jsonNode, string propertyName, T value)
58+
{
59+
jsonNode[propertyName] = JsonValue.Create(value);
60+
}
61+
62+
public static void Set<T>(this JsonNode jsonNode, string propertyName, T value, T defaultValue)
63+
{
64+
if (EqualityComparer<T>.Default.Equals(value, defaultValue))
65+
{
66+
jsonNode.AsObject().Remove(propertyName);
67+
}
68+
else
69+
{
70+
jsonNode[propertyName] = JsonValue.Create(value);
71+
}
72+
}
73+
74+
public static bool Remove(this JsonNode node, string propertyName)
75+
{
76+
if (node.GetValueKind() != JsonValueKind.Object)
77+
{
78+
return false;
79+
}
80+
81+
return node.AsObject().Remove(propertyName);
82+
}
83+
}
84+
}

Source/Core/MimeType.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
namespace glTF
2+
{
3+
internal class MimeType
4+
{
5+
public static string ToFileExtension(string? mimeType)
6+
{
7+
switch (mimeType)
8+
{
9+
case "image/png":
10+
return ".png";
11+
case "image/jpeg":
12+
return ".jpg";
13+
case "image/vnd-ms.dds":
14+
return ".dds";
15+
case "image/ktx2":
16+
return ".ktx2";
17+
case "image/webp":
18+
return ".webp";
19+
}
20+
21+
return ".bin";
22+
}
23+
24+
public static string FromFileExtension(string? fileExtension)
25+
{
26+
if (fileExtension != null)
27+
{
28+
switch (fileExtension.ToLower())
29+
{
30+
case ".png":
31+
return "image/png";
32+
case ".jpg":
33+
case ".jpeg":
34+
return "image/jpeg";
35+
case ".dds":
36+
return "image/vnd-ms.dds";
37+
case ".ktx2":
38+
return "image/ktx2";
39+
case ".webp":
40+
return "image/webp";
41+
}
42+
}
43+
44+
return "application/octet-stream";
45+
}
46+
}
47+
}

Source/Core/Packer.cs

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
using System.IO.MemoryMappedFiles;
2+
using System.Text.Json;
3+
using System.Text.Json.Nodes;
4+
5+
namespace glTF
6+
{
7+
public class Packer
8+
{
9+
private struct FileInfo
10+
{
11+
public MemoryMappedFile File;
12+
public int FileLength;
13+
public MemoryMappedViewStream Stream;
14+
public int ByteOffset;
15+
}
16+
17+
public static void Pack(string inputFilePath, string outputFilePath)
18+
{
19+
var baseUri = new Uri(inputFilePath);
20+
21+
var fileMap = new Dictionary<string, FileInfo>();
22+
var byteOffset = 0;
23+
24+
FileInfo AddFile(string filePath)
25+
{
26+
if (!fileMap.TryGetValue(filePath, out var fileInfo))
27+
{
28+
var file = MemoryMappedFile.CreateFromFile(filePath, FileMode.Open);
29+
var fileLength = Tools.GetFileLength(filePath);
30+
var stream = file.CreateViewStream(0, fileLength, MemoryMappedFileAccess.Read);
31+
fileInfo = new() { File = file, FileLength = fileLength, Stream = stream, ByteOffset = byteOffset };
32+
fileMap.Add(filePath, fileInfo);
33+
byteOffset = Tools.Align(byteOffset + fileLength);
34+
}
35+
36+
return fileInfo;
37+
}
38+
39+
JsonNode root;
40+
using (var stream = File.OpenRead(inputFilePath))
41+
{
42+
root = JsonNode.Parse(stream)!;
43+
}
44+
45+
var buffers = root.GetArray("buffers");
46+
var bufferViews = root.GetArray("bufferViews");
47+
48+
if (buffers != null)
49+
{
50+
for (var index = buffers.Count - 1; index >= 0; index--)
51+
{
52+
var buffer = buffers[index];
53+
if (buffer == null)
54+
{
55+
continue;
56+
}
57+
58+
var filePath = buffer.GetLocalPath("uri", baseUri);
59+
if (filePath == null)
60+
{
61+
continue;
62+
}
63+
64+
var fileInfo = AddFile(filePath);
65+
66+
if (bufferViews != null)
67+
{
68+
foreach (var bufferView in bufferViews)
69+
{
70+
if (bufferView == null)
71+
{
72+
continue;
73+
}
74+
75+
var bufferIndex = bufferView.GetInt("buffer");
76+
if (bufferIndex == -1)
77+
{
78+
continue;
79+
}
80+
81+
if (bufferIndex == index)
82+
{
83+
bufferView.Remove("buffer");
84+
bufferView.Set("byteOffset", fileInfo.ByteOffset + bufferView.GetInt("byteOffset", 0), 0);
85+
}
86+
else if (bufferIndex > index)
87+
{
88+
bufferView.Set("buffer", bufferIndex - 1);
89+
}
90+
}
91+
}
92+
93+
buffers.RemoveAt(index);
94+
}
95+
}
96+
97+
void ProcessArray(JsonArray array)
98+
{
99+
foreach (var element in array)
100+
{
101+
if (element == null)
102+
{
103+
continue;
104+
}
105+
106+
var filePath = element.GetLocalPath("uri", baseUri);
107+
if (filePath == null)
108+
{
109+
continue;
110+
}
111+
112+
var fileInfo = AddFile(filePath);
113+
114+
if (bufferViews == null)
115+
{
116+
bufferViews = [];
117+
root.Set("bufferViews", bufferViews);
118+
}
119+
120+
element.Remove("uri");
121+
element.Set("bufferView", bufferViews.Count);
122+
element.Set("mimeType", MimeType.FromFileExtension(Path.GetExtension(filePath)));
123+
124+
JsonNode bufferView = new JsonObject();
125+
bufferView.Set("byteOffset", fileInfo.ByteOffset, 0);
126+
bufferView.Set("byteLength", fileInfo.FileLength);
127+
bufferViews.Add(bufferView);
128+
}
129+
}
130+
131+
var images = root.GetArray("images");
132+
if (images != null)
133+
{
134+
ProcessArray(images);
135+
}
136+
137+
// EXT_lights_ies
138+
var lights = root["extensions"]?.AsObject()["EXT_lights_ies"]?.AsObject().GetArray("lights");
139+
if (lights != null)
140+
{
141+
ProcessArray(lights);
142+
}
143+
144+
if (fileMap.Count != 0)
145+
{
146+
if (buffers == null)
147+
{
148+
buffers = [];
149+
root.Set("buffers", buffers);
150+
}
151+
152+
JsonNode buffer = new JsonObject();
153+
buffer.Set("byteLength", byteOffset);
154+
buffers.Insert(0, buffer);
155+
156+
if (bufferViews != null)
157+
{
158+
foreach (var bufferView in bufferViews)
159+
{
160+
if (bufferView == null)
161+
{
162+
continue;
163+
}
164+
165+
var bufferIndex = bufferView.GetInt("buffer");
166+
bufferView["buffer"] = bufferIndex + 1;
167+
}
168+
}
169+
}
170+
171+
var outputDirectoryPath = Path.GetDirectoryName(outputFilePath);
172+
if (outputDirectoryPath != null)
173+
{
174+
Directory.CreateDirectory(outputDirectoryPath);
175+
}
176+
177+
using (var fileStream = File.Create(outputFilePath))
178+
using (var binaryWriter = new BinaryWriter(fileStream))
179+
{
180+
binaryWriter.Write(Binary.Magic);
181+
binaryWriter.Write(Binary.Version);
182+
183+
var chunksPosition = binaryWriter.BaseStream.Position;
184+
185+
binaryWriter.Write(0U); // length
186+
187+
var jsonChunkPosition = binaryWriter.BaseStream.Position;
188+
189+
binaryWriter.Write(0U); // json chunk length
190+
binaryWriter.Write(Binary.ChunkFormatJson);
191+
192+
using (var jsonTextWriter = new Utf8JsonWriter(binaryWriter.BaseStream))
193+
{
194+
root.WriteTo(jsonTextWriter);
195+
}
196+
197+
binaryWriter.BaseStream.Align(0x20);
198+
var jsonChunkLength = checked((uint)(binaryWriter.BaseStream.Length - jsonChunkPosition)) - Binary.ChunkHeaderLength;
199+
200+
binaryWriter.BaseStream.Seek(jsonChunkPosition, SeekOrigin.Begin);
201+
binaryWriter.Write(jsonChunkLength);
202+
203+
if (fileMap.Count != 0)
204+
{
205+
binaryWriter.BaseStream.Seek(0, SeekOrigin.End);
206+
var binChunkPosition = binaryWriter.BaseStream.Position;
207+
208+
binaryWriter.Write(0); // bin chunk length
209+
binaryWriter.Write(Binary.ChunkFormatBin);
210+
211+
foreach (var value in fileMap.Values)
212+
{
213+
binaryWriter.BaseStream.Align();
214+
value.Stream.CopyTo(binaryWriter.BaseStream);
215+
}
216+
217+
binaryWriter.BaseStream.Align(0x20);
218+
var binChunkLength = checked((uint)(binaryWriter.BaseStream.Length - binChunkPosition)) - Binary.ChunkHeaderLength;
219+
220+
binaryWriter.BaseStream.Seek(binChunkPosition, SeekOrigin.Begin);
221+
binaryWriter.Write(binChunkLength);
222+
}
223+
224+
var length = checked((uint)binaryWriter.BaseStream.Length);
225+
226+
binaryWriter.BaseStream.Seek(chunksPosition, SeekOrigin.Begin);
227+
binaryWriter.Write(length);
228+
}
229+
230+
foreach (var value in fileMap.Values)
231+
{
232+
value.Stream.Dispose();
233+
value.File.Dispose();
234+
}
235+
}
236+
}
237+
}

Source/glTF/Core/Tools.cs renamed to Source/Core/Tools.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using System.IO;
2-
3-
namespace glTF
1+
namespace glTF
42
{
53
internal static class Tools
64
{

0 commit comments

Comments
 (0)