diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index 5e8d1ad72b480..8fa4de3b66cc6 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -30,3 +30,4 @@ gltf = { version = "0.15.2", default-features = false, features = ["utils", "nam thiserror = "1.0" anyhow = "1.0" base64 = "0.13.0" +percent-encoding = "2.1" diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index 25da841eb3664..ca1090e4404bb 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -222,16 +222,29 @@ async fn load_gltf<'a, 'b>( Texture::from_buffer(buffer, ImageType::MimeType(mime_type))? } gltf::image::Source::Uri { uri, mime_type } => { - let parent = load_context.path().parent().unwrap(); - let image_path = parent.join(uri); - let bytes = load_context.read_asset_bytes(image_path.clone()).await?; + let uri = percent_encoding::percent_decode_str(uri) + .decode_utf8() + .unwrap(); + let uri = uri.as_ref(); + let (bytes, image_type) = match DataUri::parse(uri) { + Ok(data_uri) => (data_uri.decode()?, ImageType::MimeType(data_uri.mime_type)), + Err(()) => { + let parent = load_context.path().parent().unwrap(); + let image_path = parent.join(uri); + let bytes = load_context.read_asset_bytes(image_path.clone()).await?; + + let extension = Path::new(uri).extension().unwrap().to_str().unwrap(); + let image_type = ImageType::Extension(extension); + + (bytes, image_type) + } + }; + Texture::from_buffer( &bytes, mime_type .map(|mt| ImageType::MimeType(mt)) - .unwrap_or_else(|| { - ImageType::Extension(image_path.extension().unwrap().to_str().unwrap()) - }), + .unwrap_or(image_type), )? } }; @@ -569,23 +582,27 @@ async fn load_buffers( load_context: &LoadContext<'_>, asset_path: &Path, ) -> Result>, GltfError> { - const OCTET_STREAM_URI: &str = "data:application/octet-stream;base64,"; + const OCTET_STREAM_URI: &str = "application/octet-stream"; let mut buffer_data = Vec::new(); for buffer in gltf.buffers() { match buffer.source() { gltf::buffer::Source::Uri(uri) => { - if uri.starts_with("data:") { - buffer_data.push(base64::decode( - uri.strip_prefix(OCTET_STREAM_URI) - .ok_or(GltfError::BufferFormatUnsupported)?, - )?); - } else { - // TODO: Remove this and add dep - let buffer_path = asset_path.parent().unwrap().join(uri); - let buffer_bytes = load_context.read_asset_bytes(buffer_path).await?; - buffer_data.push(buffer_bytes); - } + let uri = percent_encoding::percent_decode_str(uri) + .decode_utf8() + .unwrap(); + let uri = uri.as_ref(); + let buffer_bytes = match DataUri::parse(uri) { + Ok(data_uri) if data_uri.mime_type == OCTET_STREAM_URI => data_uri.decode()?, + Ok(_) => return Err(GltfError::BufferFormatUnsupported), + Err(()) => { + // TODO: Remove this and add dep + let buffer_path = asset_path.parent().unwrap().join(uri); + let buffer_bytes = load_context.read_asset_bytes(buffer_path).await?; + buffer_bytes + } + }; + buffer_data.push(buffer_bytes); } gltf::buffer::Source::Bin => { if let Some(blob) = gltf.blob.as_deref() { @@ -646,6 +663,43 @@ fn resolve_node_hierarchy( .collect() } +struct DataUri<'a> { + mime_type: &'a str, + base64: bool, + data: &'a str, +} + +fn split_once(input: &str, delimiter: char) -> Option<(&str, &str)> { + let mut iter = input.splitn(2, delimiter); + Some((iter.next()?, iter.next()?)) +} + +impl<'a> DataUri<'a> { + fn parse(uri: &'a str) -> Result, ()> { + let uri = uri.strip_prefix("data:").ok_or(())?; + let (mime_type, data) = split_once(uri, ',').ok_or(())?; + + let (mime_type, base64) = match mime_type.strip_suffix(";base64") { + Some(mime_type) => (mime_type, true), + None => (mime_type, false), + }; + + Ok(DataUri { + mime_type, + base64, + data, + }) + } + + fn decode(&self) -> Result, base64::DecodeError> { + if self.base64 { + base64::decode(self.data) + } else { + Ok(self.data.as_bytes().to_owned()) + } + } +} + #[cfg(test)] mod test { use super::resolve_node_hierarchy;