Skip to content

Commit

Permalink
gltf-loader: support data url for images (bevyengine#1828)
Browse files Browse the repository at this point in the history
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 bevyengine#1802
  • Loading branch information
jakobhellermann authored and ostwilkens committed Jul 27, 2021
1 parent 11ade33 commit 100b7d5
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 18 deletions.
1 change: 1 addition & 0 deletions crates/bevy_gltf/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
90 changes: 72 additions & 18 deletions crates/bevy_gltf/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,16 +229,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),
)?
}
};
Expand Down Expand Up @@ -576,23 +589,27 @@ async fn load_buffers(
load_context: &LoadContext<'_>,
asset_path: &Path,
) -> Result<Vec<Vec<u8>>, 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() {
Expand Down Expand Up @@ -653,6 +670,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<DataUri<'a>, ()> {
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<Vec<u8>, 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;
Expand Down

0 comments on commit 100b7d5

Please sign in to comment.