Skip to content

Commit d119c1c

Browse files
gltf-loader: support data url for images (#1828)
This allows the `glTF-Embedded` variants in the [sample models](https://github.com/KhronosGroup/glTF-Sample-Models/) to be used. The data url format is relatively small, so I didn't include a crate like [docs.rs/data-url](https://docs.rs/data-url/0.1.0/data_url/). Also fixes the 'Box With Spaces' model as URIs are now percent-decoded. cc #1802
1 parent 04a37f7 commit d119c1c

File tree

2 files changed

+73
-18
lines changed

2 files changed

+73
-18
lines changed

crates/bevy_gltf/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@ gltf = { version = "0.15.2", default-features = false, features = ["utils", "nam
3030
thiserror = "1.0"
3131
anyhow = "1.0"
3232
base64 = "0.13.0"
33+
percent-encoding = "2.1"

crates/bevy_gltf/src/loader.rs

Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -229,16 +229,29 @@ async fn load_gltf<'a, 'b>(
229229
Texture::from_buffer(buffer, ImageType::MimeType(mime_type))?
230230
}
231231
gltf::image::Source::Uri { uri, mime_type } => {
232-
let parent = load_context.path().parent().unwrap();
233-
let image_path = parent.join(uri);
234-
let bytes = load_context.read_asset_bytes(image_path.clone()).await?;
232+
let uri = percent_encoding::percent_decode_str(uri)
233+
.decode_utf8()
234+
.unwrap();
235+
let uri = uri.as_ref();
236+
let (bytes, image_type) = match DataUri::parse(uri) {
237+
Ok(data_uri) => (data_uri.decode()?, ImageType::MimeType(data_uri.mime_type)),
238+
Err(()) => {
239+
let parent = load_context.path().parent().unwrap();
240+
let image_path = parent.join(uri);
241+
let bytes = load_context.read_asset_bytes(image_path.clone()).await?;
242+
243+
let extension = Path::new(uri).extension().unwrap().to_str().unwrap();
244+
let image_type = ImageType::Extension(extension);
245+
246+
(bytes, image_type)
247+
}
248+
};
249+
235250
Texture::from_buffer(
236251
&bytes,
237252
mime_type
238253
.map(|mt| ImageType::MimeType(mt))
239-
.unwrap_or_else(|| {
240-
ImageType::Extension(image_path.extension().unwrap().to_str().unwrap())
241-
}),
254+
.unwrap_or(image_type),
242255
)?
243256
}
244257
};
@@ -576,23 +589,27 @@ async fn load_buffers(
576589
load_context: &LoadContext<'_>,
577590
asset_path: &Path,
578591
) -> Result<Vec<Vec<u8>>, GltfError> {
579-
const OCTET_STREAM_URI: &str = "data:application/octet-stream;base64,";
592+
const OCTET_STREAM_URI: &str = "application/octet-stream";
580593

581594
let mut buffer_data = Vec::new();
582595
for buffer in gltf.buffers() {
583596
match buffer.source() {
584597
gltf::buffer::Source::Uri(uri) => {
585-
if uri.starts_with("data:") {
586-
buffer_data.push(base64::decode(
587-
uri.strip_prefix(OCTET_STREAM_URI)
588-
.ok_or(GltfError::BufferFormatUnsupported)?,
589-
)?);
590-
} else {
591-
// TODO: Remove this and add dep
592-
let buffer_path = asset_path.parent().unwrap().join(uri);
593-
let buffer_bytes = load_context.read_asset_bytes(buffer_path).await?;
594-
buffer_data.push(buffer_bytes);
595-
}
598+
let uri = percent_encoding::percent_decode_str(uri)
599+
.decode_utf8()
600+
.unwrap();
601+
let uri = uri.as_ref();
602+
let buffer_bytes = match DataUri::parse(uri) {
603+
Ok(data_uri) if data_uri.mime_type == OCTET_STREAM_URI => data_uri.decode()?,
604+
Ok(_) => return Err(GltfError::BufferFormatUnsupported),
605+
Err(()) => {
606+
// TODO: Remove this and add dep
607+
let buffer_path = asset_path.parent().unwrap().join(uri);
608+
let buffer_bytes = load_context.read_asset_bytes(buffer_path).await?;
609+
buffer_bytes
610+
}
611+
};
612+
buffer_data.push(buffer_bytes);
596613
}
597614
gltf::buffer::Source::Bin => {
598615
if let Some(blob) = gltf.blob.as_deref() {
@@ -653,6 +670,43 @@ fn resolve_node_hierarchy(
653670
.collect()
654671
}
655672

673+
struct DataUri<'a> {
674+
mime_type: &'a str,
675+
base64: bool,
676+
data: &'a str,
677+
}
678+
679+
fn split_once(input: &str, delimiter: char) -> Option<(&str, &str)> {
680+
let mut iter = input.splitn(2, delimiter);
681+
Some((iter.next()?, iter.next()?))
682+
}
683+
684+
impl<'a> DataUri<'a> {
685+
fn parse(uri: &'a str) -> Result<DataUri<'a>, ()> {
686+
let uri = uri.strip_prefix("data:").ok_or(())?;
687+
let (mime_type, data) = split_once(uri, ',').ok_or(())?;
688+
689+
let (mime_type, base64) = match mime_type.strip_suffix(";base64") {
690+
Some(mime_type) => (mime_type, true),
691+
None => (mime_type, false),
692+
};
693+
694+
Ok(DataUri {
695+
mime_type,
696+
base64,
697+
data,
698+
})
699+
}
700+
701+
fn decode(&self) -> Result<Vec<u8>, base64::DecodeError> {
702+
if self.base64 {
703+
base64::decode(self.data)
704+
} else {
705+
Ok(self.data.as_bytes().to_owned())
706+
}
707+
}
708+
}
709+
656710
#[cfg(test)]
657711
mod test {
658712
use super::resolve_node_hierarchy;

0 commit comments

Comments
 (0)