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
This commit is contained in:
Jakob Hellermann 2021-04-13 21:30:32 +00:00
parent 04a37f722a
commit d119c1ce14
2 changed files with 73 additions and 18 deletions

View file

@ -30,3 +30,4 @@ gltf = { version = "0.15.2", default-features = false, features = ["utils", "nam
thiserror = "1.0" thiserror = "1.0"
anyhow = "1.0" anyhow = "1.0"
base64 = "0.13.0" base64 = "0.13.0"
percent-encoding = "2.1"

View file

@ -229,16 +229,29 @@ async fn load_gltf<'a, 'b>(
Texture::from_buffer(buffer, ImageType::MimeType(mime_type))? Texture::from_buffer(buffer, ImageType::MimeType(mime_type))?
} }
gltf::image::Source::Uri { uri, mime_type } => { gltf::image::Source::Uri { uri, mime_type } => {
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 parent = load_context.path().parent().unwrap();
let image_path = parent.join(uri); let image_path = parent.join(uri);
let bytes = load_context.read_asset_bytes(image_path.clone()).await?; 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( Texture::from_buffer(
&bytes, &bytes,
mime_type mime_type
.map(|mt| ImageType::MimeType(mt)) .map(|mt| ImageType::MimeType(mt))
.unwrap_or_else(|| { .unwrap_or(image_type),
ImageType::Extension(image_path.extension().unwrap().to_str().unwrap())
}),
)? )?
} }
}; };
@ -576,23 +589,27 @@ async fn load_buffers(
load_context: &LoadContext<'_>, load_context: &LoadContext<'_>,
asset_path: &Path, asset_path: &Path,
) -> Result<Vec<Vec<u8>>, GltfError> { ) -> 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(); let mut buffer_data = Vec::new();
for buffer in gltf.buffers() { for buffer in gltf.buffers() {
match buffer.source() { match buffer.source() {
gltf::buffer::Source::Uri(uri) => { gltf::buffer::Source::Uri(uri) => {
if uri.starts_with("data:") { let uri = percent_encoding::percent_decode_str(uri)
buffer_data.push(base64::decode( .decode_utf8()
uri.strip_prefix(OCTET_STREAM_URI) .unwrap();
.ok_or(GltfError::BufferFormatUnsupported)?, let uri = uri.as_ref();
)?); let buffer_bytes = match DataUri::parse(uri) {
} else { 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 // TODO: Remove this and add dep
let buffer_path = asset_path.parent().unwrap().join(uri); let buffer_path = asset_path.parent().unwrap().join(uri);
let buffer_bytes = load_context.read_asset_bytes(buffer_path).await?; let buffer_bytes = load_context.read_asset_bytes(buffer_path).await?;
buffer_data.push(buffer_bytes); buffer_bytes
} }
};
buffer_data.push(buffer_bytes);
} }
gltf::buffer::Source::Bin => { gltf::buffer::Source::Bin => {
if let Some(blob) = gltf.blob.as_deref() { if let Some(blob) = gltf.blob.as_deref() {
@ -653,6 +670,43 @@ fn resolve_node_hierarchy(
.collect() .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)] #[cfg(test)]
mod test { mod test {
use super::resolve_node_hierarchy; use super::resolve_node_hierarchy;