Repository: zeux/meshoptimizer (language: cpp)

Issue:
gltfpack: EXT_texture_webp support
Spec [here](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/EXT_texture_webp).

Model before:
[webp_demo.zip](https://github.com/zeux/meshoptimizer/files/10695236/webp_demo.zip)
```json
{
"textures":[{"sampler":0,"extensions":{"EXT_texture_webp":{"source":0}}}],
"extensionsUsed":["EXT_texture_webp"],"extensionsRequired":["EXT_texture_webp"]
}
```
Model after:
[webp_demo_gltfpack.zip](https://github.com/zeux/meshoptimizer/files/10695237/webp_demo_gltfpack.zip)
```json
{
"extensionsUsed":["KHR_mesh_quantization","KHR_texture_transform"],"extensionsRequired":["KHR_mesh_quantization"],
"textures":[{}]
}
```


Discussion / hints:
A similar thing is the proposal of [EXT_texture_avif](https://github.com/KhronosGroup/glTF/pull/2235). Or maybe gltfpack can just keep all unknown extensions of texture for maximum compatibility.
This requires https://github.com/jkuhlmann/cgltf/issues/248; I'm not very interested in implementing this myself and gltfpack can't encode webp images without external dependencies anyway so the best it can do here is to preserve inputs as is, but if the linked issue gets implemented I can take a look at this.

Keeping unknown extensions doesn't work because image files need to be parsed for material transparency optimization.

Fix patch:
diff --git a/extern/cgltf.h b/extern/cgltf.h
index ac392aba4..36fd644e1 100644
--- a/extern/cgltf.h
+++ b/extern/cgltf.h
@@ -395,6 +395,8 @@ typedef struct cgltf_texture
 	cgltf_sampler* sampler;
 	cgltf_bool has_basisu;
 	cgltf_image* basisu_image;
+	cgltf_bool has_webp;
+	cgltf_image* webp_image;
 	cgltf_extras extras;
 	cgltf_size extensions_count;
 	cgltf_extension* extensions;
@@ -4551,6 +4553,34 @@ static int cgltf_parse_json_texture(cgltf_options* options, jsmntok_t const* tok
 						}
 					}
 				}
+				else if (cgltf_json_strcmp(tokens + i, json_chunk, "EXT_texture_webp") == 0)
+				{
+					out_texture->has_webp = 1;
+					++i;
+					CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+					int num_properties = tokens[i].size;
+					++i;
+
+					for (int t = 0; t < num_properties; ++t)
+					{
+						CGLTF_CHECK_KEY(tokens[i]);
+
+						if (cgltf_json_strcmp(tokens + i, json_chunk, "source") == 0)
+						{
+							++i;
+							out_texture->webp_image = CGLTF_PTRINDEX(cgltf_image, cgltf_json_to_int(tokens + i, json_chunk));
+							++i;
+						}
+						else
+						{
+							i = cgltf_skip_json(tokens, i + 1);
+						}
+						if (i < 0)
+						{
+							return i;
+						}
+					}
+				}
 				else
 				{
 					i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_texture->extensions[out_texture->extensions_count++]));
@@ -6561,6 +6591,7 @@ static int cgltf_fixup_pointers(cgltf_data* data)
 	{
 		CGLTF_PTRFIXUP(data->textures[i].image, data->images, data->images_count);
 		CGLTF_PTRFIXUP(data->textures[i].basisu_image, data->images, data->images_count);
+		CGLTF_PTRFIXUP(data->textures[i].webp_image, data->images, data->images_count);
 		CGLTF_PTRFIXUP(data->textures[i].sampler, data->samplers, data->samplers_count);
 	}
 
diff --git a/gltf/README.md b/gltf/README.md
index ffe6d1e12..4b278676a 100644
--- a/gltf/README.md
+++ b/gltf/README.md
@@ -85,6 +85,7 @@ gltfpack supports most Khronos extensions and some multi-vendor extensions in th
 - KHR_texture_transform
 - EXT_mesh_gpu_instancing
 - EXT_meshopt_compression
+- EXT_texture_webp
 
 Even if the source file does not use extensions, gltfpack may use some extensions in the output file either by default or when certain options are used:
 
diff --git a/gltf/gltfpack.cpp b/gltf/gltfpack.cpp
index f3156d3e3..ee46fa6a7 100644
--- a/gltf/gltfpack.cpp
+++ b/gltf/gltfpack.cpp
@@ -454,6 +454,7 @@ static void process(cgltf_data* data, const char* input_path, const char* output
 	bool ext_instancing = false;
 	bool ext_texture_transform = false;
 	bool ext_texture_basisu = false;
+	bool ext_texture_webp = false;
 
 	size_t accr_offset = 0;
 	size_t node_offset = 0;
@@ -512,6 +513,7 @@ static void process(cgltf_data* data, const char* input_path, const char* output
 		assert(textures[i].remap == int(texture_offset));
 		texture_offset++;
 		ext_texture_basisu = ext_texture_basisu || texture.has_basisu;
+		ext_texture_webp = ext_texture_webp || texture.has_webp;
 	}
 
 	for (size_t i = 0; i < data->materials_count; ++i)
@@ -838,6 +840,7 @@ static void process(cgltf_data* data, const char* input_path, const char* output
 	    {"KHR_materials_variants", data->variants_count > 0, false},
 	    {"KHR_lights_punctual", data->lights_count > 0, false},
 	    {"KHR_texture_basisu", (!json_textures.empty() && settings.texture_ktx2) || ext_texture_basisu, true},
+	    {"EXT_texture_webp", ext_texture_webp, true},
 	    {"EXT_mesh_gpu_instancing", ext_instancing, true},
 	};
 
diff --git a/gltf/image.cpp b/gltf/image.cpp
index 129938a0e..972b4c441 100644
--- a/gltf/image.cpp
+++ b/gltf/image.cpp
@@ -8,6 +8,7 @@ static const char* kMimeTypes[][2] = {
     {"image/jpeg", ".jpeg"},
     {"image/png", ".png"},
     {"image/ktx2", ".ktx2"},
+    {"image/webp", ".webp"},
 };
 
 static const char* inferMimeType(const char* path)
@@ -128,6 +129,7 @@ static int readInt32LE(const std::string& data, size_t offset)
 	       (unsigned((unsigned char)data[offset + 3]) << 24);
 }
 
