diff --git a/inox2d/examples/copy-parameters.rs b/inox2d/examples/copy-parameters.rs new file mode 100644 index 00000000..d96aa046 --- /dev/null +++ b/inox2d/examples/copy-parameters.rs @@ -0,0 +1,103 @@ +use std::fs::File; +use std::io::{BufReader, Read}; +use std::path::PathBuf; + +use clap::Parser; +use inox2d::formats::inp::{parse_inp_parts, serialize_parts}; +use inox2d::formats::vendors::{SessionBinding, SESSION_BINDINGS_KEY}; +use inox2d::puppet::Puppet; +use std::collections::HashSet; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Cli { + #[arg(help = "The .inp to copy parameter mappings from")] + in_path: PathBuf, + + #[arg(help = "The .inp to copy parameter mappings to")] + out_path: PathBuf, +} + +fn main() { + let cli = Cli::parse(); + + let indata = { + let file = File::open(cli.in_path).unwrap(); + let mut file = BufReader::new(file); + let mut data = Vec::new(); + file.read_to_end(&mut data).unwrap(); + data + }; + + let (_in_puppet, _in_textures, in_vendors) = match parse_inp_parts(indata.as_slice()) { + Ok(m) => m, + Err(e) => { + eprintln!("Error when reading input puppet: {e}"); + return; + } + }; + + let outdata = { + let file = File::open(cli.out_path.clone()).unwrap(); + let mut file = BufReader::new(file); + let mut data = Vec::new(); + file.read_to_end(&mut data).unwrap(); + data + }; + + let (out_puppet, out_textures, mut out_vendors) = match parse_inp_parts(outdata.as_slice()) { + Ok(m) => m, + Err(e) => { + eprintln!("Error when reading input puppet: {e}"); + return; + } + }; + + // Bindings are treated as "vendor data" for some reason + let mut in_bindings = None; + for (index, item) in in_vendors.iter().enumerate() { + if item.name == SESSION_BINDINGS_KEY { + in_bindings = Some(index); + } + } + + let in_bindings = in_vendors + .get(in_bindings.expect("Input puppet has bindings data")) + .expect("Bindings data didn't change from prior lookup"); + + let mut out_bindings = None; + for (index, item) in out_vendors.iter().enumerate() { + if item.name == SESSION_BINDINGS_KEY { + out_bindings = Some(index); + } + } + + if out_bindings.is_some() { + eprintln!("Output puppet already has bindings data! Refusing to continue."); + return; + } + + // Validate that all params mentioned in in_bindings exists in out_puppet. + let out_puppet_data = Puppet::new_from_json(&out_puppet).expect("valid puppet JSON"); + let mut good_bindings = HashSet::new(); + for (_param_name, param) in out_puppet_data.params { + good_bindings.insert(param.uuid); + } + + let in_bindings_data = SessionBinding::new_from_json_list(&in_bindings.payload).expect("valid bindings data"); + for binding_block in in_bindings_data { + if !good_bindings.contains(&binding_block.param) { + eprintln!( + "Binding name {} refers to nonexistent param {:?}.", + binding_block.name, binding_block.param + ); + eprintln!("These puppets do not have compatible bindings and cannot have bindings copied."); + return; + } + } + + out_vendors.push(in_bindings.clone()); + + let file = File::create(cli.out_path).unwrap(); + serialize_parts(file, out_puppet, &out_textures, &out_vendors).unwrap(); +} diff --git a/inox2d/src/formats.rs b/inox2d/src/formats.rs index 932e694f..f176b83a 100644 --- a/inox2d/src/formats.rs +++ b/inox2d/src/formats.rs @@ -1,6 +1,7 @@ pub mod inp; mod json; mod payload; +pub mod vendors; use glam::Vec2; diff --git a/inox2d/src/formats/inp.rs b/inox2d/src/formats/inp.rs index 9041d71c..d9d2bebb 100644 --- a/inox2d/src/formats/inp.rs +++ b/inox2d/src/formats/inp.rs @@ -14,6 +14,8 @@ use super::json::JsonError; use super::payload::InoxParseError; use super::{read_be_u32, read_n, read_u8, read_vec}; +use json::JsonValue; + #[derive(Debug, thiserror::Error)] #[error("Could not parse INP file\n - {0}")] pub enum ParseInpError { @@ -40,8 +42,8 @@ const TEX_SECT: &[u8] = b"TEX_SECT"; /// Optional EXTended Vendor Data section for app provided settings for the puppet const EXT_SECT: &[u8] = b"EXT_SECT"; -/// Parse `.inp` and `.inx` files. -pub fn parse_inp(mut data: R) -> Result { +/// Parse `.inp` and `.inx` files into parts. +pub fn parse_inp_parts(mut data: R) -> Result<(JsonValue, Vec, Vec), ParseInpError> { // check magic bytes let magic = read_n::<_, 8>(&mut data)?; if magic != MAGIC { @@ -52,8 +54,7 @@ pub fn parse_inp(mut data: R) -> Result { let length = read_be_u32(&mut data)? as usize; let payload = read_vec(&mut data, length)?; let payload = std::str::from_utf8(&payload)?; - let payload = json::parse(payload)?; - let puppet = Puppet::new_from_json(&payload)?; + let puppet = json::parse(payload)?; // check texture section header let tex_sect = read_n::<_, 8>(&mut data).map_err(|_| ParseInpError::NoTexSect)?; @@ -101,6 +102,15 @@ pub fn parse_inp(mut data: R) -> Result { _ => Vec::new(), }; + Ok((puppet, textures, vendors)) +} + +/// Parse `.inp` and `.inx` files into a Model. +pub fn parse_inp(data: R) -> Result { + let (puppet_json, textures, vendors) = parse_inp_parts(data)?; + + let puppet = Puppet::new_from_json(&puppet_json)?; + Ok(Model { puppet, textures, @@ -108,7 +118,7 @@ pub fn parse_inp(mut data: R) -> Result { }) } -/// Parse `.inp` and `.inx` files. +/// Parse `.inp` and `.inx` files into an on-disk format. pub fn dump_inp(mut data: R, directory: &Path) -> Result<(), ParseInpError> { // check magic bytes let magic = read_n::<_, 8>(&mut data)?; @@ -188,6 +198,7 @@ pub fn dump_inp(mut data: R, directory: &Path) -> Result<(), ParseInpEr Ok(()) } +/// Serialize on-disk JSON and texture files into an INP file. pub fn dump_to_inp(directory: &Path, w: &mut W) -> io::Result<()> { let mut payload_file = File::open(directory.join("payload.json"))?; @@ -243,3 +254,58 @@ pub fn dump_to_inp(directory: &Path, w: &mut W) -> io::Result<()> { w.flush().unwrap(); Ok(()) } + +/// Serialize an INP1 file as parts. +/// +/// The parts taken by this function are equivalent to those specified in +/// `parse_inp_parts`. +pub fn serialize_parts( + mut file: W, + puppet: JsonValue, + textures: &[ModelTexture], + vendors: &[VendorData], +) -> Result<(), ParseInpError> { + file.write_all(MAGIC)?; + + let json = json::stringify(puppet).into_bytes(); + file.write_all(&(json.len() as u32).to_be_bytes())?; + file.write_all(&json)?; + + file.write_all(TEX_SECT)?; + + file.write_all(&(textures.len() as u32).to_be_bytes())?; + for texture in textures.iter() { + file.write_all(&(texture.data.len() as u32).to_be_bytes())?; + + file.write_all( + &(match texture.format { + ImageFormat::Png => 0, + ImageFormat::Tga => 1, + _ => return Err(ParseInpError::InvalidTexEncoding(0xFF)), //TODO: WriteInpError + } as u8) + .to_be_bytes(), + )?; + + file.write_all(&texture.data)?; + } + + if vendors.is_empty() { + //Don't write extended data if we don't have to. + return Ok(()); + } + + file.write_all(EXT_SECT)?; + file.write_all(&(vendors.len() as u32).to_be_bytes())?; + + for vendor in vendors.iter() { + let name = vendor.name.as_bytes(); + file.write_all(&(name.len() as u32).to_be_bytes())?; + file.write_all(&name)?; + + let json = json::stringify(vendor.payload.clone()).into_bytes(); + file.write_all(&(json.len() as u32).to_be_bytes())?; + file.write_all(&json)?; + } + + Ok(()) +} diff --git a/inox2d/src/formats/payload.rs b/inox2d/src/formats/payload.rs index 4b3bfb3a..de7b9b20 100644 --- a/inox2d/src/formats/payload.rs +++ b/inox2d/src/formats/payload.rs @@ -46,6 +46,8 @@ pub enum InoxParseError { OddNumberOfFloatsInList(usize), #[error("Expected 2 floats in list, got {0}")] Not2FloatsInList(usize), + #[error("Unknown vendor data value \"{1}\" for key {0}")] + UnknownVendorKeyValue(&'static str, String), } // json structure helpers @@ -63,14 +65,14 @@ fn vals(key: &str, res: InoxParseResult) -> InoxParseResult { res.map_err(|e| e.nested(key)) } -fn as_nested_list(index: usize, val: &json::JsonValue) -> InoxParseResult<&[json::JsonValue]> { +pub fn as_nested_list(index: usize, val: &json::JsonValue) -> InoxParseResult<&[json::JsonValue]> { match val { json::JsonValue::Array(arr) => Ok(arr), _ => Err(InoxParseError::JsonError(JsonError::ValueIsNotList(index.to_string()))), } } -fn as_object<'file>(msg: &str, val: &'file JsonValue) -> InoxParseResult> { +pub fn as_object<'file>(msg: &str, val: &'file JsonValue) -> InoxParseResult> { if let Some(obj) = val.as_object() { Ok(JsonObject(obj)) } else { diff --git a/inox2d/src/formats/vendors.rs b/inox2d/src/formats/vendors.rs new file mode 100644 index 00000000..e84509c7 --- /dev/null +++ b/inox2d/src/formats/vendors.rs @@ -0,0 +1,52 @@ +/// Additional payload support for known vendor blocks. +use crate::formats::json::JsonObject; +use crate::formats::payload::{as_nested_list, as_object, InoxParseError, InoxParseResult}; +use crate::params::{Binding, ParamUuid}; +use json::JsonValue; + +pub const SESSION_BINDINGS_KEY: &str = "com.inochi2d.inochi-session.bindings"; + +pub enum BindingType { + RatioBinding, + ExpressionBinding, +} + +pub struct SessionBinding<'file> { + pub name: &'file str, + pub source_name: &'file str, + pub source_display_name: &'file str, + pub source_type: &'file str, + pub binding_type: BindingType, + pub param: ParamUuid, + pub axis: u8, + pub dampen_level: f32, +} + +impl<'file> SessionBinding<'file> { + pub fn new_from_json_object(object: JsonObject<'file>) -> InoxParseResult { + Ok(Self { + name: object.get_str("name")?, + source_name: object.get_str("sourceName")?, + source_display_name: object.get_str("sourceDisplayName")?, + source_type: object.get_str("sourceType")?, + binding_type: match object.get_str("bindingType")? { + "RatioBinding" => BindingType::RatioBinding, + "ExpressionBinding" => BindingType::ExpressionBinding, + unknown => return Err(InoxParseError::UnknownVendorKeyValue("bindingType", unknown.to_owned())), + }, + param: ParamUuid(object.get_u32("param")?), + axis: object.get_u8("axis")?, + dampen_level: object.get_f32("dampenLevel")?, + }) + } + + pub fn new_from_json_list(value: &'file JsonValue) -> InoxParseResult> { + let mut out = vec![]; + + for (index, binding) in as_nested_list(0, value)?.iter().enumerate() { + out.push(Self::new_from_json_object(as_object(&format!("{}", index), binding)?)?); + } + + Ok(out) + } +} diff --git a/inox2d/src/puppet.rs b/inox2d/src/puppet.rs index ce67ff8d..e4325bca 100644 --- a/inox2d/src/puppet.rs +++ b/inox2d/src/puppet.rs @@ -26,7 +26,7 @@ pub struct Puppet { pub(crate) transform_ctx: Option, /// Context for rendering this puppet. See `.init_rendering()`. pub render_ctx: Option, - pub(crate) params: HashMap, + pub params: HashMap, /// Context for animating puppet with parameters. See `.init_params()` pub param_ctx: Option, }