From 04f946d17cbf7232e874bebbfcf8967e4e72fb07 Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Tue, 2 Jan 2024 16:27:51 -0800 Subject: [PATCH 1/7] Recursive Operation Returns + Reconstruction Mode Refactors operation return handling into its own proc so it can recurse. I don't like just packing all my returns into the same type, thought it would be nicer if returns could like "compose" each other IDK if this breaks some optimization, if it does I can undo it Implements StringMap for mapping strings to strings in configs Implements ConfigWrapped, which wraps some other output with a toml config. Implements BitmaskSliceReconstruct, a config mode that will do its damndest to construct a working png and toml file from a cut dmi. It's not perfect, but it is pretty powerful. Knows how to extract prefixes, delays, and the icon size variables. --- examples/bitmask-slice-restore.toml | 18 ++ hypnagogic_cli/src/main.rs | 98 +++++--- hypnagogic_core/src/config/blocks/cutters.rs | 54 ++++- hypnagogic_core/src/operations/error.rs | 2 + .../format_converter/bitmask_to_precut.rs | 215 ++++++++++++++++++ hypnagogic_core/src/operations/mod.rs | 50 ++++ 6 files changed, 399 insertions(+), 38 deletions(-) create mode 100644 examples/bitmask-slice-restore.toml diff --git a/examples/bitmask-slice-restore.toml b/examples/bitmask-slice-restore.toml new file mode 100644 index 0000000..60dffab --- /dev/null +++ b/examples/bitmask-slice-restore.toml @@ -0,0 +1,18 @@ +# Bitmask restoration! +# Allows for easy mass extraction of template pngs and their configs from a dmi +# Use this if you have a dmi and you want a cutter config you can edit easily +# Of note, while it tries its best it is nowhere near perfect. We don't parity check against the existing dmi +# And we also do not account for overrided states very well +# Always double check (and be aware that dmi is weird so you may get diffs of 1 rgb value when doin this) +mode = "BitmaskSliceReconstruct" +# List of icon states to pull out +extract = ["0", "3", "12", "15", "255"] + +# Map of name -> state that will be encoded into a positions list later +# Lets you extract particular states and use them to fill in for states later +# Useful to carry over odd snowflake states +#[bespoke] + +# Map of key -> value to set on the created config +# Lets you set arbitrary values on the created config, mostly useful for batch processing +#[set] diff --git a/hypnagogic_cli/src/main.rs b/hypnagogic_cli/src/main.rs index d55dbe2..66a3cd7 100644 --- a/hypnagogic_cli/src/main.rs +++ b/hypnagogic_cli/src/main.rs @@ -18,8 +18,10 @@ use hypnagogic_core::operations::{ InputIcon, NamedIcon, OperationMode, + Output, OutputImage, - ProcessorPayload, + OutputText, + ProcessorPayload, }; use rayon::prelude::*; use tracing::{debug, info, Level}; @@ -250,6 +252,47 @@ fn process_icon( fs::create_dir_all(output_path)?; } + let out_paths: Vec<(PathBuf, Output)> = handle_payload(out, input_icon_path, output, flatten); + + for (mut path, output) in out_paths { + let parent_dir = path.parent().expect( + "Failed to get parent? (this is a program error, not a config error! Please report!)", + ); + + fs::create_dir_all(parent_dir).expect( + "Failed to create dirs (This is a program error, not a config error! Please report!)", + ); + + let mut file = File::create(path.as_path()).expect( + "Failed to create output file (This is a program error, not a config error! Please \ + report!)", + ); + + // TODO: figure out a better thing to do than just the unwrap + match output { + Output::Image(icon) => match icon { + OutputImage::Png(png) => { + png.save(&mut path).unwrap(); + } + OutputImage::Dmi(dmi) => { + dmi.save(&mut file).unwrap(); + } + } + Output::Text(text) => match text { + OutputText::PngConfig(config) | OutputText::DmiConfig(config) => { + fs::write(path, config).expect( + "Failed to write config text, (This is a program error, not a config error! Please \ + report!)") + } + } + } + } + Ok(()) +} + +#[allow(clippy::result_large_err)] +fn handle_payload(payload: ProcessorPayload, input_path: PathBuf, output_at: &Option, flatten: bool) -> Vec<(PathBuf, Output)> { + let mut out_paths: Vec<(PathBuf, Output)> = vec![]; let process_path = |path: PathBuf, named_img: Option<&NamedIcon>| -> PathBuf { debug!(path = ?path, img = ?named_img, "Processing path"); let processed_path = if let Some(named_img) = named_img { @@ -263,7 +306,7 @@ fn process_icon( let mut path = PathBuf::new(); - if let Some(output) = &output { + if let Some(output) = &output_at { path = PathBuf::from(output).join(&path); } @@ -272,55 +315,36 @@ fn process_icon( } path.push(processed_path); info!(path = ?path, "Processed path"); - path }; - let mut out_paths: Vec<(PathBuf, OutputImage)> = vec![]; - - match out { + match payload { ProcessorPayload::Single(inner) => { - let mut processed_path = process_path(input_icon_path.clone(), None); + let mut processed_path = process_path(input_path.clone(), None); processed_path.set_extension(inner.extension()); - out_paths.push((processed_path, *inner)); + out_paths.push((processed_path, Output::Image(*inner))); } ProcessorPayload::SingleNamed(named) => { - let mut processed_path = process_path(input_icon_path.clone(), Some(&named)); + let mut processed_path = process_path(input_path.clone(), Some(&named)); processed_path.set_extension(named.image.extension()); - out_paths.push((processed_path, named.image)) + out_paths.push((processed_path, Output::Image(named.image))) } ProcessorPayload::MultipleNamed(icons) => { for icon in icons { - let mut processed_path = process_path(input_icon_path.clone(), Some(&icon)); + let mut processed_path = process_path(input_path.clone(), Some(&icon)); processed_path.set_extension(icon.image.extension()); - out_paths.push((processed_path, icon.image)) + out_paths.push((processed_path, Output::Image(icon.image))) } } - } - - for (mut path, icon) in out_paths { - let parent_dir = path.parent().expect( - "Failed to get parent? (this is a program error, not a config error! Please report!)", - ); - - fs::create_dir_all(parent_dir).expect( - "Failed to create dirs (This is a program error, not a config error! Please report!)", - ); - - let mut file = File::create(path.as_path()).expect( - "Failed to create output file (This is a program error, not a config error! Please \ - report!)", - ); - - // TODO: figure out a better thing to do than just the unwrap - match icon { - OutputImage::Png(png) => { - png.save(&mut path).unwrap(); - } - OutputImage::Dmi(dmi) => { - dmi.save(&mut file).unwrap(); - } + ProcessorPayload::ConfigWrapped(payload, config_text) => { + // First, we'll pack in our config + let mut processed_path = process_path(input_path.clone(), None); + processed_path.set_extension(config_text.extension()); + out_paths.push((processed_path, Output::Text(*config_text))); + // Then we recurse and handle the enclosed payload + let mut contained = handle_payload(*payload, input_path, output_at, flatten); + out_paths.append(&mut contained); } } - Ok(()) + out_paths } diff --git a/hypnagogic_core/src/config/blocks/cutters.rs b/hypnagogic_core/src/config/blocks/cutters.rs index 74198c8..1c1bb7a 100644 --- a/hypnagogic_core/src/config/blocks/cutters.rs +++ b/hypnagogic_core/src/config/blocks/cutters.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; use fixed_map::Map; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -104,6 +104,58 @@ impl Default for Positions { } } +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct StringMap(pub HashMap); + +impl StringMap { + #[must_use] + pub fn get(&self, key: &str) -> Option<&String> { + self.0.get(key.clone()) + } +} + +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +#[serde(transparent)] +struct StringMapHelper { + map: HashMap, +} + +impl Serialize for StringMap { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = HashMap::new(); + + for (k, v) in self.0.iter() { + map.insert(k.clone(), v.clone()); + } + + StringMapHelper { map }.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for StringMap { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Deserialize::deserialize(deserializer).map(|StringMapHelper { map }| { + let mut result = HashMap::new(); + for (k, v) in map { + result.insert(k, v); + } + StringMap(result) + }) + } +} + +impl Default for StringMap { + fn default() -> Self { + StringMap(HashMap::new()) + } +} + #[derive(Clone, Eq, PartialEq, Debug, Default)] pub struct Prefabs(pub BTreeMap); diff --git a/hypnagogic_core/src/operations/error.rs b/hypnagogic_core/src/operations/error.rs index f69b9db..b9be4fd 100644 --- a/hypnagogic_core/src/operations/error.rs +++ b/hypnagogic_core/src/operations/error.rs @@ -6,6 +6,8 @@ pub enum ProcessorError { FormatError(String), #[error("Error processing image:\n{0}")] ImageError(#[from] image::error::ImageError), + #[error("Error restoring dmi:\n{0}")] + DmiError(String), #[error("Error generating icon for processor:\n{0}")] GenerationError(#[from] crate::generation::error::GenerationError), #[error("Error within image config:")] diff --git a/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs b/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs index 8b13789..863f59f 100644 --- a/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs +++ b/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs @@ -1 +1,216 @@ +use dmi::icon::IconState; +use image::{DynamicImage, GenericImage}; +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; +use tracing::debug; +use crate::config::blocks::cutters::{ + OutputIconPosition, + StringMap, +}; +use crate::operations::error::{ProcessorError, ProcessorResult}; +use crate::operations::{ + IconOperationConfig, + InputIcon, + OperationMode, + ProcessorPayload, +}; + +#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)] +pub struct BitmaskSliceReconstruct { + // List of icon states to extract + pub extract: Vec, + // Map of state name -> state to insert as + pub bespoke: Option, + // Map of key -> value to set on the created config + // Exists to let you set arbitrary values + pub set: Option, +} + +impl IconOperationConfig for BitmaskSliceReconstruct { + #[tracing::instrument(skip(input))] + fn perform_operation( + &self, + input: &InputIcon, + mode: OperationMode, + ) -> ProcessorResult { + debug!("Starting bitmask slice reconstruction"); + let InputIcon::Dmi(icon) = input else { + return Err(ProcessorError::FormatError("This operation only accepts dmis".to_string())); + }; + + // First, pull out icon states from DMI + let states = icon.states.clone(); + + let bespoke = match self.bespoke.as_ref() { + Some(bespoke) => { + bespoke.clone() + } + None => { + StringMap::default() + } + }; + + // Try and work out the output prefix by pulling from the first frame + let mut problem_entries: Vec = vec![]; + let output_prefix = states.first() + .and_then(|first_frame| first_frame.name.split("-").next()); + + // Next, check if anything conflicts, if it does we'll error + let frames_drop_prefix = states.clone().into_iter().map(|state| { + let full_name = state.name.clone(); + let mut split_name = full_name.split("-"); + let prefix = split_name.next(); + if prefix != output_prefix { + problem_entries.push(full_name.clone()); + } + let suffix = split_name.last().unwrap_or(prefix.unwrap_or_default()); + (state, suffix.to_string()) + }).collect::>(); + + if let Some(troublesome_states) = problem_entries.into_iter() + .reduce(|acc, elem| format!("{}, {}", acc, elem)) { + return Err(ProcessorError::DmiError( + format!("The following icon states are named with inconsistent prefixes (with the rest of the file) [{}]", troublesome_states) + )); + } + // Now, we remove the "core" frames, and dump them out + let extract_length = self.extract.len(); + let iter_extract = self.extract.clone().into_iter(); + let mut bespoke_found: Vec = vec![]; + // Extract just the bits we care about + let mut trimmed_frames = frames_drop_prefix.clone().into_iter().filter_map(|(mut state, suffix)| { + state.name = suffix.clone(); + if let Some(_) = bespoke.get(suffix.as_str()){ + bespoke_found.push(suffix); + Some(state) + } else if self.extract.contains(&suffix) { + Some(state) + } else { + None + } + }).collect::>(); + + // Check for any states that aren't extracted and aren't entirely numbers + // If we find any, error (cause we're dropping them here) + let strings_caught = trimmed_frames.clone().into_iter().map(|state| state.name.clone()).collect::>(); + let ignored_states = frames_drop_prefix.into_iter().filter_map(|(_, suffix)| { + if let Ok(_) = suffix.parse::() { + None + } else if strings_caught.iter().any(|caught| *caught == suffix) { + None + } else { + Some(format!("({})", suffix)) + } + }).reduce(|acc, elem| { + format!{"{}, {}", acc, elem} + }); + + + if let Some(missed_suffixes) = ignored_states { + let caught_text = strings_caught.into_iter().reduce(|acc, entry| format!("{}, {}", acc, entry)).unwrap_or_default(); + return Err(ProcessorError::DmiError( + format!("Restoration would fail to properly parse the following icon states [{}] not parsed like [{}]", missed_suffixes, caught_text) + )); + } + + // Alright next we're gonna work out the order of our insertion into the png based off the order of the extract/bespoke maps + // Extract first, then bespoke + let bespoke_iter = bespoke_found.clone().into_iter(); + + // I don't like all these clones but position() mutates and I don't want that so I'm not sure what else to do + let get_pos = |search_for: &String| { iter_extract.clone().position(|name| name == *search_for).unwrap_or( + if let Some(position) = bespoke_iter.clone().position(|name| name == *search_for) { + position + extract_length + } else { + usize::MAX + } + )}; + trimmed_frames.sort_by(|a, b| { + let a_pos = get_pos(&a.name); + let b_pos = get_pos(&b.name); + if a_pos > b_pos { + Ordering::Greater + } else if a_pos == b_pos { + Ordering::Equal + } else { + Ordering::Less + } + }); + + let frame_count = trimmed_frames.len(); + let longest_frame = trimmed_frames.clone().into_iter().map(|state| state.frames).max().unwrap_or(1); + // We now have a set of frames that we want to draw, ordered as requested + // So all we gotta do is make that png + // We assume all states have the same animation length, + let mut output_image = DynamicImage::new_rgba8(icon.width * frame_count as u32, icon.height * longest_frame); + let mut x = 0; + let delays: Option> = trimmed_frames.first() + .and_then(|first_frame| first_frame.delay.clone()); + + let text_delays = |textify: Vec, suffix: &str| -> String { + format!("[{}]", textify.into_iter().map(|ds| format!("{}{}", ds, suffix)).reduce(|acc, text_ds| format!("{}, {}", acc, text_ds)).unwrap_or_default()) + }; + for state in trimmed_frames { + if delays != state.delay { + return Err(ProcessorError::DmiError( + format!("Icon state {}'s delays {} do not match with the rest of the file {}", + state.name, + text_delays(state.delay.unwrap_or_default(), "ds"), + text_delays(delays.unwrap_or_default(), "ds")) + )); + } + let mut y = 0; + for frame in state.images { + debug!("{} {}", state.name, y); + output_image.copy_from(&frame, x * icon.width, y * icon.height).expect(format!("Failed to copy frame (bad dmi?): {} #{}", state.name, y).as_str()); + y += 1 + } + x += 1; + } + + let mut config: Vec = vec![]; + if let Some(prefix_name) = output_prefix { + config.push(format!("output_name = \"{}\"", prefix_name)); + } + if let Some(map) = &self.set { + map.0.clone().into_iter().for_each(|entry| { + config.push(format!("{} = {}", entry.0, entry.1)); + }); + config.push("".to_string()); + } + let mut count = frame_count - bespoke_found.len(); + if let Some(map) = &self.bespoke { + config.push("[prefabs]".to_string()); + map.0.clone().into_iter().for_each(|entry| { + config.push(format!("{} = {}", entry.1, count)); + count += 1; + }); + config.push("".to_string()); + } + if let Some(actual_delay) = delays { + config.push("[animation]".to_string()); + config.push(format!("delays = {}", + text_delays(actual_delay, "") + )); + config.push("".to_string()); + }; + config.push("[icon_size]".to_string()); + config.push(format!("x = {}", icon.width)); + config.push(format!("y = {}", icon.height)); + config.push("".to_string()); + config.push("[output_icon_size]".to_string()); + config.push(format!("x = {}", icon.width)); + config.push(format!("y = {}", icon.height)); + config.push("".to_string()); + config.push("[cut_pos]".to_string()); + config.push(format!("x = {}", icon.width / 2)); + config.push(format!("y = {}", icon.height / 2)); + Ok(ProcessorPayload::wrap_png_config(ProcessorPayload::from_image(output_image), config.join("\n"))) + } + + fn verify_config(&self) -> ProcessorResult<()> { + // TODO: Actual verification + Ok(()) + } +} diff --git a/hypnagogic_core/src/operations/mod.rs b/hypnagogic_core/src/operations/mod.rs index d62ad85..ea63539 100644 --- a/hypnagogic_core/src/operations/mod.rs +++ b/hypnagogic_core/src/operations/mod.rs @@ -5,6 +5,7 @@ use std::path::{Path, PathBuf}; use cutters::bitmask_dir_visibility::BitmaskDirectionalVis; use cutters::bitmask_slice::BitmaskSlice; use cutters::bitmask_windows::BitmaskWindows; +use format_converter::bitmask_to_precut::BitmaskSliceReconstruct; use dmi::error::DmiError; use dmi::icon::Icon; use enum_dispatch::enum_dispatch; @@ -130,6 +131,23 @@ impl NamedIcon { } } +/// Represents the possible actual outputs of an icon operation +#[derive(Clone)] +pub enum Output { + Image(OutputImage), + Text(OutputText) +} + +impl Output { + #[must_use] + pub const fn extension(&self) -> &'static str { + match self { + Output::Image(image) => image.extension(), + Output::Text(text) => text.extension(), + } + } +} + /// Represents the possible actual output images of an icon operation #[derive(Clone)] pub enum OutputImage { @@ -147,6 +165,23 @@ impl OutputImage { } } +/// Represents the possible text outputs of an icon operation +#[derive(Clone)] +pub enum OutputText { + PngConfig(String), + DmiConfig(String), +} + +impl OutputText { + #[must_use] + pub const fn extension(&self) -> &'static str { + match self { + OutputText::PngConfig(_) => "png.toml", + OutputText::DmiConfig(_) => "dmi.toml", + } + } +} + /// Represents the result of an icon operation /// It's entirely up to consumers to decide what to do with this #[derive(Clone)] @@ -159,6 +194,8 @@ pub enum ProcessorPayload { SingleNamed(Box), /// Multiple named icons. See [NamedIcon] for more info. MultipleNamed(Vec), + /// Payload of some sort with a config to produce inline with it + ConfigWrapped(Box, Box) } impl ProcessorPayload { @@ -166,6 +203,18 @@ impl ProcessorPayload { pub fn from_icon(icon: Icon) -> Self { Self::Single(Box::new(OutputImage::Dmi(icon))) } + #[must_use] + pub fn from_image(image: DynamicImage) -> Self { + Self::Single(Box::new(OutputImage::Png(image))) + } + #[must_use] + pub fn wrap_png_config(payload: ProcessorPayload, text: String) -> Self { + Self::ConfigWrapped(Box::new(payload), Box::new(OutputText::PngConfig(text))) + } + #[must_use] + pub fn wrap_dmi_config(payload: ProcessorPayload, text: String) -> Self { + Self::ConfigWrapped(Box::new(payload), Box::new(OutputText::DmiConfig(text))) + } } /// Possible generic modes of operation for an icon operation @@ -229,4 +278,5 @@ pub enum IconOperation { BitmaskSlice, BitmaskDirectionalVis, BitmaskWindows, + BitmaskSliceReconstruct, } From b2384e3f4a929ff140fda8432524568225fce8a2 Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Tue, 2 Jan 2024 17:01:32 -0800 Subject: [PATCH 2/7] some clippy work --- hypnagogic_core/src/config/blocks/cutters.rs | 10 +-- .../format_converter/bitmask_to_precut.rs | 61 +++++++------------ 2 files changed, 25 insertions(+), 46 deletions(-) diff --git a/hypnagogic_core/src/config/blocks/cutters.rs b/hypnagogic_core/src/config/blocks/cutters.rs index 1c1bb7a..53460f0 100644 --- a/hypnagogic_core/src/config/blocks/cutters.rs +++ b/hypnagogic_core/src/config/blocks/cutters.rs @@ -104,7 +104,7 @@ impl Default for Positions { } } -#[derive(Clone, Eq, PartialEq, Debug)] +#[derive(Clone, Eq, PartialEq, Debug, Default)] pub struct StringMap(pub HashMap); impl StringMap { @@ -127,7 +127,7 @@ impl Serialize for StringMap { { let mut map = HashMap::new(); - for (k, v) in self.0.iter() { + for (k, v) in &self.0 { map.insert(k.clone(), v.clone()); } @@ -150,12 +150,6 @@ impl<'de> Deserialize<'de> for StringMap { } } -impl Default for StringMap { - fn default() -> Self { - StringMap(HashMap::new()) - } -} - #[derive(Clone, Eq, PartialEq, Debug, Default)] pub struct Prefabs(pub BTreeMap); diff --git a/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs b/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs index 863f59f..4535a1a 100644 --- a/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs +++ b/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs @@ -4,10 +4,7 @@ use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use tracing::debug; -use crate::config::blocks::cutters::{ - OutputIconPosition, - StringMap, -}; +use crate::config::blocks::cutters::StringMap; use crate::operations::error::{ProcessorError, ProcessorResult}; use crate::operations::{ IconOperationConfig, @@ -54,12 +51,12 @@ impl IconOperationConfig for BitmaskSliceReconstruct { // Try and work out the output prefix by pulling from the first frame let mut problem_entries: Vec = vec![]; let output_prefix = states.first() - .and_then(|first_frame| first_frame.name.split("-").next()); + .and_then(|first_frame| first_frame.name.split('-').next()); // Next, check if anything conflicts, if it does we'll error let frames_drop_prefix = states.clone().into_iter().map(|state| { let full_name = state.name.clone(); - let mut split_name = full_name.split("-"); + let mut split_name = full_name.split('-'); let prefix = split_name.next(); if prefix != output_prefix { problem_entries.push(full_name.clone()); @@ -69,9 +66,9 @@ impl IconOperationConfig for BitmaskSliceReconstruct { }).collect::>(); if let Some(troublesome_states) = problem_entries.into_iter() - .reduce(|acc, elem| format!("{}, {}", acc, elem)) { + .reduce(|acc, elem| format!("{acc}, {elem}")) { return Err(ProcessorError::DmiError( - format!("The following icon states are named with inconsistent prefixes (with the rest of the file) [{}]", troublesome_states) + format!("The following icon states are named with inconsistent prefixes (with the rest of the file) [{troublesome_states}]") )); } // Now, we remove the "core" frames, and dump them out @@ -81,7 +78,7 @@ impl IconOperationConfig for BitmaskSliceReconstruct { // Extract just the bits we care about let mut trimmed_frames = frames_drop_prefix.clone().into_iter().filter_map(|(mut state, suffix)| { state.name = suffix.clone(); - if let Some(_) = bespoke.get(suffix.as_str()){ + if bespoke.get(suffix.as_str()).is_some(){ bespoke_found.push(suffix); Some(state) } else if self.extract.contains(&suffix) { @@ -95,22 +92,20 @@ impl IconOperationConfig for BitmaskSliceReconstruct { // If we find any, error (cause we're dropping them here) let strings_caught = trimmed_frames.clone().into_iter().map(|state| state.name.clone()).collect::>(); let ignored_states = frames_drop_prefix.into_iter().filter_map(|(_, suffix)| { - if let Ok(_) = suffix.parse::() { - None - } else if strings_caught.iter().any(|caught| *caught == suffix) { + if suffix.parse::().is_ok() || strings_caught.iter().any(|caught| *caught == suffix) { None } else { - Some(format!("({})", suffix)) + Some(format!("({suffix})")) } }).reduce(|acc, elem| { - format!{"{}, {}", acc, elem} + format!{"{acc}, {elem}"} }); if let Some(missed_suffixes) = ignored_states { - let caught_text = strings_caught.into_iter().reduce(|acc, entry| format!("{}, {}", acc, entry)).unwrap_or_default(); + let caught_text = strings_caught.into_iter().reduce(|acc, entry| format!("{acc}, {entry}")).unwrap_or_default(); return Err(ProcessorError::DmiError( - format!("Restoration would fail to properly parse the following icon states [{}] not parsed like [{}]", missed_suffixes, caught_text) + format!("Restoration would fail to properly parse the following icon states [{missed_suffixes}] not parsed like [{caught_text}]") )); } @@ -129,13 +124,7 @@ impl IconOperationConfig for BitmaskSliceReconstruct { trimmed_frames.sort_by(|a, b| { let a_pos = get_pos(&a.name); let b_pos = get_pos(&b.name); - if a_pos > b_pos { - Ordering::Greater - } else if a_pos == b_pos { - Ordering::Equal - } else { - Ordering::Less - } + a_pos.cmp(&b_pos) }); let frame_count = trimmed_frames.len(); @@ -144,14 +133,13 @@ impl IconOperationConfig for BitmaskSliceReconstruct { // So all we gotta do is make that png // We assume all states have the same animation length, let mut output_image = DynamicImage::new_rgba8(icon.width * frame_count as u32, icon.height * longest_frame); - let mut x = 0; let delays: Option> = trimmed_frames.first() .and_then(|first_frame| first_frame.delay.clone()); let text_delays = |textify: Vec, suffix: &str| -> String { - format!("[{}]", textify.into_iter().map(|ds| format!("{}{}", ds, suffix)).reduce(|acc, text_ds| format!("{}, {}", acc, text_ds)).unwrap_or_default()) + format!("[{}]", textify.into_iter().map(|ds| format!("{ds}{suffix}")).reduce(|acc, text_ds| format!("{acc}, {text_ds}")).unwrap_or_default()) }; - for state in trimmed_frames { + for (x, state) in trimmed_frames.into_iter().enumerate() { if delays != state.delay { return Err(ProcessorError::DmiError( format!("Icon state {}'s delays {} do not match with the rest of the file {}", @@ -160,24 +148,21 @@ impl IconOperationConfig for BitmaskSliceReconstruct { text_delays(delays.unwrap_or_default(), "ds")) )); } - let mut y = 0; - for frame in state.images { - debug!("{} {}", state.name, y); - output_image.copy_from(&frame, x * icon.width, y * icon.height).expect(format!("Failed to copy frame (bad dmi?): {} #{}", state.name, y).as_str()); - y += 1 + for (y, frame) in state.images.into_iter().enumerate() { + debug!("{} {} {}", state.name, x, y); + output_image.copy_from(&frame, (x as u32) * icon.width, (y as u32) * icon.height).unwrap_or_else(|_| panic!("Failed to copy frame (bad dmi?): {} #{}", state.name, y)); } - x += 1; } let mut config: Vec = vec![]; if let Some(prefix_name) = output_prefix { - config.push(format!("output_name = \"{}\"", prefix_name)); + config.push(format!("output_name = \"{prefix_name}\"")); } if let Some(map) = &self.set { map.0.clone().into_iter().for_each(|entry| { config.push(format!("{} = {}", entry.0, entry.1)); }); - config.push("".to_string()); + config.push(String::new()); } let mut count = frame_count - bespoke_found.len(); if let Some(map) = &self.bespoke { @@ -186,23 +171,23 @@ impl IconOperationConfig for BitmaskSliceReconstruct { config.push(format!("{} = {}", entry.1, count)); count += 1; }); - config.push("".to_string()); + config.push(String::new()); } if let Some(actual_delay) = delays { config.push("[animation]".to_string()); config.push(format!("delays = {}", text_delays(actual_delay, "") )); - config.push("".to_string()); + config.push(String::new()); }; config.push("[icon_size]".to_string()); config.push(format!("x = {}", icon.width)); config.push(format!("y = {}", icon.height)); - config.push("".to_string()); + config.push(String::new()); config.push("[output_icon_size]".to_string()); config.push(format!("x = {}", icon.width)); config.push(format!("y = {}", icon.height)); - config.push("".to_string()); + config.push(String::new()); config.push("[cut_pos]".to_string()); config.push(format!("x = {}", icon.width / 2)); config.push(format!("y = {}", icon.height / 2)); From 014ea4d8953cbd9ae5908cf5a3ad2139409bd393 Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Tue, 2 Jan 2024 17:03:20 -0800 Subject: [PATCH 3/7] missed a spot --- .../src/operations/format_converter/bitmask_to_precut.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs b/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs index 4535a1a..1b1b1bf 100644 --- a/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs +++ b/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs @@ -1,7 +1,6 @@ use dmi::icon::IconState; use image::{DynamicImage, GenericImage}; use serde::{Deserialize, Serialize}; -use std::cmp::Ordering; use tracing::debug; use crate::config::blocks::cutters::StringMap; From 6f20719c42fb50e9e03a0146bcc70d22944a653c Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Tue, 2 Jan 2024 19:36:24 -0800 Subject: [PATCH 4/7] ensures we newline at the end of files so it doesn't happen automatically --- Cargo.lock | 4 ++-- .../src/operations/format_converter/bitmask_to_precut.rs | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bce7623..7ac94c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -516,7 +516,7 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hypnagogic-cli" -version = "3.0.0" +version = "3.0.1" dependencies = [ "anyhow", "assert_cmd", @@ -538,7 +538,7 @@ dependencies = [ [[package]] name = "hypnagogic-core" -version = "3.0.0" +version = "3.0.1" dependencies = [ "bitflags 1.3.2", "dmi", diff --git a/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs b/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs index 1b1b1bf..9f4f50e 100644 --- a/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs +++ b/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs @@ -190,6 +190,8 @@ impl IconOperationConfig for BitmaskSliceReconstruct { config.push("[cut_pos]".to_string()); config.push(format!("x = {}", icon.width / 2)); config.push(format!("y = {}", icon.height / 2)); + // Newline gang + config.push(String::new()); Ok(ProcessorPayload::wrap_png_config(ProcessorPayload::from_image(output_image), config.join("\n"))) } From c6727647118cdaca4199eae9e1fb84efdf9ce83e Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Tue, 2 Jan 2024 20:41:46 -0800 Subject: [PATCH 5/7] fmt --- hypnagogic_cli/src/main.rs | 36 +-- .../format_converter/bitmask_to_precut.rs | 220 +++++++++++------- hypnagogic_core/src/operations/mod.rs | 9 +- 3 files changed, 162 insertions(+), 103 deletions(-) diff --git a/hypnagogic_cli/src/main.rs b/hypnagogic_cli/src/main.rs index 66a3cd7..5b2bb3b 100644 --- a/hypnagogic_cli/src/main.rs +++ b/hypnagogic_cli/src/main.rs @@ -21,7 +21,7 @@ use hypnagogic_core::operations::{ Output, OutputImage, OutputText, - ProcessorPayload, + ProcessorPayload, }; use rayon::prelude::*; use tracing::{debug, info, Level}; @@ -270,19 +270,24 @@ fn process_icon( // TODO: figure out a better thing to do than just the unwrap match output { - Output::Image(icon) => match icon { - OutputImage::Png(png) => { - png.save(&mut path).unwrap(); - } - OutputImage::Dmi(dmi) => { - dmi.save(&mut file).unwrap(); + Output::Image(icon) => { + match icon { + OutputImage::Png(png) => { + png.save(&mut path).unwrap(); + } + OutputImage::Dmi(dmi) => { + dmi.save(&mut file).unwrap(); + } } } - Output::Text(text) => match text { - OutputText::PngConfig(config) | OutputText::DmiConfig(config) => { - fs::write(path, config).expect( - "Failed to write config text, (This is a program error, not a config error! Please \ - report!)") + Output::Text(text) => { + match text { + OutputText::PngConfig(config) | OutputText::DmiConfig(config) => { + fs::write(path, config).expect( + "Failed to write config text, (This is a program error, not a config \ + error! Please report!)", + ) + } } } } @@ -291,7 +296,12 @@ fn process_icon( } #[allow(clippy::result_large_err)] -fn handle_payload(payload: ProcessorPayload, input_path: PathBuf, output_at: &Option, flatten: bool) -> Vec<(PathBuf, Output)> { +fn handle_payload( + payload: ProcessorPayload, + input_path: PathBuf, + output_at: &Option, + flatten: bool, +) -> Vec<(PathBuf, Output)> { let mut out_paths: Vec<(PathBuf, Output)> = vec![]; let process_path = |path: PathBuf, named_img: Option<&NamedIcon>| -> PathBuf { debug!(path = ?path, img = ?named_img, "Processing path"); diff --git a/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs b/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs index 9f4f50e..ceb6924 100644 --- a/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs +++ b/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs @@ -5,12 +5,7 @@ use tracing::debug; use crate::config::blocks::cutters::StringMap; use crate::operations::error::{ProcessorError, ProcessorResult}; -use crate::operations::{ - IconOperationConfig, - InputIcon, - OperationMode, - ProcessorPayload, -}; +use crate::operations::{IconOperationConfig, InputIcon, OperationMode, ProcessorPayload}; #[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)] pub struct BitmaskSliceReconstruct { @@ -32,124 +27,174 @@ impl IconOperationConfig for BitmaskSliceReconstruct { ) -> ProcessorResult { debug!("Starting bitmask slice reconstruction"); let InputIcon::Dmi(icon) = input else { - return Err(ProcessorError::FormatError("This operation only accepts dmis".to_string())); + return Err(ProcessorError::FormatError( + "This operation only accepts dmis".to_string(), + )); }; // First, pull out icon states from DMI let states = icon.states.clone(); - + let bespoke = match self.bespoke.as_ref() { - Some(bespoke) => { - bespoke.clone() - } - None => { - StringMap::default() - } + Some(bespoke) => bespoke.clone(), + None => StringMap::default(), }; - + // Try and work out the output prefix by pulling from the first frame let mut problem_entries: Vec = vec![]; - let output_prefix = states.first() + let output_prefix = states + .first() .and_then(|first_frame| first_frame.name.split('-').next()); // Next, check if anything conflicts, if it does we'll error - let frames_drop_prefix = states.clone().into_iter().map(|state| { - let full_name = state.name.clone(); - let mut split_name = full_name.split('-'); - let prefix = split_name.next(); - if prefix != output_prefix { - problem_entries.push(full_name.clone()); - } - let suffix = split_name.last().unwrap_or(prefix.unwrap_or_default()); - (state, suffix.to_string()) - }).collect::>(); - - if let Some(troublesome_states) = problem_entries.into_iter() - .reduce(|acc, elem| format!("{acc}, {elem}")) { - return Err(ProcessorError::DmiError( - format!("The following icon states are named with inconsistent prefixes (with the rest of the file) [{troublesome_states}]") - )); + let frames_drop_prefix = states + .clone() + .into_iter() + .map(|state| { + let full_name = state.name.clone(); + let mut split_name = full_name.split('-'); + let prefix = split_name.next(); + if prefix != output_prefix { + problem_entries.push(full_name.clone()); + } + let suffix = split_name.last().unwrap_or(prefix.unwrap_or_default()); + (state, suffix.to_string()) + }) + .collect::>(); + + if let Some(troublesome_states) = problem_entries + .into_iter() + .reduce(|acc, elem| format!("{acc}, {elem}")) + { + return Err(ProcessorError::DmiError(format!( + "The following icon states are named with inconsistent prefixes (with the rest of \ + the file) [{troublesome_states}]" + ))); } - // Now, we remove the "core" frames, and dump them out + // Now, we remove the "core" frames, and dump them out let extract_length = self.extract.len(); let iter_extract = self.extract.clone().into_iter(); let mut bespoke_found: Vec = vec![]; // Extract just the bits we care about - let mut trimmed_frames = frames_drop_prefix.clone().into_iter().filter_map(|(mut state, suffix)| { - state.name = suffix.clone(); - if bespoke.get(suffix.as_str()).is_some(){ - bespoke_found.push(suffix); - Some(state) - } else if self.extract.contains(&suffix) { - Some(state) - } else { - None - } - }).collect::>(); - + let mut trimmed_frames = frames_drop_prefix + .clone() + .into_iter() + .filter_map(|(mut state, suffix)| { + state.name = suffix.clone(); + if bespoke.get(suffix.as_str()).is_some() { + bespoke_found.push(suffix); + Some(state) + } else if self.extract.contains(&suffix) { + Some(state) + } else { + None + } + }) + .collect::>(); + // Check for any states that aren't extracted and aren't entirely numbers // If we find any, error (cause we're dropping them here) - let strings_caught = trimmed_frames.clone().into_iter().map(|state| state.name.clone()).collect::>(); - let ignored_states = frames_drop_prefix.into_iter().filter_map(|(_, suffix)| { - if suffix.parse::().is_ok() || strings_caught.iter().any(|caught| *caught == suffix) { - None - } else { - Some(format!("({suffix})")) - } - }).reduce(|acc, elem| { - format!{"{acc}, {elem}"} - }); + let strings_caught = trimmed_frames + .clone() + .into_iter() + .map(|state| state.name.clone()) + .collect::>(); + let ignored_states = frames_drop_prefix + .into_iter() + .filter_map(|(_, suffix)| { + if suffix.parse::().is_ok() + || strings_caught.iter().any(|caught| *caught == suffix) + { + None + } else { + Some(format!("({suffix})")) + } + }) + .reduce(|acc, elem| { + format! {"{acc}, {elem}"} + }); if let Some(missed_suffixes) = ignored_states { - let caught_text = strings_caught.into_iter().reduce(|acc, entry| format!("{acc}, {entry}")).unwrap_or_default(); - return Err(ProcessorError::DmiError( - format!("Restoration would fail to properly parse the following icon states [{missed_suffixes}] not parsed like [{caught_text}]") - )); + let caught_text = strings_caught + .into_iter() + .reduce(|acc, entry| format!("{acc}, {entry}")) + .unwrap_or_default(); + return Err(ProcessorError::DmiError(format!( + "Restoration would fail to properly parse the following icon states \ + [{missed_suffixes}] not parsed like [{caught_text}]" + ))); } - // Alright next we're gonna work out the order of our insertion into the png based off the order of the extract/bespoke maps - // Extract first, then bespoke + // Alright next we're gonna work out the order of our insertion into the png + // based off the order of the extract/bespoke maps Extract first, then + // bespoke let bespoke_iter = bespoke_found.clone().into_iter(); - // I don't like all these clones but position() mutates and I don't want that so I'm not sure what else to do - let get_pos = |search_for: &String| { iter_extract.clone().position(|name| name == *search_for).unwrap_or( - if let Some(position) = bespoke_iter.clone().position(|name| name == *search_for) { - position + extract_length - } else { - usize::MAX - } - )}; + // I don't like all these clones but position() mutates and I don't want that so + // I'm not sure what else to do + let get_pos = |search_for: &String| { + iter_extract + .clone() + .position(|name| name == *search_for) + .unwrap_or( + if let Some(position) = + bespoke_iter.clone().position(|name| name == *search_for) + { + position + extract_length + } else { + usize::MAX + }, + ) + }; trimmed_frames.sort_by(|a, b| { let a_pos = get_pos(&a.name); let b_pos = get_pos(&b.name); a_pos.cmp(&b_pos) }); - + let frame_count = trimmed_frames.len(); - let longest_frame = trimmed_frames.clone().into_iter().map(|state| state.frames).max().unwrap_or(1); + let longest_frame = trimmed_frames + .clone() + .into_iter() + .map(|state| state.frames) + .max() + .unwrap_or(1); // We now have a set of frames that we want to draw, ordered as requested // So all we gotta do is make that png - // We assume all states have the same animation length, - let mut output_image = DynamicImage::new_rgba8(icon.width * frame_count as u32, icon.height * longest_frame); - let delays: Option> = trimmed_frames.first() + // We assume all states have the same animation length, + let mut output_image = + DynamicImage::new_rgba8(icon.width * frame_count as u32, icon.height * longest_frame); + let delays: Option> = trimmed_frames + .first() .and_then(|first_frame| first_frame.delay.clone()); let text_delays = |textify: Vec, suffix: &str| -> String { - format!("[{}]", textify.into_iter().map(|ds| format!("{ds}{suffix}")).reduce(|acc, text_ds| format!("{acc}, {text_ds}")).unwrap_or_default()) + format!( + "[{}]", + textify + .into_iter() + .map(|ds| format!("{ds}{suffix}")) + .reduce(|acc, text_ds| format!("{acc}, {text_ds}")) + .unwrap_or_default() + ) }; for (x, state) in trimmed_frames.into_iter().enumerate() { if delays != state.delay { - return Err(ProcessorError::DmiError( - format!("Icon state {}'s delays {} do not match with the rest of the file {}", - state.name, - text_delays(state.delay.unwrap_or_default(), "ds"), - text_delays(delays.unwrap_or_default(), "ds")) - )); - } + return Err(ProcessorError::DmiError(format!( + "Icon state {}'s delays {} do not match with the rest of the file {}", + state.name, + text_delays(state.delay.unwrap_or_default(), "ds"), + text_delays(delays.unwrap_or_default(), "ds") + ))); + } for (y, frame) in state.images.into_iter().enumerate() { debug!("{} {} {}", state.name, x, y); - output_image.copy_from(&frame, (x as u32) * icon.width, (y as u32) * icon.height).unwrap_or_else(|_| panic!("Failed to copy frame (bad dmi?): {} #{}", state.name, y)); + output_image + .copy_from(&frame, (x as u32) * icon.width, (y as u32) * icon.height) + .unwrap_or_else(|_| { + panic!("Failed to copy frame (bad dmi?): {} #{}", state.name, y) + }); } } @@ -174,9 +219,7 @@ impl IconOperationConfig for BitmaskSliceReconstruct { } if let Some(actual_delay) = delays { config.push("[animation]".to_string()); - config.push(format!("delays = {}", - text_delays(actual_delay, "") - )); + config.push(format!("delays = {}", text_delays(actual_delay, ""))); config.push(String::new()); }; config.push("[icon_size]".to_string()); @@ -192,7 +235,10 @@ impl IconOperationConfig for BitmaskSliceReconstruct { config.push(format!("y = {}", icon.height / 2)); // Newline gang config.push(String::new()); - Ok(ProcessorPayload::wrap_png_config(ProcessorPayload::from_image(output_image), config.join("\n"))) + Ok(ProcessorPayload::wrap_png_config( + ProcessorPayload::from_image(output_image), + config.join("\n"), + )) } fn verify_config(&self) -> ProcessorResult<()> { diff --git a/hypnagogic_core/src/operations/mod.rs b/hypnagogic_core/src/operations/mod.rs index ea63539..6cc4756 100644 --- a/hypnagogic_core/src/operations/mod.rs +++ b/hypnagogic_core/src/operations/mod.rs @@ -5,10 +5,10 @@ use std::path::{Path, PathBuf}; use cutters::bitmask_dir_visibility::BitmaskDirectionalVis; use cutters::bitmask_slice::BitmaskSlice; use cutters::bitmask_windows::BitmaskWindows; -use format_converter::bitmask_to_precut::BitmaskSliceReconstruct; use dmi::error::DmiError; use dmi::icon::Icon; use enum_dispatch::enum_dispatch; +use format_converter::bitmask_to_precut::BitmaskSliceReconstruct; use image::{DynamicImage, ImageError, ImageFormat}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -135,7 +135,7 @@ impl NamedIcon { #[derive(Clone)] pub enum Output { Image(OutputImage), - Text(OutputText) + Text(OutputText), } impl Output { @@ -195,7 +195,7 @@ pub enum ProcessorPayload { /// Multiple named icons. See [NamedIcon] for more info. MultipleNamed(Vec), /// Payload of some sort with a config to produce inline with it - ConfigWrapped(Box, Box) + ConfigWrapped(Box, Box), } impl ProcessorPayload { @@ -203,14 +203,17 @@ impl ProcessorPayload { pub fn from_icon(icon: Icon) -> Self { Self::Single(Box::new(OutputImage::Dmi(icon))) } + #[must_use] pub fn from_image(image: DynamicImage) -> Self { Self::Single(Box::new(OutputImage::Png(image))) } + #[must_use] pub fn wrap_png_config(payload: ProcessorPayload, text: String) -> Self { Self::ConfigWrapped(Box::new(payload), Box::new(OutputText::PngConfig(text))) } + #[must_use] pub fn wrap_dmi_config(payload: ProcessorPayload, text: String) -> Self { Self::ConfigWrapped(Box::new(payload), Box::new(OutputText::DmiConfig(text))) From c7842745a824cee490f877ac45f234a932cf2c23 Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Tue, 2 Jan 2024 20:44:26 -0800 Subject: [PATCH 6/7] clippy --- hypnagogic_core/src/config/blocks/cutters.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypnagogic_core/src/config/blocks/cutters.rs b/hypnagogic_core/src/config/blocks/cutters.rs index 53460f0..f2736b0 100644 --- a/hypnagogic_core/src/config/blocks/cutters.rs +++ b/hypnagogic_core/src/config/blocks/cutters.rs @@ -110,7 +110,7 @@ pub struct StringMap(pub HashMap); impl StringMap { #[must_use] pub fn get(&self, key: &str) -> Option<&String> { - self.0.get(key.clone()) + self.0.get(key) } } From 2294ac7e4136793803083b889517bb4da344932d Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Fri, 26 Apr 2024 17:25:44 -0700 Subject: [PATCH 7/7] yeeet --- .../src/operations/format_converter/bitmask_to_precut.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs b/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs index ceb6924..90e8e70 100644 --- a/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs +++ b/hypnagogic_core/src/operations/format_converter/bitmask_to_precut.rs @@ -114,7 +114,6 @@ impl IconOperationConfig for BitmaskSliceReconstruct { format! {"{acc}, {elem}"} }); - if let Some(missed_suffixes) = ignored_states { let caught_text = strings_caught .into_iter()