+// https://en.wikipedia.org/wiki/PNG#File_format
 static bool getDimensionsPng(const std::string& data, int& width, int& height)
 {
 	if (data.size() < 8 + 8 + 13 + 4)
@@ -146,6 +148,7 @@ static bool getDimensionsPng(const std::string& data, int& width, int& height)
 	return true;
 }
 
+// https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format#File_format_structure
 static bool getDimensionsJpeg(const std::string& data, int& width, int& height)
 {
 	size_t offset = 0;
@@ -189,6 +192,7 @@ static bool getDimensionsJpeg(const std::string& data, int& width, int& height)
 	return false;
 }
 
+// https://en.wikipedia.org/wiki/PNG#File_format
 static bool hasTransparencyPng(const std::string& data)
 {
 	if (data.size() < 8 + 8 + 13 + 4)
@@ -224,6 +228,7 @@ static bool hasTransparencyPng(const std::string& data)
 	return false;
 }
 
+// https://github.khronos.org/KTX-Specification/ktxspec.v2.html
 static bool hasTransparencyKtx2(const std::string& data)
 {
 	if (data.size() < 12 + 17 * 4)
@@ -261,12 +266,45 @@ static bool hasTransparencyKtx2(const std::string& data)
 	return false;
 }
 
+// https://developers.google.com/speed/webp/docs/riff_container
+static bool hasTransparencyWebP(const std::string& data)
+{
+	if (data.size() < 12 + 4 + 12)
+		return false;
+
+	if (data.compare(0, 4, "RIFF") != 0)
+		return false;
+	if (data.compare(8, 4, "WEBP") != 0)
+		return false;
+
+	// WebP data may use VP8L, VP8X or VP8 format, but VP8 does not support transparency
+	if (data.compare(12, 4, "VP8L") == 0)
+	{
+		if (unsigned(data[20]) != 0x2f)
+			return false;
+
+		// width (14) | height (14) | alpha_is_used (1) | version_number(3)
+		unsigned int header = readInt32LE(data, 21);
+		return (header & (1 << 28)) != 0;
+	}
+	else if (data.compare(12, 4, "VP8X") == 0)
+	{
+		// zero (2) | icc (1) | alpha (1) | exif (1) | xmp (1) | animation (1) | zero (1)
+		unsigned char header = data[20];
+		return (header & (1 << 4)) != 0;
+	}
+
+	return false;
+}
+
 bool hasAlpha(const std::string& data, const char* mime_type)
 {
 	if (strcmp(mime_type, "image/png") == 0)
 		return hasTransparencyPng(data);
 	else if (strcmp(mime_type, "image/ktx2") == 0)
 		return hasTransparencyKtx2(data);
+	else if (strcmp(mime_type, "image/webp") == 0)
+		return hasTransparencyWebP(data);
 	else
 		return false;
 }
diff --git a/gltf/material.cpp b/gltf/material.cpp
index 0931b2e05..68d5610c8 100644
--- a/gltf/material.cpp
+++ b/gltf/material.cpp
@@ -11,10 +11,10 @@ static bool areTexturesEqual(const cgltf_texture& lhs, const cgltf_texture& rhs)
 	if (lhs.sampler != rhs.sampler)
 		return false;
 
-	if (lhs.has_basisu != rhs.has_basisu)
+	if (lhs.basisu_image != rhs.basisu_image)
 		return false;
 
-	if (lhs.basisu_image != rhs.basisu_image)
+	if (lhs.webp_image != rhs.webp_image)
 		return false;
 
 	return true;
@@ -446,9 +446,12 @@ static const cgltf_image* getTextureImage(const cgltf_texture* texture)
 	if (texture && texture->image)
 		return texture->image;
 
-	if (texture && texture->has_basisu && texture->basisu_image)
+	if (texture && texture->basisu_image)
 		return texture->basisu_image;
 
+	if (texture && texture->webp_image)
+		return texture->webp_image;
+
 	return NULL;
 }
 
diff --git a/gltf/write.cpp b/gltf/write.cpp
index 13f2f3ad5..429d0c446 100644
--- a/gltf/write.cpp
+++ b/gltf/write.cpp
@@ -975,13 +975,20 @@ void writeTexture(std::string& json, const cgltf_texture& texture, const ImageIn
 		}
 	}
 
-	if (texture.has_basisu && texture.basisu_image)
+	if (texture.basisu_image)
 	{
 		comma(json);
 		append(json, "\"extensions\":{\"KHR_texture_basisu\":{\"source\":");
 		append(json, size_t(texture.basisu_image - data->images));
 		append(json, "}}");
 	}
+	else if (texture.webp_image)
+	{
+		comma(json);
+		append(json, "\"extensions\":{\"EXT_texture_webp\":{\"source\":");
+		append(json, size_t(texture.webp_image - data->images));
+		append(json, "}}");
+	}
 }
 
 void writeMeshAttributes(std::string& json, std::vector<BufferView>& views, std::string& json_accessors, size_t& accr_offset, const Mesh& mesh, int target, const QuantizationPosition& qp, const QuantizationTexture& qt, const Settings& settings)


Test patch: