use std::ffi::OsString;
use std::fs::File;
use std::io;
use std::mem::size_of;
use std::path::PathBuf;
use gltf_json::Index;
use super::glue::{create_accessor, push_and_return_index, u32size, Lef32};
#[derive(Clone, Debug, PartialEq)]
#[allow(clippy::derive_partial_eq_without_eq)]
pub struct GltfDataDestination {
discard: bool,
maximum_inline_length: usize,
file_base_path: Option<PathBuf>,
}
impl GltfDataDestination {
#[cfg(test)]
pub const fn null() -> GltfDataDestination {
Self {
discard: true,
maximum_inline_length: 0,
file_base_path: None,
}
}
pub fn new(file_base_path: Option<PathBuf>, maximum_inline_length: usize) -> Self {
Self {
discard: false,
maximum_inline_length,
file_base_path,
}
}
pub fn write<F>(
&self,
buffer_entity_name: String,
file_suffix: &str,
contents_fn: F,
) -> io::Result<gltf_json::Buffer>
where
F: FnOnce(&mut dyn io::Write) -> io::Result<()>,
{
assert!(
!file_suffix.contains(['/', '\0', '%']),
"Invalid character in buffer file name {file_suffix:?}"
);
let mut implementation = if self.discard {
SwitchingWriter::Null { bytes_written: 0 }
} else if let Some(file_base_path) = &self.file_base_path {
let mut buffer_file_name: OsString = file_base_path.file_stem().unwrap().to_owned();
buffer_file_name.push(format!("-{file_suffix}.glbin"));
let relative_url = buffer_file_name
.to_str()
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"glTF file path must be valid UTF-8, but “{}” was not",
buffer_file_name.to_string_lossy()
),
)
})?
.to_string();
let mut buffer_file_path = file_base_path.clone();
buffer_file_path.set_file_name(&buffer_file_name);
SwitchingWriter::Memory {
buffer: Vec::new(),
limit: self.maximum_inline_length,
path: Some(buffer_file_path),
future_file_uri: Some(relative_url),
}
} else {
SwitchingWriter::Memory {
buffer: Vec::new(),
limit: self.maximum_inline_length,
path: None,
future_file_uri: None,
}
};
contents_fn(&mut implementation)?;
let (uri, byte_length) = implementation.close()?;
Ok(gltf_json::Buffer {
byte_length: u32size(byte_length),
name: Some(buffer_entity_name),
uri,
extensions: Default::default(),
extras: Default::default(),
})
}
}
#[derive(Debug)]
enum SwitchingWriter {
Null {
bytes_written: usize,
},
Memory {
buffer: Vec<u8>,
limit: usize,
future_file_uri: Option<String>,
path: Option<PathBuf>,
},
File {
file: io::BufWriter<File>,
bytes_written: usize,
file_uri: Option<String>,
},
}
impl SwitchingWriter {
fn close(self) -> io::Result<(Option<String>, usize)> {
match self {
SwitchingWriter::Null { bytes_written } => Ok((None, bytes_written)),
SwitchingWriter::Memory { buffer, .. } => {
let prefix = "data:application/gltf-buffer;base64,";
let mut url = String::with_capacity(prefix.len() + buffer.len() * 6 / 8 + 3);
url += prefix;
base64::encode_config_buf(&buffer, base64::STANDARD_NO_PAD, &mut url);
Ok((Some(url), buffer.len()))
}
SwitchingWriter::File {
bytes_written,
file,
file_uri,
..
} => {
let file = file.into_inner()?;
file.sync_all()?;
#[allow(clippy::drop_non_drop)]
drop(file);
Ok((file_uri, bytes_written))
}
}
}
}
impl io::Write for SwitchingWriter {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
match *self {
SwitchingWriter::Null {
ref mut bytes_written,
} => {
*bytes_written += bytes.len();
Ok(bytes.len())
}
SwitchingWriter::Memory {
ref mut buffer,
limit,
ref path,
ref future_file_uri,
} => {
let n = buffer.write(bytes)?;
if buffer.len() > limit {
let path = path.as_ref().ok_or_else(|| {
io::Error::new(
io::ErrorKind::Other,
format!("no destination was provided for glTF buffers > {limit} bytes"),
)
})?;
let file = File::create(path)?;
let mut new_writer = SwitchingWriter::File {
file: io::BufWriter::new(file),
bytes_written: 0,
file_uri: future_file_uri.clone(),
};
new_writer.write_all(buffer)?;
*self = new_writer;
}
Ok(n)
}
SwitchingWriter::File {
ref mut file,
ref mut bytes_written,
file_uri: _,
} => {
let n = file.write(bytes)?;
*bytes_written += n;
Ok(n)
}
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
SwitchingWriter::Null { .. } => Ok(()),
SwitchingWriter::Memory { .. } => Ok(()),
SwitchingWriter::File { file, .. } => file.flush(),
}
}
}
pub(crate) fn create_buffer_and_accessor<I, const COMPONENTS: usize>(
root: &mut gltf_json::Root,
dest: &mut GltfDataDestination,
name: String,
file_suffix: &str,
data_source: I,
) -> io::Result<Index<gltf_json::Accessor>>
where
I: IntoIterator<Item = [f32; COMPONENTS]> + Clone,
I::IntoIter: ExactSizeIterator,
[Lef32; COMPONENTS]: bytemuck::Pod,
{
let length = data_source.clone().into_iter().len();
let buffer = dest.write(name.clone(), file_suffix, |w| {
for item in data_source.clone() {
w.write_all(bytemuck::bytes_of(&item.map(Lef32::from)))?;
}
Ok(())
})?;
let buffer_index = push_and_return_index(&mut root.buffers, buffer);
let buffer_view = push_and_return_index(
&mut root.buffer_views,
gltf_json::buffer::View {
buffer: buffer_index,
byte_length: u32size(length * size_of::<[Lef32; COMPONENTS]>()),
byte_offset: None,
byte_stride: None,
name: Some(name.clone()),
target: None,
extensions: Default::default(),
extras: Default::default(),
},
);
let accessor_index = push_and_return_index(
&mut root.accessors,
create_accessor(name, buffer_view, 0, data_source),
);
Ok(accessor_index)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn discard() {
let d = GltfDataDestination::null();
let buffer_entity = d
.write("foo".into(), "bar", |w| w.write_all(&[1, 2, 3]))
.unwrap();
assert_eq!(buffer_entity.name, Some("foo".into()));
assert_eq!(buffer_entity.uri, None);
assert_eq!(buffer_entity.byte_length, 3);
}
#[test]
fn inline_only_success() {
let d = GltfDataDestination::new(None, usize::MAX);
let buffer_entity = d
.write("foo".into(), "bar", |w| w.write_all(&[1, 2, 255]))
.unwrap();
assert_eq!(buffer_entity.name, Some("foo".into()));
assert_eq!(
buffer_entity.uri.as_deref(),
Some("data:application/gltf-buffer;base64,AQL/") );
assert_eq!(buffer_entity.byte_length, 3);
}
#[test]
fn inline_only_failure() {
let d = GltfDataDestination::new(None, 1);
let error = d
.write("foo".into(), "bar", |w| w.write_all(&[1, 2, 255]))
.unwrap_err();
assert_eq!(
error.to_string(),
"no destination was provided for glTF buffers > 1 bytes"
);
}
#[test]
fn switch_to_file() {
let temp_dir = tempfile::tempdir().unwrap();
let mut file_base_path = temp_dir.path().to_owned();
file_base_path.push("basepath.gltf");
println!("Base path: {}", file_base_path.display());
let d = GltfDataDestination::new(Some(file_base_path), 3);
let buffer_entity = d
.write("foo".into(), "bar", |w| {
w.write_all(&[1, 2, 3])?;
w.write_all(&[4, 5, 6])?;
Ok(())
})
.unwrap();
assert_eq!(buffer_entity.name, Some("foo".into()));
assert_eq!(buffer_entity.uri.as_deref(), Some("basepath-bar.glbin"));
assert_eq!(buffer_entity.byte_length, 6);
}
}