use std::io::{self, Seek, SeekFrom, Write};
use crate::fbx_ascii_syntax::FBX_VERSION;
use crate::fbx_node::{FbxNode, FbxProperty};
pub(crate) const FBX_MAGIC: &[u8; 21] = b"Kaydara FBX Binary \0";
const NULL_RECORD_SIZE_64: usize = 25;
const NULL_RECORD_SIZE_32: usize = 13;
const FBX_FOOTER_ID: [u8; 16] = [
0xFA, 0xBC, 0xAB, 0x09, 0xD0, 0xC8, 0xD4, 0x66, 0xB1, 0x76, 0xFB, 0x83, 0x1C, 0xF7, 0x26, 0x7E,
];
const FBX_FOOTER_MAGIC: [u8; 16] = [
0xF8, 0x5A, 0x8C, 0x6A, 0xDE, 0xF5, 0xD9, 0x7E, 0xEC, 0xE9, 0x0C, 0xE3, 0x75, 0x8F, 0x29, 0x0B,
];
pub(crate) struct WriterOptions {
pub(crate) compress: bool,
pub(crate) compression_threshold: usize,
}
impl Default for WriterOptions {
fn default() -> Self {
Self {
compress: false,
compression_threshold: 128,
}
}
}
pub(crate) fn encode_node<W: Write + Seek>(
writer: &mut W,
node: &FbxNode,
is_64: bool,
options: &WriterOptions,
) -> io::Result<()> {
let mut record = NodeWriter::start(writer, &node.name, is_64)?;
for property in &node.properties {
match property {
FbxProperty::Bool(value) => record.write_property_bool(*value)?,
FbxProperty::U8(value) => record.write_property_u8(*value)?,
FbxProperty::I16(value) => record.write_property_i16(*value)?,
FbxProperty::I32(value) => record.write_property_i32(*value)?,
FbxProperty::I64(value) => record.write_property_i64(*value)?,
FbxProperty::F32(value) => record.write_property_f32(*value)?,
FbxProperty::F64(value) => record.write_property_f64(*value)?,
FbxProperty::String(value) => record.write_property_string(value)?,
FbxProperty::Raw(value) => record.write_property_raw(value)?,
FbxProperty::BoolArray(values) => record.write_property_bool_array(values, options)?,
FbxProperty::I32Array(values) => record.write_property_i32_array(values, options)?,
FbxProperty::I64Array(values) => record.write_property_i64_array(values, options)?,
FbxProperty::F32Array(values) => record.write_property_f32_array(values, options)?,
FbxProperty::F64Array(values) => record.write_property_f64_array(values, options)?,
}
}
if node.children.is_empty() {
record.finish()
} else {
record.finish_with_children(|w| {
for child in &node.children {
encode_node(w, child, is_64, options)?;
}
Ok(())
})
}
}
struct NodeWriter<'a, W: Write + Seek> {
writer: &'a mut W,
start_pos: u64,
properties_start: u64,
num_properties: u64,
is_64: bool,
}
impl<'a, W: Write + Seek> NodeWriter<'a, W> {
fn start(writer: &'a mut W, name: &str, is_64: bool) -> io::Result<Self> {
let start_pos = writer.stream_position()?;
let header_size = if is_64 { 24 } else { 12 }; writer.write_all(&vec![0u8; header_size])?;
writer.write_all(&[name.len() as u8])?;
writer.write_all(name.as_bytes())?;
let properties_start = writer.stream_position()?;
Ok(Self {
writer,
start_pos,
properties_start,
num_properties: 0,
is_64,
})
}
fn write_property_bool(&mut self, value: bool) -> io::Result<()> {
self.writer.write_all(b"C")?;
self.writer.write_all(&[u8::from(value)])?;
self.num_properties += 1;
Ok(())
}
fn write_property_u8(&mut self, value: u8) -> io::Result<()> {
self.writer.write_all(b"Z")?;
self.writer.write_all(&[value])?;
self.num_properties += 1;
Ok(())
}
fn write_property_i16(&mut self, value: i16) -> io::Result<()> {
self.writer.write_all(b"Y")?;
self.writer.write_all(&value.to_le_bytes())?;
self.num_properties += 1;
Ok(())
}
fn write_property_i32(&mut self, value: i32) -> io::Result<()> {
self.writer.write_all(b"I")?;
self.writer.write_all(&value.to_le_bytes())?;
self.num_properties += 1;
Ok(())
}
fn write_property_i64(&mut self, value: i64) -> io::Result<()> {
self.writer.write_all(b"L")?;
self.writer.write_all(&value.to_le_bytes())?;
self.num_properties += 1;
Ok(())
}
fn write_property_f32(&mut self, value: f32) -> io::Result<()> {
self.writer.write_all(b"F")?;
self.writer.write_all(&value.to_le_bytes())?;
self.num_properties += 1;
Ok(())
}
fn write_property_f64(&mut self, value: f64) -> io::Result<()> {
self.writer.write_all(b"D")?;
self.writer.write_all(&value.to_le_bytes())?;
self.num_properties += 1;
Ok(())
}
fn write_property_string(&mut self, value: &str) -> io::Result<()> {
self.writer.write_all(b"S")?;
self.writer.write_all(&(value.len() as u32).to_le_bytes())?;
self.writer.write_all(value.as_bytes())?;
self.num_properties += 1;
Ok(())
}
fn write_property_bool_array(
&mut self,
values: &[bool],
options: &WriterOptions,
) -> io::Result<()> {
self.write_array_property(b'b', values, options, |v| vec![u8::from(*v)])
}
fn write_property_f64_array(
&mut self,
values: &[f64],
options: &WriterOptions,
) -> io::Result<()> {
self.write_array_property(b'd', values, options, |v| v.to_le_bytes().to_vec())
}
fn write_property_i32_array(
&mut self,
values: &[i32],
options: &WriterOptions,
) -> io::Result<()> {
self.write_array_property(b'i', values, options, |v| v.to_le_bytes().to_vec())
}
fn write_property_i64_array(
&mut self,
values: &[i64],
options: &WriterOptions,
) -> io::Result<()> {
self.write_array_property(b'l', values, options, |v| v.to_le_bytes().to_vec())
}
fn write_property_f32_array(
&mut self,
values: &[f32],
options: &WriterOptions,
) -> io::Result<()> {
self.write_array_property(b'f', values, options, |v| v.to_le_bytes().to_vec())
}
fn write_property_raw(&mut self, data: &[u8]) -> io::Result<()> {
self.writer.write_all(b"R")?;
self.writer.write_all(&(data.len() as u32).to_le_bytes())?;
self.writer.write_all(data)?;
self.num_properties += 1;
Ok(())
}
fn write_array_property<T, F>(
&mut self,
type_code: u8,
values: &[T],
options: &WriterOptions,
to_bytes: F,
) -> io::Result<()>
where
F: Fn(&T) -> Vec<u8>,
{
self.writer.write_all(&[type_code])?;
self.writer
.write_all(&(values.len() as u32).to_le_bytes())?;
let raw_data: Vec<u8> = values.iter().flat_map(&to_bytes).collect();
let raw_size = raw_data.len();
let should_compress = options.compress && raw_size >= options.compression_threshold;
#[cfg(feature = "compression")]
if should_compress {
use miniz_oxide::deflate::compress_to_vec_zlib;
let compressed = compress_to_vec_zlib(&raw_data, 6);
if compressed.len() < raw_size {
self.writer.write_all(&1u32.to_le_bytes())?; self.writer
.write_all(&(compressed.len() as u32).to_le_bytes())?;
self.writer.write_all(&compressed)?;
self.num_properties += 1;
return Ok(());
}
}
#[cfg(not(feature = "compression"))]
let _ = should_compress;
self.writer.write_all(&0u32.to_le_bytes())?; self.writer.write_all(&(raw_size as u32).to_le_bytes())?;
self.writer.write_all(&raw_data)?;
self.num_properties += 1;
Ok(())
}
fn finish(self) -> io::Result<()> {
write_null_record(self.writer, self.is_64)?;
self.finalize_header()
}
fn finish_with_children<F>(self, write_children: F) -> io::Result<()>
where
F: FnOnce(&mut W) -> io::Result<()>,
{
let properties_end = self.writer.stream_position()?;
let property_list_len = properties_end - self.properties_start;
write_children(self.writer)?;
write_null_record(self.writer, self.is_64)?;
let end_pos = self.writer.stream_position()?;
self.writer.seek(SeekFrom::Start(self.start_pos))?;
if self.is_64 {
self.writer.write_all(&end_pos.to_le_bytes())?;
self.writer.write_all(&self.num_properties.to_le_bytes())?;
self.writer.write_all(&property_list_len.to_le_bytes())?;
} else {
self.writer.write_all(&(end_pos as u32).to_le_bytes())?;
self.writer
.write_all(&(self.num_properties as u32).to_le_bytes())?;
self.writer
.write_all(&(property_list_len as u32).to_le_bytes())?;
}
self.writer.seek(SeekFrom::Start(end_pos))?;
Ok(())
}
fn finalize_header(self) -> io::Result<()> {
let end_pos = self.writer.stream_position()?;
let null_size = if self.is_64 {
NULL_RECORD_SIZE_64
} else {
NULL_RECORD_SIZE_32
};
let property_list_len = if self.num_properties > 0 {
end_pos - self.properties_start - null_size as u64
} else {
0u64
};
self.writer.seek(SeekFrom::Start(self.start_pos))?;
if self.is_64 {
self.writer.write_all(&end_pos.to_le_bytes())?;
self.writer.write_all(&self.num_properties.to_le_bytes())?;
self.writer.write_all(&property_list_len.to_le_bytes())?;
} else {
self.writer.write_all(&(end_pos as u32).to_le_bytes())?;
self.writer
.write_all(&(self.num_properties as u32).to_le_bytes())?;
self.writer
.write_all(&(property_list_len as u32).to_le_bytes())?;
}
self.writer.seek(SeekFrom::Start(end_pos))?;
Ok(())
}
}
pub(crate) fn write_null_record<W: Write>(writer: &mut W, is_64: bool) -> io::Result<()> {
let size = if is_64 {
NULL_RECORD_SIZE_64
} else {
NULL_RECORD_SIZE_32
};
writer.write_all(&vec![0u8; size])
}
pub(crate) fn write_footer<W: Write + Seek>(writer: &mut W) -> io::Result<()> {
writer.write_all(&FBX_FOOTER_ID)?;
writer.write_all(&[0u8; 4])?;
let position = writer.stream_position()?;
let mut padding = (16 - (position % 16)) % 16;
if padding == 0 {
padding = 16;
}
writer.write_all(&vec![0u8; padding as usize])?;
writer.write_all(&FBX_VERSION.to_le_bytes())?;
writer.write_all(&[0u8; 120])?;
writer.write_all(&FBX_FOOTER_MAGIC)?;
Ok(())
}
#[cfg(all(test, feature = "fbx-reader"))]
mod tests {
use super::*;
use crate::fbx_container::FbxMemoryReader;
use std::io::Cursor;
fn round_trip(nodes: &[FbxNode], options: &WriterOptions) -> Vec<FbxNode> {
let mut cursor = Cursor::new(Vec::new());
cursor.write_all(FBX_MAGIC).unwrap();
cursor.write_all(&[0x1A, 0x00]).unwrap();
cursor.write_all(&FBX_VERSION.to_le_bytes()).unwrap();
let is_64 = FBX_VERSION >= 7500;
for node in nodes {
encode_node(&mut cursor, node, is_64, options).unwrap();
}
write_null_record(&mut cursor, is_64).unwrap();
write_footer(&mut cursor).unwrap();
let mut reader = FbxMemoryReader::from_bytes(cursor.into_inner()).unwrap();
reader.read_nodes().unwrap()
}
#[test]
fn every_property_variant_survives_a_round_trip() {
let node = FbxNode {
name: "Everything".to_string(),
properties: vec![
FbxProperty::Bool(true),
FbxProperty::U8(200),
FbxProperty::I16(-3),
FbxProperty::I32(-70_000),
FbxProperty::I64(-5_000_000_000),
FbxProperty::F32(0.5),
FbxProperty::F64(-0.25),
FbxProperty::String("name\u{0}\u{1}Class".to_string()),
FbxProperty::Raw(vec![1, 2, 3]),
FbxProperty::BoolArray(vec![true, false, true]),
FbxProperty::I32Array(vec![1, -2, 3]),
FbxProperty::I64Array(vec![4, -5]),
FbxProperty::F32Array(vec![1.5, -2.5]),
FbxProperty::F64Array(vec![3.5, -4.5]),
],
children: Vec::new(),
};
let read = round_trip(std::slice::from_ref(&node), &WriterOptions::default());
assert_eq!(read.len(), 1);
assert_eq!(read[0].name, "Everything");
assert_eq!(
format!("{:?}", read[0].properties),
format!("{:?}", node.properties)
);
}
#[test]
fn a_compressed_array_decodes_to_the_same_values() {
let values: Vec<f64> = (0..512).map(f64::from).collect();
let node = FbxNode {
name: "Vertices".to_string(),
properties: vec![FbxProperty::F64Array(values.clone())],
children: Vec::new(),
};
let options = WriterOptions {
compress: true,
compression_threshold: 128,
};
let read = round_trip(std::slice::from_ref(&node), &options);
match &read[0].properties[0] {
FbxProperty::F64Array(decoded) => assert_eq!(decoded, &values),
other => panic!("expected an f64 array, got {other:?}"),
}
}
#[test]
fn nested_children_survive_the_backpatched_end_offset() {
let node = FbxNode {
name: "Objects".to_string(),
properties: Vec::new(),
children: vec![FbxNode {
name: "Geometry".to_string(),
properties: vec![FbxProperty::I64(42)],
children: vec![FbxNode {
name: "Vertices".to_string(),
properties: vec![FbxProperty::F64Array(vec![1.0, 2.0, 3.0])],
children: Vec::new(),
}],
}],
};
let read = round_trip(std::slice::from_ref(&node), &WriterOptions::default());
assert_eq!(read[0].children[0].name, "Geometry");
assert_eq!(read[0].children[0].children[0].name, "Vertices");
assert_eq!(format!("{:?}", read), format!("{:?}", vec![node]));
}
}