From 7efb826bef58c796274b099fc2d225d27d1145ec Mon Sep 17 00:00:00 2001 From: itsmeow Date: Wed, 10 Jan 2024 01:10:54 -0600 Subject: [PATCH 1/5] GAGS --- dmsrc/iconforge.dm | 13 ++ src/iconforge.rs | 500 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 470 insertions(+), 43 deletions(-) diff --git a/dmsrc/iconforge.dm b/dmsrc/iconforge.dm index 5f6d9bf0..ec01869d 100644 --- a/dmsrc/iconforge.dm +++ b/dmsrc/iconforge.dm @@ -52,6 +52,19 @@ #define rustg_iconforge_cache_valid(input_hash, dmi_hashes, sprites) RUSTG_CALL(RUST_G, "iconforge_cache_valid")(input_hash, dmi_hashes, sprites) /// Returns a job_id for use with rustg_iconforge_check() #define rustg_iconforge_cache_valid_async(input_hash, dmi_hashes, sprites) RUSTG_CALL(RUST_G, "iconforge_cache_valid_async")(input_hash, dmi_hashes, sprites) +/// Provided a /datum/greyscale_config typepath, JSON string containing the greyscale config, and path to a DMI file containing the base icons, +/// Loads that config into memory for later use by rustg_iconforge_gags(). The config_path is the unique identifier used later. +/// JSON Config schema: https://hackmd.io/@tgstation/GAGS-Layer-Types +/// Unsupported features: color_matrix layer type, 'or' blend_mode. May not have BYOND parity with animated icons or varying dirs between layers. +/// Returns "OK" if successful, otherwise, returns a string containing the error. +#define rustg_iconforge_load_gags_config(config_path, config_json, config_icon_path) RUSTG_CALL(RUST_G, "iconforge_load_gags_config")("[config_path]", config_json, config_icon_path) +/// Given a config_path (previously loaded by rustg_iconforge_load_gags_config), and a string of hex colors formatted as "#ff00ff#ffaa00" +/// Outputs a DMI containing all of the states within the config JSON to output_dmi_path, creating any directories leading up to it if necessary. +/// Returns "OK" if successful, otherwise, returns a string containing the error. +#define rustg_iconforge_gags(config_path, colors, output_dmi_path) RUSTG_CALL(RUST_G, "iconforge_gags")("[config_path]", colors, output_dmi_path) +/// Returns a job_id for use with rustg_iconforge_check() +#define rustg_iconforge_gags_async(config_path, colors, output_dmi_path) RUSTG_CALL(RUST_G, "iconforge_gags")("[config_path]", colors, output_dmi_path) + #define RUSTG_ICONFORGE_BLEND_COLOR "BlendColor" #define RUSTG_ICONFORGE_BLEND_ICON "BlendIcon" diff --git a/src/iconforge.rs b/src/iconforge.rs index 40145dea..ca9e82bf 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -9,9 +9,9 @@ use crate::{ use dashmap::DashMap; use dmi::{ dirs::Dirs, - icon::{Icon, IconState}, + icon::{Icon, IconState, DmiVersion}, }; -use image::{Pixel, RgbaImage}; +use image::{Pixel, RgbaImage, DynamicImage}; use once_cell::sync::Lazy; use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}; use serde::{Deserialize, Serialize}; @@ -106,6 +106,44 @@ byond_fn!(fn iconforge_cache_valid_async(input_hash, dmi_hashes, sprites) { result }); +byond_fn!(fn iconforge_load_gags_config(config_path, config_json, config_icon_path) { + let config_path = config_path.to_owned(); + let config_json = config_json.to_owned(); + let config_icon_path = config_icon_path.to_owned(); + let result = Some(match catch_panic(|| load_gags_config(&config_path, &config_json, &config_icon_path)) { + Ok(o) => o.to_string(), + Err(e) => e.to_string() + }); + frame!(); + result +}); + +byond_fn!(fn iconforge_gags(config_path, colors, output_dmi_path) { + let config_path = config_path.to_owned(); + let colors = colors.to_owned(); + let output_dmi_path = output_dmi_path.to_owned(); + let result = Some(match catch_panic(|| gags(&config_path, &colors, &output_dmi_path)) { + Ok(o) => o.to_string(), + Err(e) => e.to_string() + }); + frame!(); + result +}); + +byond_fn!(fn iconforge_gags_async(config_path, colors, output_dmi_path) { + let config_path = config_path.to_owned(); + let colors = colors.to_owned(); + let output_dmi_path = output_dmi_path.to_owned(); + Some(jobs::start(move || { + let result = match catch_panic(|| gags(&config_path, &colors, &output_dmi_path)) { + Ok(o) => o.to_string(), + Err(e) => e.to_string() + }; + frame!(); + result + })) +}); + #[derive(Serialize)] struct SpritesheetResult { sizes: Vec, @@ -359,7 +397,7 @@ fn generate_spritesheet( icon_to_icons(icon) .into_par_iter() - .for_each(|icon| match icon_to_dmi(icon) { + .for_each(|icon| match filepath_to_dmi(&icon.icon_file) { Ok(_) => { if hash_icons && !dmi_hashes.contains_key(&icon.icon_file) { zone!("hash_dmi"); @@ -709,10 +747,9 @@ fn icon_to_icons_io(icon_in: &IconObjectIO) -> Vec<&IconObjectIO> { icons } -/// Given an IconObject, returns a DMI Icon structure and caches it. -fn icon_to_dmi(icon: &IconObject) -> Result, String> { - zone!("icon_to_dmi"); - let icon_path = &icon.icon_file; +/// Given a DMI filepath, returns a DMI Icon structure and caches it. +fn filepath_to_dmi(icon_path: &str) -> Result, String> { + zone!("filepath_to_dmi"); { zone!("check_dmi_exists"); if let Some(found) = ICON_FILES.get(icon_path) { @@ -771,7 +808,7 @@ fn icon_to_image( return Err(String::from("Image not found in cache!")); } } - let dmi = icon_to_dmi(icon)?; + let dmi = filepath_to_dmi(&icon.icon_file)?; let mut matched_state: Option<&IconState> = None; { zone!("match_icon_state"); @@ -835,35 +872,59 @@ fn apply_all_transforms(image: &mut RgbaImage, transforms: &Vec) -> R Ok(()) } +fn blend_color(image: &mut RgbaImage, color: &String, blend_mode: &u8) -> Result<(), String> { + zone!("blend_color"); + let mut color2: [u8; 4] = [0, 0, 0, 255]; + { + zone!("from_hex"); + let mut hex: String = color.to_owned(); + if hex.starts_with('#') { + hex = hex[1..].to_string(); + } + if hex.len() == 6 { + hex += "ff"; + } + + if let Err(err) = hex::decode_to_slice(hex, &mut color2) { + return Err(format!("Decoding hex color {} failed: {}", color, err)); + } + } + for x in 0..image.width() { + for y in 0..image.height() { + let px = image.get_pixel_mut(x, y); + let pixel = px.channels(); + let blended = Rgba::blend_u8(pixel, &color2, *blend_mode); + + *px = image::Rgba::(blended); + } + } + Ok(()) +} + +fn blend_icon(image: &mut RgbaImage, other_image: &RgbaImage, blend_mode: &u8) -> Result<(), String> { + zone!("blend_icon"); + for x in 0..std::cmp::min(image.width(), other_image.width()) { + for y in 0..std::cmp::min(image.width(), other_image.width()) { + let px1 = image.get_pixel_mut(x, y); + let px2 = other_image.get_pixel(x, y); + let pixel_1 = px1.channels(); + let pixel_2 = px2.channels(); + + let blended = Rgba::blend_u8(pixel_1, pixel_2, *blend_mode); + + *px1 = image::Rgba::(blended); + } + } + Ok(()) +} + /// Applies transforms to a RgbaImage. fn transform_image(image: &mut RgbaImage, transform: &Transform) -> Result<(), String> { zone!("transform_image"); match transform { Transform::BlendColor { color, blend_mode } => { - zone!("blend_color"); - let mut color2: [u8; 4] = [0, 0, 0, 255]; - { - zone!("from_hex"); - let mut hex: String = color.to_owned(); - if hex.starts_with('#') { - hex = hex[1..].to_string(); - } - if hex.len() == 6 { - hex += "ff"; - } - - if let Err(err) = hex::decode_to_slice(hex, &mut color2) { - return Err(format!("Decoding hex color {} failed: {}", color, err)); - } - } - for x in 0..image.width() { - for y in 0..image.height() { - let px = image.get_pixel_mut(x, y); - let pixel = px.channels(); - let blended = Rgba::blend_u8(pixel, &color2, *blend_mode); - - *px = image::Rgba::(blended); - } + if let Err(err) = blend_color(image, color, blend_mode) { + return Err(err); } } Transform::BlendIcon { icon, blend_mode } => { @@ -874,17 +935,8 @@ fn transform_image(image: &mut RgbaImage, transform: &Transform) -> Result<(), S if !cached { apply_all_transforms(&mut other_image, &icon.transform)?; }; - for x in 0..std::cmp::min(image.width(), other_image.width()) { - for y in 0..std::cmp::min(image.width(), other_image.width()) { - let px1 = image.get_pixel_mut(x, y); - let px2 = other_image.get_pixel(x, y); - let pixel_1 = px1.channels(); - let pixel_2 = px2.channels(); - - let blended = Rgba::blend_u8(pixel_1, pixel_2, *blend_mode); - - *px1 = image::Rgba::(blended); - } + if let Err(err) = blend_icon(image, &other_image, blend_mode) { + return Err(err); } if let Err(err) = return_image(other_image, icon) { return Err(err.to_string()); @@ -983,6 +1035,368 @@ fn transform_image(image: &mut RgbaImage, transform: &Transform) -> Result<(), S Ok(()) } +type GAGSConfigEntry = Vec; + +#[derive(Serialize, Deserialize, Clone)] +#[serde(untagged)] +enum GAGSLayerGroupOption { + GAGSLayer(GAGSLayer), + GAGSLayerGroup(Vec), +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(untagged)] +enum GAGSColorID { + GAGSColorStatic(String), + GAGSColorIndex(u8), +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(tag = "type", rename_all = "snake_case")] +enum GAGSLayer { + IconState { + icon_state: String, + blend_mode: String, + #[serde(default)] + color_ids: Vec, + }, + Reference { + reference_type: String, + #[serde(default)] + icon_state: String, + blend_mode: String, + #[serde(default)] + color_ids: Vec, + }, + // Unsupported, but exists nonetheless. + ColorMatrix { + blend_mode: String, + color_matrix: [[f32; 4]; 5], + } +} + +impl GAGSLayer { + fn get_blendmode(&self) -> String { + match self { + GAGSLayer::IconState { icon_state: _, blend_mode, color_ids: _ } => blend_mode.to_owned(), + GAGSLayer::Reference { reference_type: _, icon_state: _, blend_mode, color_ids: _ } => blend_mode.to_owned(), + GAGSLayer::ColorMatrix { blend_mode, color_matrix: _ } => blend_mode.to_owned(), + } + } +} + +type GAGSConfig = HashMap; + +struct GAGSData { + config: GAGSConfig, + config_path: String, + config_icon: Arc, +} + +static GAGS_CACHE: Lazy> = + Lazy::new(DashMap::new); + +/// Loads a GAGS config and the requested DMIs into memory for use by iconforge_gags() +fn load_gags_config(config_path: &str, config_json: &str, config_icon_path: &str) -> Result { + zone!("load_gags_config"); + let gags_config: GAGSConfig; + { + zone!("gags_from_json"); + gags_config = serde_json::from_str::(config_json)?; + } + let icon_data = match filepath_to_dmi(config_icon_path) { + Ok(data) => data, + Err(err) => { + return Err(Error::IconForge(err)); + } + }; + { + zone!("gags_insert_config"); + GAGS_CACHE.insert(config_path.to_owned(), GAGSData { + config: gags_config, + config_path: config_path.to_owned(), + config_icon: icon_data, + }); + } + Ok(String::from("OK")) +} + +/// Given an config path and a list of color_ids, outputs a dmi at output_dmi_path with the config's states. +fn gags(config_path: &str, colors: &str, output_dmi_path: &str) -> Result { + zone!("gags"); + let gags_data = match GAGS_CACHE.get(config_path) { + Some(config) => config, + None => { + return Err(Error::IconForge(format!("Provided config_path {} has not been loaded by iconforge_load_gags_config!", config_path))); + } + }; + + let colors_vec = colors.split("#").map(|x| String::from("#") + x).filter(|x| x != "#").collect::>(); + let errors = Arc::new(Mutex::new(Vec::::new())); + + let output_states = Arc::new(Mutex::new(Vec::::new())); + gags_data.config.par_iter().for_each(|(icon_state_name, layer_groups)| { + zone!("gags_create_icon_state"); + let mut first_matched_state: Option = None; + let transformed_images = match generate_layer_groups_for_iconstate(icon_state_name, &colors_vec, layer_groups, &gags_data, None, &mut first_matched_state) { + Ok(images) => images, + Err(err) => { + errors.lock().unwrap().push(err); + return; + } + }; + let icon_state = match first_matched_state { + Some(state) => state, + None => { + errors.lock().unwrap().push(format!("GAGS state {} for GAGS config {} had no matching icon_states in any layers!", icon_state_name, config_path)); + return; + } + }; + + { + zone!("gags_insert_icon_state"); + output_states.lock().unwrap().push(IconState { + name: icon_state_name.to_owned(), + dirs: icon_state.dirs, + frames: icon_state.frames, + delay: icon_state.delay.to_owned(), + loop_flag: icon_state.loop_flag, + rewind: icon_state.rewind, + movement: icon_state.movement, + unknown_settings: icon_state.unknown_settings.to_owned(), + hotspot: icon_state.hotspot, + images: transformed_images, + }); + } + }); + + let errors_unlocked = errors.lock().unwrap(); + if !errors_unlocked.is_empty() { + return Err(Error::IconForge(errors_unlocked.join("\n"))); + } + + { + zone!("gags_write_dmi"); + let path = std::path::Path::new(output_dmi_path); + std::fs::create_dir_all(path.parent().unwrap())?; + let mut output_file = File::create(path)?; + + if let Err(err) = (Icon { + version: DmiVersion::default(), + width: gags_data.config_icon.width, + height: gags_data.config_icon.height, + states: output_states.lock().unwrap().to_owned(), + }.save(&mut output_file)) { + return Err(Error::IconForge(format!("Error during icon saving: {}", err.to_string()))); + } + } + + Ok(String::from("OK")) +} + +/// Version of gags() for use by the reference layer type that acts in memory +fn gags_internal(config_path: &str, colors_vec: &Vec, icon_state: &String, last_external_images: Option>, first_matched_state: &mut Option) -> Result, String> { + zone!("gags_internal"); + let gags_data = match GAGS_CACHE.get(config_path) { + Some(config) => config, + None => { + return Err(format!("Provided config_path {} has not been loaded by iconforge_load_gags_config (from gags_internal)!", config_path)); + } + }; + + let layer_groups = match gags_data.config.get(icon_state) { + Some(data) => data, + None => { + return Err(format!("Provided config_path {} did not contain requested icon_state {} for reference type.", config_path, icon_state)); + } + }; + { + zone!("gags_create_icon_state"); + let mut first_matched_state_internal: Option = None; + let transformed_images = match generate_layer_groups_for_iconstate(icon_state, colors_vec, layer_groups, &gags_data, last_external_images, &mut first_matched_state_internal) { + Ok(images) => images, + Err(err) => { + return Err(err); + } + }; + { + zone!("update_first_matched_state"); + if first_matched_state.is_none() && first_matched_state_internal.is_some() { + *first_matched_state = first_matched_state_internal; + } + } + Ok(transformed_images) + } +} + +/// Recursive function that parses out GAGS configs into layer groups. +fn generate_layer_groups_for_iconstate(state_name: &str, colors: &Vec, layer_groups: &Vec, gags_data: &GAGSData, last_external_images: Option>, first_matched_state: &mut Option) -> Result, String> { + zone!("generate_layer_groups_for_iconstate"); + let mut new_images: Option> = None; + for option in layer_groups { + zone!("process_gags_layergroup_option"); + let (layer_images, blend_mode) = match option { + GAGSLayerGroupOption::GAGSLayer(layer) => (generate_layer_for_iconstate(state_name, colors, layer, gags_data, new_images.clone().or(last_external_images.clone()), first_matched_state)?, layer.get_blendmode()), + GAGSLayerGroupOption::GAGSLayerGroup(layers) => { + if layers.is_empty() { + return Err(format!("Empty layer group provided to GAGS state {} for GAGS config {} !", state_name, gags_data.config_path)); + } + (generate_layer_groups_for_iconstate(state_name, colors, layers, gags_data, new_images.clone().or(last_external_images.clone()), first_matched_state)?, match layers.first().unwrap() { + GAGSLayerGroupOption::GAGSLayer(layer) => layer.get_blendmode(), + GAGSLayerGroupOption::GAGSLayerGroup(_) => { + return Err(format!("Layer group began with another layer group in GAGS state {} for GAGS config {} !", state_name, gags_data.config_path)); + } + }) + } + }; + + new_images = match new_images { + Some(images) => Some(blend_images_other(images, layer_images, &blend_mode)?), + None => Some(layer_images) + } + } + match new_images { + Some(images) => Ok(images), + None => Err(format!("No image found for GAGS state {}", state_name)) + } +} + +/// Generates a specific layer. +fn generate_layer_for_iconstate(state_name: &str, colors: &Vec, layer: &GAGSLayer, gags_data: &GAGSData, new_images: Option>, first_matched_state: &mut Option) -> Result, String> { + zone!("generate_layer_for_iconstate"); + let images_result: Option> = match layer { + GAGSLayer::IconState { icon_state, blend_mode: _, color_ids } => { + zone!("gags_layer_type_icon_state"); + let icon_state: &IconState = match gags_data.config_icon.states.iter().find(|state| state.name == *icon_state) { + Some(state) => state, + None => { + return Err(format!("Invalid icon_state {} in layer provided for GAGS config {}", state_name, gags_data.config_path)); + } + }; + + if first_matched_state.is_none() { + *first_matched_state = Some(icon_state.clone()); + } + + let images = icon_state.images.clone(); + if !color_ids.is_empty() { + // silly BYOND, indexes from 1! Also, for some reason this is an array despite only ever having one value. Thanks TG :) + let actual_color = match color_ids.first().unwrap() { + GAGSColorID::GAGSColorIndex(idx) => colors.get(*idx as usize - 1).unwrap(), + GAGSColorID::GAGSColorStatic(color) => color, + }; + return Ok(blend_images_color(images, actual_color, &String::from("multiply"))?); + } else { + return Ok(images); // this will get blended by the layergroup. + } + }, + GAGSLayer::Reference { reference_type, icon_state, blend_mode: _, color_ids } => { + zone!("gags_layer_type_reference"); + let mut colors_in: Vec = colors.clone(); + if !color_ids.is_empty() { + colors_in = color_ids.iter().map(|color| match color { + GAGSColorID::GAGSColorIndex(idx) => colors.get(*idx as usize - 1).unwrap().clone(), + GAGSColorID::GAGSColorStatic(color) => color.clone(), + }).collect(); + } + Some(gags_internal(reference_type, &colors_in, icon_state, new_images, first_matched_state)?) + }, + GAGSLayer::ColorMatrix { blend_mode: _, color_matrix: _ } => new_images, // unsupported! TROLLED! + }; + + match images_result { + Some(images) => Ok(images), + None => Err(format!("No images found for GAGS state {} for GAGS config {} !", state_name, gags_data.config_path)) + } +} + +/// Blends a set of images with a color. +fn blend_images_color(images: Vec, color: &String, blend_mode: &String) -> Result, Error> { + zone!("blend_images_color"); + let errors = Arc::new(Mutex::new(Vec::::new())); + let images_out = images.into_par_iter().map(|image| { + zone!("blend_image_color"); + let mut new_image = image.clone().into_rgba8(); + if let Err(err) = blend_color(&mut new_image, color, &match blend_mode.as_str() { + "add" => 0, + "subtract" => 1, + "multiply" => 2, + "overlay" => 3, + "underlay" => 6, + _ => { + errors.lock().unwrap().push(format!("blend_mode '{}' is not supported!", blend_mode)); + 3 + } + }) { + errors.lock().unwrap().push(err); + } + DynamicImage::ImageRgba8(new_image) + }).collect(); + let errors_unlock = errors.lock().unwrap(); + if !errors_unlock.is_empty() { + return Err(Error::IconForge(errors_unlock.join("\n"))); + } + Ok(images_out) +} + +/// Blends a set of images with another set of images. +fn blend_images_other(images: Vec, mut images_other: Vec, blend_mode: &String) -> Result, Error> { + zone!("blend_images_other"); + let errors = Arc::new(Mutex::new(Vec::::new())); + let images_out: Vec; + if images_other.len() == 1 { // This is useful in the case where the something with 4+ dirs blends with 1dir + let first_image = images_other.remove(0).into_rgba8(); + images_out = images.into_par_iter().map(|image| { + zone!("blend_image_other_simple"); + let mut new_image = image.clone().into_rgba8(); + match blend_icon(&mut new_image, &first_image, &match blend_mode.as_str() { + "add" => 0, + "subtract" => 1, + "multiply" => 2, + "overlay" => 3, + "underlay" => 6, + _ => { + errors.lock().unwrap().push(format!("blend_mode '{}' is not supported!", blend_mode)); + 3 + } + }) { + Ok(_) => (), + Err(error) => { + errors.lock().unwrap().push(error); + } + }; + DynamicImage::ImageRgba8(new_image) + }).collect(); + } else { + images_out = (images, images_other).into_par_iter().map(|(image, image2)| { + zone!("blend_image_other"); + let mut new_image = image.clone().into_rgba8(); + match blend_icon(&mut new_image, &image2.into_rgba8(), &match blend_mode.as_str() { + "add" => 0, + "subtract" => 1, + "multiply" => 2, + "overlay" => 3, + "underlay" => 6, + _ => { + errors.lock().unwrap().push(format!("blend_mode '{}' is not supported!", blend_mode)); + 3 + } + }) { + Ok(_) => (), + Err(error) => { + errors.lock().unwrap().push(error); + } + }; + DynamicImage::ImageRgba8(new_image) + }).collect(); + } + let errors_unlock = errors.lock().unwrap(); + if !errors_unlock.is_empty() { + return Err(Error::IconForge(errors_unlock.join("\n"))); + } + Ok(images_out) +} + #[derive(Clone)] struct Rgba { r: f32, From 8bea71e550b5d8952d434dfbe6709aebfc95521d Mon Sep 17 00:00:00 2001 From: itsmeow Date: Wed, 10 Jan 2024 02:01:18 -0600 Subject: [PATCH 2/5] Add async load --- dmsrc/iconforge.dm | 5 +++-- src/iconforge.rs | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/dmsrc/iconforge.dm b/dmsrc/iconforge.dm index ec01869d..2e97b949 100644 --- a/dmsrc/iconforge.dm +++ b/dmsrc/iconforge.dm @@ -63,8 +63,9 @@ /// Returns "OK" if successful, otherwise, returns a string containing the error. #define rustg_iconforge_gags(config_path, colors, output_dmi_path) RUSTG_CALL(RUST_G, "iconforge_gags")("[config_path]", colors, output_dmi_path) /// Returns a job_id for use with rustg_iconforge_check() -#define rustg_iconforge_gags_async(config_path, colors, output_dmi_path) RUSTG_CALL(RUST_G, "iconforge_gags")("[config_path]", colors, output_dmi_path) - +#define rustg_iconforge_load_gags_config_async(config_path, config_json, config_icon_path) RUSTG_CALL(RUST_G, "iconforge_load_gags_config_async")("[config_path]", config_json, config_icon_path) +/// Returns a job_id for use with rustg_iconforge_check() +#define rustg_iconforge_gags_async(config_path, colors, output_dmi_path) RUSTG_CALL(RUST_G, "iconforge_gags_async")("[config_path]", colors, output_dmi_path) #define RUSTG_ICONFORGE_BLEND_COLOR "BlendColor" #define RUSTG_ICONFORGE_BLEND_ICON "BlendIcon" diff --git a/src/iconforge.rs b/src/iconforge.rs index ca9e82bf..75fb04d8 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -1,4 +1,4 @@ -// DMI spritesheet generator +// Multi-threaded DMI spritesheet generator and GAGS re-implementation // Developed by itsmeow use crate::{ byond::catch_panic, @@ -118,6 +118,20 @@ byond_fn!(fn iconforge_load_gags_config(config_path, config_json, config_icon_pa result }); +byond_fn!(fn iconforge_load_gags_config_async(config_path, config_json, config_icon_path) { + let config_path = config_path.to_owned(); + let config_json = config_json.to_owned(); + let config_icon_path = config_icon_path.to_owned(); + Some(jobs::start(move || { + let result = match catch_panic(|| load_gags_config(&config_path, &config_json, &config_icon_path)) { + Ok(o) => o.to_string(), + Err(e) => e.to_string() + }; + frame!(); + result + })) +}); + byond_fn!(fn iconforge_gags(config_path, colors, output_dmi_path) { let config_path = config_path.to_owned(); let colors = colors.to_owned(); From a3b32e21c2fe9bcdb164a0a380de62cdb940faec Mon Sep 17 00:00:00 2001 From: itsmeow Date: Wed, 10 Jan 2024 22:12:48 -0600 Subject: [PATCH 3/5] Format --- src/iconforge.rs | 387 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 274 insertions(+), 113 deletions(-) diff --git a/src/iconforge.rs b/src/iconforge.rs index 75fb04d8..df320141 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -9,9 +9,9 @@ use crate::{ use dashmap::DashMap; use dmi::{ dirs::Dirs, - icon::{Icon, IconState, DmiVersion}, + icon::{DmiVersion, Icon, IconState}, }; -use image::{Pixel, RgbaImage, DynamicImage}; +use image::{DynamicImage, Pixel, RgbaImage}; use once_cell::sync::Lazy; use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}; use serde::{Deserialize, Serialize}; @@ -409,9 +409,8 @@ fn generate_spritesheet( sprites_map.par_iter().for_each(|(sprite_name, icon)| { zone!("sprite_to_icons"); - icon_to_icons(icon) - .into_par_iter() - .for_each(|icon| match filepath_to_dmi(&icon.icon_file) { + icon_to_icons(icon).into_par_iter().for_each(|icon| { + match filepath_to_dmi(&icon.icon_file) { Ok(_) => { if hash_icons && !dmi_hashes.contains_key(&icon.icon_file) { zone!("hash_dmi"); @@ -427,7 +426,8 @@ fn generate_spritesheet( } } Err(err) => error.lock().unwrap().push(err), - }); + } + }); { zone!("map_to_base"); @@ -915,7 +915,11 @@ fn blend_color(image: &mut RgbaImage, color: &String, blend_mode: &u8) -> Result Ok(()) } -fn blend_icon(image: &mut RgbaImage, other_image: &RgbaImage, blend_mode: &u8) -> Result<(), String> { +fn blend_icon( + image: &mut RgbaImage, + other_image: &RgbaImage, + blend_mode: &u8, +) -> Result<(), String> { zone!("blend_icon"); for x in 0..std::cmp::min(image.width(), other_image.width()) { for y in 0..std::cmp::min(image.width(), other_image.width()) { @@ -1086,15 +1090,27 @@ enum GAGSLayer { ColorMatrix { blend_mode: String, color_matrix: [[f32; 4]; 5], - } + }, } impl GAGSLayer { fn get_blendmode(&self) -> String { match self { - GAGSLayer::IconState { icon_state: _, blend_mode, color_ids: _ } => blend_mode.to_owned(), - GAGSLayer::Reference { reference_type: _, icon_state: _, blend_mode, color_ids: _ } => blend_mode.to_owned(), - GAGSLayer::ColorMatrix { blend_mode, color_matrix: _ } => blend_mode.to_owned(), + GAGSLayer::IconState { + icon_state: _, + blend_mode, + color_ids: _, + } => blend_mode.to_owned(), + GAGSLayer::Reference { + reference_type: _, + icon_state: _, + blend_mode, + color_ids: _, + } => blend_mode.to_owned(), + GAGSLayer::ColorMatrix { + blend_mode, + color_matrix: _, + } => blend_mode.to_owned(), } } } @@ -1107,11 +1123,14 @@ struct GAGSData { config_icon: Arc, } -static GAGS_CACHE: Lazy> = - Lazy::new(DashMap::new); +static GAGS_CACHE: Lazy> = Lazy::new(DashMap::new); /// Loads a GAGS config and the requested DMIs into memory for use by iconforge_gags() -fn load_gags_config(config_path: &str, config_json: &str, config_icon_path: &str) -> Result { +fn load_gags_config( + config_path: &str, + config_json: &str, + config_icon_path: &str, +) -> Result { zone!("load_gags_config"); let gags_config: GAGSConfig; { @@ -1126,11 +1145,14 @@ fn load_gags_config(config_path: &str, config_json: &str, config_icon_path: &str }; { zone!("gags_insert_config"); - GAGS_CACHE.insert(config_path.to_owned(), GAGSData { - config: gags_config, - config_path: config_path.to_owned(), - config_icon: icon_data, - }); + GAGS_CACHE.insert( + config_path.to_owned(), + GAGSData { + config: gags_config, + config_path: config_path.to_owned(), + config_icon: icon_data, + }, + ); } Ok(String::from("OK")) } @@ -1141,11 +1163,18 @@ fn gags(config_path: &str, colors: &str, output_dmi_path: &str) -> Result config, None => { - return Err(Error::IconForge(format!("Provided config_path {} has not been loaded by iconforge_load_gags_config!", config_path))); + return Err(Error::IconForge(format!( + "Provided config_path {} has not been loaded by iconforge_load_gags_config!", + config_path + ))); } }; - let colors_vec = colors.split("#").map(|x| String::from("#") + x).filter(|x| x != "#").collect::>(); + let colors_vec = colors + .split("#") + .map(|x| String::from("#") + x) + .filter(|x| x != "#") + .collect::>(); let errors = Arc::new(Mutex::new(Vec::::new())); let output_states = Arc::new(Mutex::new(Vec::::new())); @@ -1200,8 +1229,13 @@ fn gags(config_path: &str, colors: &str, output_dmi_path: &str) -> Result Result, icon_state: &String, last_external_images: Option>, first_matched_state: &mut Option) -> Result, String> { +fn gags_internal( + config_path: &str, + colors_vec: &Vec, + icon_state: &String, + last_external_images: Option>, + first_matched_state: &mut Option, +) -> Result, String> { zone!("gags_internal"); let gags_data = match GAGS_CACHE.get(config_path) { Some(config) => config, @@ -1227,7 +1267,14 @@ fn gags_internal(config_path: &str, colors_vec: &Vec, icon_state: &Strin { zone!("gags_create_icon_state"); let mut first_matched_state_internal: Option = None; - let transformed_images = match generate_layer_groups_for_iconstate(icon_state, colors_vec, layer_groups, &gags_data, last_external_images, &mut first_matched_state_internal) { + let transformed_images = match generate_layer_groups_for_iconstate( + icon_state, + colors_vec, + layer_groups, + &gags_data, + last_external_images, + &mut first_matched_state_internal, + ) { Ok(images) => images, Err(err) => { return Err(err); @@ -1244,47 +1291,96 @@ fn gags_internal(config_path: &str, colors_vec: &Vec, icon_state: &Strin } /// Recursive function that parses out GAGS configs into layer groups. -fn generate_layer_groups_for_iconstate(state_name: &str, colors: &Vec, layer_groups: &Vec, gags_data: &GAGSData, last_external_images: Option>, first_matched_state: &mut Option) -> Result, String> { +fn generate_layer_groups_for_iconstate( + state_name: &str, + colors: &Vec, + layer_groups: &Vec, + gags_data: &GAGSData, + last_external_images: Option>, + first_matched_state: &mut Option, +) -> Result, String> { zone!("generate_layer_groups_for_iconstate"); let mut new_images: Option> = None; for option in layer_groups { zone!("process_gags_layergroup_option"); let (layer_images, blend_mode) = match option { - GAGSLayerGroupOption::GAGSLayer(layer) => (generate_layer_for_iconstate(state_name, colors, layer, gags_data, new_images.clone().or(last_external_images.clone()), first_matched_state)?, layer.get_blendmode()), + GAGSLayerGroupOption::GAGSLayer(layer) => ( + generate_layer_for_iconstate( + state_name, + colors, + layer, + gags_data, + new_images.clone().or(last_external_images.clone()), + first_matched_state, + )?, + layer.get_blendmode(), + ), GAGSLayerGroupOption::GAGSLayerGroup(layers) => { if layers.is_empty() { - return Err(format!("Empty layer group provided to GAGS state {} for GAGS config {} !", state_name, gags_data.config_path)); + return Err(format!( + "Empty layer group provided to GAGS state {} for GAGS config {} !", + state_name, gags_data.config_path + )); } - (generate_layer_groups_for_iconstate(state_name, colors, layers, gags_data, new_images.clone().or(last_external_images.clone()), first_matched_state)?, match layers.first().unwrap() { - GAGSLayerGroupOption::GAGSLayer(layer) => layer.get_blendmode(), - GAGSLayerGroupOption::GAGSLayerGroup(_) => { - return Err(format!("Layer group began with another layer group in GAGS state {} for GAGS config {} !", state_name, gags_data.config_path)); - } - }) + ( + generate_layer_groups_for_iconstate( + state_name, + colors, + layers, + gags_data, + new_images.clone().or(last_external_images.clone()), + first_matched_state, + )?, + match layers.first().unwrap() { + GAGSLayerGroupOption::GAGSLayer(layer) => layer.get_blendmode(), + GAGSLayerGroupOption::GAGSLayerGroup(_) => { + return Err(format!("Layer group began with another layer group in GAGS state {} for GAGS config {} !", state_name, gags_data.config_path)); + } + }, + ) } }; new_images = match new_images { Some(images) => Some(blend_images_other(images, layer_images, &blend_mode)?), - None => Some(layer_images) + None => Some(layer_images), } } match new_images { Some(images) => Ok(images), - None => Err(format!("No image found for GAGS state {}", state_name)) + None => Err(format!("No image found for GAGS state {}", state_name)), } } /// Generates a specific layer. -fn generate_layer_for_iconstate(state_name: &str, colors: &Vec, layer: &GAGSLayer, gags_data: &GAGSData, new_images: Option>, first_matched_state: &mut Option) -> Result, String> { +fn generate_layer_for_iconstate( + state_name: &str, + colors: &Vec, + layer: &GAGSLayer, + gags_data: &GAGSData, + new_images: Option>, + first_matched_state: &mut Option, +) -> Result, String> { zone!("generate_layer_for_iconstate"); let images_result: Option> = match layer { - GAGSLayer::IconState { icon_state, blend_mode: _, color_ids } => { + GAGSLayer::IconState { + icon_state, + blend_mode: _, + color_ids, + } => { zone!("gags_layer_type_icon_state"); - let icon_state: &IconState = match gags_data.config_icon.states.iter().find(|state| state.name == *icon_state) { + let icon_state: &IconState = match gags_data + .config_icon + .states + .iter() + .find(|state| state.name == *icon_state) + { Some(state) => state, None => { - return Err(format!("Invalid icon_state {} in layer provided for GAGS config {}", state_name, gags_data.config_path)); + return Err(format!( + "Invalid icon_state {} in layer provided for GAGS config {}", + state_name, gags_data.config_path + )); } }; @@ -1299,53 +1395,93 @@ fn generate_layer_for_iconstate(state_name: &str, colors: &Vec, layer: & GAGSColorID::GAGSColorIndex(idx) => colors.get(*idx as usize - 1).unwrap(), GAGSColorID::GAGSColorStatic(color) => color, }; - return Ok(blend_images_color(images, actual_color, &String::from("multiply"))?); + return Ok(blend_images_color( + images, + actual_color, + &String::from("multiply"), + )?); } else { return Ok(images); // this will get blended by the layergroup. } - }, - GAGSLayer::Reference { reference_type, icon_state, blend_mode: _, color_ids } => { + } + GAGSLayer::Reference { + reference_type, + icon_state, + blend_mode: _, + color_ids, + } => { zone!("gags_layer_type_reference"); let mut colors_in: Vec = colors.clone(); if !color_ids.is_empty() { - colors_in = color_ids.iter().map(|color| match color { - GAGSColorID::GAGSColorIndex(idx) => colors.get(*idx as usize - 1).unwrap().clone(), - GAGSColorID::GAGSColorStatic(color) => color.clone(), - }).collect(); + colors_in = color_ids + .iter() + .map(|color| match color { + GAGSColorID::GAGSColorIndex(idx) => { + colors.get(*idx as usize - 1).unwrap().clone() + } + GAGSColorID::GAGSColorStatic(color) => color.clone(), + }) + .collect(); } - Some(gags_internal(reference_type, &colors_in, icon_state, new_images, first_matched_state)?) - }, - GAGSLayer::ColorMatrix { blend_mode: _, color_matrix: _ } => new_images, // unsupported! TROLLED! + Some(gags_internal( + reference_type, + &colors_in, + icon_state, + new_images, + first_matched_state, + )?) + } + GAGSLayer::ColorMatrix { + blend_mode: _, + color_matrix: _, + } => new_images, // unsupported! TROLLED! }; match images_result { Some(images) => Ok(images), - None => Err(format!("No images found for GAGS state {} for GAGS config {} !", state_name, gags_data.config_path)) + None => Err(format!( + "No images found for GAGS state {} for GAGS config {} !", + state_name, gags_data.config_path + )), } } /// Blends a set of images with a color. -fn blend_images_color(images: Vec, color: &String, blend_mode: &String) -> Result, Error> { +fn blend_images_color( + images: Vec, + color: &String, + blend_mode: &String, +) -> Result, Error> { zone!("blend_images_color"); let errors = Arc::new(Mutex::new(Vec::::new())); - let images_out = images.into_par_iter().map(|image| { - zone!("blend_image_color"); - let mut new_image = image.clone().into_rgba8(); - if let Err(err) = blend_color(&mut new_image, color, &match blend_mode.as_str() { - "add" => 0, - "subtract" => 1, - "multiply" => 2, - "overlay" => 3, - "underlay" => 6, - _ => { - errors.lock().unwrap().push(format!("blend_mode '{}' is not supported!", blend_mode)); - 3 + let images_out = images + .into_par_iter() + .map(|image| { + zone!("blend_image_color"); + let mut new_image = image.clone().into_rgba8(); + if let Err(err) = blend_color( + &mut new_image, + color, + &match blend_mode.as_str() { + "add" => 0, + "subtract" => 1, + "multiply" => 2, + "overlay" => 3, + "underlay" => 6, + _ => { + errors + .lock() + .unwrap() + .push(format!("blend_mode '{}' is not supported!", blend_mode)); + 3 + } + }, + ) { + errors.lock().unwrap().push(err); } - }) { - errors.lock().unwrap().push(err); - } - DynamicImage::ImageRgba8(new_image) - }).collect(); + DynamicImage::ImageRgba8(new_image) + }) + .collect(); let errors_unlock = errors.lock().unwrap(); if !errors_unlock.is_empty() { return Err(Error::IconForge(errors_unlock.join("\n"))); @@ -1354,55 +1490,80 @@ fn blend_images_color(images: Vec, color: &String, blend_mode: &St } /// Blends a set of images with another set of images. -fn blend_images_other(images: Vec, mut images_other: Vec, blend_mode: &String) -> Result, Error> { +fn blend_images_other( + images: Vec, + mut images_other: Vec, + blend_mode: &String, +) -> Result, Error> { zone!("blend_images_other"); let errors = Arc::new(Mutex::new(Vec::::new())); let images_out: Vec; - if images_other.len() == 1 { // This is useful in the case where the something with 4+ dirs blends with 1dir + if images_other.len() == 1 { + // This is useful in the case where the something with 4+ dirs blends with 1dir let first_image = images_other.remove(0).into_rgba8(); - images_out = images.into_par_iter().map(|image| { - zone!("blend_image_other_simple"); - let mut new_image = image.clone().into_rgba8(); - match blend_icon(&mut new_image, &first_image, &match blend_mode.as_str() { - "add" => 0, - "subtract" => 1, - "multiply" => 2, - "overlay" => 3, - "underlay" => 6, - _ => { - errors.lock().unwrap().push(format!("blend_mode '{}' is not supported!", blend_mode)); - 3 - } - }) { - Ok(_) => (), - Err(error) => { - errors.lock().unwrap().push(error); - } - }; - DynamicImage::ImageRgba8(new_image) - }).collect(); + images_out = images + .into_par_iter() + .map(|image| { + zone!("blend_image_other_simple"); + let mut new_image = image.clone().into_rgba8(); + match blend_icon( + &mut new_image, + &first_image, + &match blend_mode.as_str() { + "add" => 0, + "subtract" => 1, + "multiply" => 2, + "overlay" => 3, + "underlay" => 6, + _ => { + errors + .lock() + .unwrap() + .push(format!("blend_mode '{}' is not supported!", blend_mode)); + 3 + } + }, + ) { + Ok(_) => (), + Err(error) => { + errors.lock().unwrap().push(error); + } + }; + DynamicImage::ImageRgba8(new_image) + }) + .collect(); } else { - images_out = (images, images_other).into_par_iter().map(|(image, image2)| { - zone!("blend_image_other"); - let mut new_image = image.clone().into_rgba8(); - match blend_icon(&mut new_image, &image2.into_rgba8(), &match blend_mode.as_str() { - "add" => 0, - "subtract" => 1, - "multiply" => 2, - "overlay" => 3, - "underlay" => 6, - _ => { - errors.lock().unwrap().push(format!("blend_mode '{}' is not supported!", blend_mode)); - 3 - } - }) { - Ok(_) => (), - Err(error) => { - errors.lock().unwrap().push(error); - } - }; - DynamicImage::ImageRgba8(new_image) - }).collect(); + images_out = (images, images_other) + .into_par_iter() + .map(|(image, image2)| { + zone!("blend_image_other"); + let mut new_image = image.clone().into_rgba8(); + match blend_icon( + &mut new_image, + &image2.into_rgba8(), + &match blend_mode.as_str() { + "add" => 0, + "subtract" => 1, + "multiply" => 2, + "overlay" => 3, + "underlay" => 6, + _ => { + errors + .lock() + .unwrap() + .push(format!("blend_mode '{}' is not supported!", blend_mode)); + 3 + } + }, + ) { + Ok(_) => (), + Err(error) => { + errors.lock().unwrap().push(error); + } + }; + DynamicImage::ImageRgba8(new_image) + }) + .collect(); } let errors_unlock = errors.lock().unwrap(); if !errors_unlock.is_empty() { From c6f991d016845a771925022fc7014d34473969c2 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Mon, 16 Sep 2024 21:46:27 -0500 Subject: [PATCH 4/5] Clippy --- src/iconforge.rs | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/iconforge.rs b/src/iconforge.rs index df320141..5747f8ba 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -940,11 +940,7 @@ fn blend_icon( fn transform_image(image: &mut RgbaImage, transform: &Transform) -> Result<(), String> { zone!("transform_image"); match transform { - Transform::BlendColor { color, blend_mode } => { - if let Err(err) = blend_color(image, color, blend_mode) { - return Err(err); - } - } + Transform::BlendColor { color, blend_mode } => blend_color(image, color, blend_mode)?, Transform::BlendIcon { icon, blend_mode } => { zone!("blend_icon"); let (mut other_image, cached) = @@ -953,9 +949,7 @@ fn transform_image(image: &mut RgbaImage, transform: &Transform) -> Result<(), S if !cached { apply_all_transforms(&mut other_image, &icon.transform)?; }; - if let Err(err) = blend_icon(image, &other_image, blend_mode) { - return Err(err); - } + blend_icon(image, &other_image, blend_mode)?; if let Err(err) = return_image(other_image, icon) { return Err(err.to_string()); } @@ -1171,7 +1165,7 @@ fn gags(config_path: &str, colors: &str, output_dmi_path: &str) -> Result>(); @@ -1234,7 +1228,7 @@ fn gags(config_path: &str, colors: &str, output_dmi_path: &str) -> Result, + colors: &[String], layer: &GAGSLayer, gags_data: &GAGSData, new_images: Option>, @@ -1411,7 +1405,7 @@ fn generate_layer_for_iconstate( color_ids, } => { zone!("gags_layer_type_reference"); - let mut colors_in: Vec = colors.clone(); + let mut colors_in: Vec = colors.to_owned(); if !color_ids.is_empty() { colors_in = color_ids .iter() @@ -1497,11 +1491,10 @@ fn blend_images_other( ) -> Result, Error> { zone!("blend_images_other"); let errors = Arc::new(Mutex::new(Vec::::new())); - let images_out: Vec; - if images_other.len() == 1 { + let images_out: Vec = if images_other.len() == 1 { // This is useful in the case where the something with 4+ dirs blends with 1dir let first_image = images_other.remove(0).into_rgba8(); - images_out = images + images .into_par_iter() .map(|image| { zone!("blend_image_other_simple"); @@ -1531,9 +1524,9 @@ fn blend_images_other( }; DynamicImage::ImageRgba8(new_image) }) - .collect(); + .collect() } else { - images_out = (images, images_other) + (images, images_other) .into_par_iter() .map(|(image, image2)| { zone!("blend_image_other"); @@ -1563,8 +1556,8 @@ fn blend_images_other( }; DynamicImage::ImageRgba8(new_image) }) - .collect(); - } + .collect() + }; let errors_unlock = errors.lock().unwrap(); if !errors_unlock.is_empty() { return Err(Error::IconForge(errors_unlock.join("\n"))); From 402de43810638c2817a9ac0c21e3eee5c0f2f6d6 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Wed, 18 Sep 2024 01:05:13 -0500 Subject: [PATCH 5/5] Update README to reflect new functionality --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3f36e0a8..0bd6ed3c 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ Additional features are: * allow_non_32bit: Disables the forced compile errors on non-32bit targets. Only use this if you know exactly what you are doing. * batchnoise: Discrete Batched Perlin-like Noise, fast and multi-threaded - sent over once instead of having to query for every tile. * hash: Faster replacement for `md5`, support for SHA-1, SHA-256, and SHA-512. Requires OpenSSL on Linux. -* iconforge: A much faster replacement for the spritesheet generation system used by [/tg/station]. +* iconforge: A much faster replacement for various bulk DM /icon operations such as [/tg/station]'s asset subsystem spritesheet generation and GAGS bundle generation. * pathfinder: An a* pathfinder used for finding the shortest path in a static node map. Not to be used for a non-static map. * poissonnoise: A way to generate a 2D poisson disk distribution ('blue noise'), which is relatively uniform. * redis_pubsub: Library for sending and receiving messages through Redis.