From 173449f61af55164e01dc0e876d25f945718a429 Mon Sep 17 00:00:00 2001 From: Y0SH1M4S73R Date: Tue, 15 Jul 2025 22:04:25 -0400 Subject: [PATCH 1/4] DMI metadata injection --- Cargo.lock | 12 +++++++ Cargo.toml | 9 +++-- dmsrc/dmi.dm | 22 ++++++++++++ src/dmi.rs | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++- src/error.rs | 2 ++ 5 files changed, 137 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 58b92425..95754e56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2865,6 +2865,7 @@ dependencies = [ "regex", "serde", "serde_json", + "serde_repr", "sha-1", "sha2", "symphonia", @@ -3053,6 +3054,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_spanned" version = "0.6.9" diff --git a/Cargo.toml b/Cargo.toml index a3197ceb..3f8cd5e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ redis = { version = "0.32", optional = true, features = ["ahash"] } ureq = { version = "2.12", optional = true } serde = { version = "1.0", optional = true, features = ["derive"] } serde_json = { version = "1.0", optional = true } +serde_repr = { version = "0.1", optional = true } once_cell = { version = "1.21", optional = true } mysql = { git = "https://github.com/ZeWaka/rust-mysql-simple.git", tag = "v26.0.0", default-features = false, optional = true } dashmap = { version = "6.1", optional = true, features = ["rayon", "serde"] } @@ -66,7 +67,11 @@ fast_poisson = { version = "1.0.2", optional = true, features = [ ] } symphonia = { version = "0.5.4", optional = true, features = ["all-codecs"] } caith = { version = "4.2.4", optional = true } -uuid = { version = "1.17", optional = true, features = ["v4", "v7", "fast-rng"] } +uuid = { version = "1.17", optional = true, features = [ + "v4", + "v7", + "fast-rng", +] } cuid2 = { version = "0.1.4", optional = true } [features] @@ -127,7 +132,7 @@ all = [ acreplace = ["aho-corasick"] batchnoise = ["dbpnoise"] cellularnoise = ["rand", "rayon"] -dmi = ["png", "image"] +dmi = ["png", "image", "serde_repr"] file = [] git = ["gix", "chrono"] hash = [ diff --git a/dmsrc/dmi.dm b/dmsrc/dmi.dm index 9ebb8edc..d35d0b8f 100644 --- a/dmsrc/dmi.dm +++ b/dmsrc/dmi.dm @@ -7,3 +7,25 @@ * output: json_encode'd list. json_decode to get a flat list with icon states in the order they're in inside the .dmi */ #define rustg_dmi_icon_states(fname) RUSTG_CALL(RUST_G, "dmi_icon_states")(fname) + +/** + * Inject dmi metadata into a png file located at `path` + * + * `metadata` format: + * list( + * "width": number, + * "height": number, + * "states": list([STATE_DATA], ...) + * ) + * + * STATE_DATA format: + * list( + * "name": string, + * "dirs": 1 | 4 | 8, + * "delays"?: list(number, ...), + * "rewind"?: TRUE | FALSE, + * "movement"?: TRUE | FALSE, + * "loop"?: number + * ) + */ +#define rustg_dmi_inject_metadata(path, metadata) RUSTG_CALL(RUST_G, "dmi_inject_metadata")(path, metadata) diff --git a/src/dmi.rs b/src/dmi.rs index e2b0ddbc..57e5a292 100644 --- a/src/dmi.rs +++ b/src/dmi.rs @@ -1,8 +1,12 @@ use crate::error::{Error, Result}; -use png::{Decoder, Encoder, OutputInfo, Reader}; +use png::{text_metadata::ZTXtChunk, Decoder, Encoder, OutputInfo, Reader}; +use serde::Deserialize; +use serde_repr::Deserialize_repr; use std::{ + fmt::Write, fs::{create_dir_all, File}, io::BufReader, + num::NonZeroU32, path::Path, }; @@ -30,6 +34,10 @@ byond_fn!(fn dmi_icon_states(path) { read_states(path).ok() }); +byond_fn!(fn dmi_inject_metadata(path, metadata) { + inject_metadata(path, metadata).err() +}); + fn strip_metadata(path: &str) -> Result<()> { let (reader, frame_info, image) = read_png(path)?; write_png(path, &reader, &frame_info, &image, true) @@ -146,3 +154,88 @@ fn read_states(path: &str) -> Result { } Ok(serde_json::to_string(&states)?) } + +#[derive(Deserialize_repr, Clone, Copy)] +#[repr(u8)] +enum DmiStateDirCount { + One = 1, + Four = 4, + Eight = 8, +} + +#[derive(Deserialize)] +struct DmiState { + name: String, + dirs: DmiStateDirCount, + #[serde(default)] + delay: Option>, + #[serde(default)] + rewind: Option, + #[serde(default)] + movement: Option, + #[serde(default)] + loop_count: Option, +} + +#[derive(Deserialize)] +struct DmiMetadata { + width: u32, + height: u32, + states: Vec, +} + +fn inject_metadata(path: &str, metadata: &str) -> Result<()> { + let read_file = File::open(path).map(BufReader::new)?; + let decoder = png::Decoder::new(read_file); + let mut reader = decoder.read_info().map_err(|_| Error::InvalidPngData)?; + let new_dmi_metadata: DmiMetadata = serde_json::from_str(metadata)?; + let mut new_metadata_string = String::new(); + writeln!(new_metadata_string, "# BEGIN DMI")?; + writeln!(new_metadata_string, "version = 4.0")?; + writeln!(new_metadata_string, "\twidth = {}", new_dmi_metadata.width)?; + writeln!( + new_metadata_string, + "\theight = {}", + new_dmi_metadata.height + )?; + for state in new_dmi_metadata.states { + writeln!(new_metadata_string, "state = \"{}\"", state.name)?; + writeln!(new_metadata_string, "\tdirs = {}", state.dirs as u8)?; + writeln!( + new_metadata_string, + "\tframes = {}", + state.delay.as_ref().map_or(1, Vec::len) + )?; + if let Some(delay) = state.delay { + writeln!( + new_metadata_string, + "\tdelay = {}", + delay + .iter() + .map(f32::to_string) + .collect::>() + .join(",") + )?; + } + if state.rewind.is_some_and(|r| r != 0) { + writeln!(new_metadata_string, "\trewind = 1")?; + } + if state.movement.is_some_and(|m| m != 0) { + writeln!(new_metadata_string, "\tmovement = 1")?; + } + if let Some(loop_count) = state.loop_count { + writeln!(new_metadata_string, "\tloop = {}", loop_count)?; + } + } + writeln!(new_metadata_string, "# END DMI")?; + let mut info = reader.info().clone(); + info.compressed_latin1_text + .push(ZTXtChunk::new("Description", new_metadata_string)); + let mut raw_image_data: Vec = vec![]; + while let Some(row) = reader.next_row()? { + raw_image_data.append(&mut row.data().to_vec()); + } + let encoder = png::Encoder::with_info(File::create(path)?, info)?; + encoder.write_header()?.write_image_data(&raw_image_data)?; + Ok(()) +} diff --git a/src/error.rs b/src/error.rs index 87b94b9a..953712b6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -71,6 +71,8 @@ pub enum Error { #[cfg(feature = "dice")] #[error(transparent)] DiceRoll(#[from] caith::RollError), + #[error(transparent)] + Formatting(#[from] std::fmt::Error), #[error("Panic during function execution: {0}")] Panic(String), } From 4809a0ea5189c0143dbbbdf1712646821804a59a Mon Sep 17 00:00:00 2001 From: Y0SH1M4S73R Date: Wed, 16 Jul 2025 01:16:37 -0400 Subject: [PATCH 2/4] Update dmi.rs --- src/dmi.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dmi.rs b/src/dmi.rs index 57e5a292..19c66e07 100644 --- a/src/dmi.rs +++ b/src/dmi.rs @@ -224,7 +224,7 @@ fn inject_metadata(path: &str, metadata: &str) -> Result<()> { writeln!(new_metadata_string, "\tmovement = 1")?; } if let Some(loop_count) = state.loop_count { - writeln!(new_metadata_string, "\tloop = {}", loop_count)?; + writeln!(new_metadata_string, "\tloop = {loop_count}")?; } } writeln!(new_metadata_string, "# END DMI")?; From 904468f1d3707a3ad379f0a84502140b22766a1f Mon Sep 17 00:00:00 2001 From: Y0SH1M4S73R Date: Sat, 19 Jul 2025 03:46:18 -0400 Subject: [PATCH 3/4] alright here's your metadata reading function --- dmsrc/dmi.dm | 14 +++++++--- src/dmi.rs | 73 ++++++++++++++++++++++++++++++++++++++++++++++++---- src/error.rs | 2 ++ 3 files changed, 81 insertions(+), 8 deletions(-) diff --git a/dmsrc/dmi.dm b/dmsrc/dmi.dm index d35d0b8f..961903d1 100644 --- a/dmsrc/dmi.dm +++ b/dmsrc/dmi.dm @@ -9,9 +9,7 @@ #define rustg_dmi_icon_states(fname) RUSTG_CALL(RUST_G, "dmi_icon_states")(fname) /** - * Inject dmi metadata into a png file located at `path` - * - * `metadata` format: + * The below functions involve dmi metadata represented in the following format: * list( * "width": number, * "height": number, @@ -28,4 +26,14 @@ * "loop"?: number * ) */ + +/** + * Get the dmi metadata of the file located at `fname`. + * Returns a json_encode'd list in the metadata format listed above, or an error message. + */ +#define rustg_dmi_read_metadata(fname) RUSTG_CALL(RUST_G, "dmi_read_metadata")(fname) +/** + * Inject dmi metadata into a png file located at `path`. + * `metadata` must be a json_encode'd list in the metadata format listed above. + */ #define rustg_dmi_inject_metadata(path, metadata) RUSTG_CALL(RUST_G, "dmi_inject_metadata")(path, metadata) diff --git a/src/dmi.rs b/src/dmi.rs index 19c66e07..d24ec590 100644 --- a/src/dmi.rs +++ b/src/dmi.rs @@ -1,7 +1,11 @@ use crate::error::{Error, Result}; +use dmi::{ + error::DmiError, + icon::{Icon, Looping}, +}; use png::{text_metadata::ZTXtChunk, Decoder, Encoder, OutputInfo, Reader}; -use serde::Deserialize; -use serde_repr::Deserialize_repr; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; use std::{ fmt::Write, fs::{create_dir_all, File}, @@ -34,6 +38,13 @@ byond_fn!(fn dmi_icon_states(path) { read_states(path).ok() }); +byond_fn!(fn dmi_read_metadata(path) { + match read_metadata(path) { + Ok(metadata) => Some(metadata), + Err(error) => Some(format!("\"{error:?}\"")), + } +}); + byond_fn!(fn dmi_inject_metadata(path, metadata) { inject_metadata(path, metadata).err() }); @@ -155,7 +166,7 @@ fn read_states(path: &str) -> Result { Ok(serde_json::to_string(&states)?) } -#[derive(Deserialize_repr, Clone, Copy)] +#[derive(Serialize_repr, Deserialize_repr, Clone, Copy)] #[repr(u8)] enum DmiStateDirCount { One = 1, @@ -163,7 +174,19 @@ enum DmiStateDirCount { Eight = 8, } -#[derive(Deserialize)] +impl TryFrom for DmiStateDirCount { + type Error = u8; + fn try_from(value: u8) -> std::result::Result { + match value { + 1 => Ok(Self::One), + 4 => Ok(Self::Four), + 8 => Ok(Self::Eight), + n => Err(n), + } + } +} + +#[derive(Serialize, Deserialize)] struct DmiState { name: String, dirs: DmiStateDirCount, @@ -175,15 +198,49 @@ struct DmiState { movement: Option, #[serde(default)] loop_count: Option, + #[serde(default)] + hotspot: Option<(u32, u32, u32)>, } -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] struct DmiMetadata { width: u32, height: u32, states: Vec, } +fn read_metadata(path: &str) -> Result { + let dmi = Icon::load(File::open(path).map(BufReader::new)?)?; + let metadata = DmiMetadata { + width: dmi.width, + height: dmi.height, + states: dmi + .states + .iter() + .map(|state| { + Ok(DmiState { + name: state.name.clone(), + dirs: DmiStateDirCount::try_from(state.dirs).map_err(|n| { + DmiError::IconState(format!( + "State \"{}\" has invalid dir count (expected 1, 4, or 8, got {})", + state.name, n + )) + })?, + delay: state.delay.clone(), + movement: state.movement.then_some(1), + rewind: state.rewind.then_some(1), + loop_count: match state.loop_flag { + Looping::Indefinitely => None, + Looping::NTimes(n) => Some(n), + }, + hotspot: state.hotspot.map(|hotspot| (hotspot.x, hotspot.y, 1)), + }) + }) + .collect::>>()?, + }; + Ok(serde_json::to_string(&metadata)?) +} + fn inject_metadata(path: &str, metadata: &str) -> Result<()> { let read_file = File::open(path).map(BufReader::new)?; let decoder = png::Decoder::new(read_file); @@ -226,6 +283,12 @@ fn inject_metadata(path: &str, metadata: &str) -> Result<()> { if let Some(loop_count) = state.loop_count { writeln!(new_metadata_string, "\tloop = {loop_count}")?; } + if let Some((hotspot_x, hotspot_y, hotspot_frame)) = state.hotspot { + writeln!( + new_metadata_string, + "\totspot = {hotspot_x},{hotspot_y},{hotspot_frame}" + )?; + } } writeln!(new_metadata_string, "# END DMI")?; let mut info = reader.info().clone(); diff --git a/src/error.rs b/src/error.rs index 953712b6..0395db6d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -73,6 +73,8 @@ pub enum Error { DiceRoll(#[from] caith::RollError), #[error(transparent)] Formatting(#[from] std::fmt::Error), + #[error(transparent)] + Dmi(#[from] dmi::error::DmiError), #[error("Panic during function execution: {0}")] Panic(String), } From 9547bb2fc746da209724df519267da9aa5cf5988 Mon Sep 17 00:00:00 2001 From: Y0SH1M4S73R Date: Sat, 19 Jul 2025 20:55:51 -0400 Subject: [PATCH 4/4] slight tweak to `dmi_read_metadata` --- dmsrc/dmi.dm | 4 ++-- src/dmi.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dmsrc/dmi.dm b/dmsrc/dmi.dm index 961903d1..de64ed11 100644 --- a/dmsrc/dmi.dm +++ b/dmsrc/dmi.dm @@ -29,9 +29,9 @@ /** * Get the dmi metadata of the file located at `fname`. - * Returns a json_encode'd list in the metadata format listed above, or an error message. + * Returns a list in the metadata format listed above, or an error message. */ -#define rustg_dmi_read_metadata(fname) RUSTG_CALL(RUST_G, "dmi_read_metadata")(fname) +#define rustg_dmi_read_metadata(fname) json_decode(RUSTG_CALL(RUST_G, "dmi_read_metadata")(fname)) /** * Inject dmi metadata into a png file located at `path`. * `metadata` must be a json_encode'd list in the metadata format listed above. diff --git a/src/dmi.rs b/src/dmi.rs index d24ec590..2a075065 100644 --- a/src/dmi.rs +++ b/src/dmi.rs @@ -41,7 +41,7 @@ byond_fn!(fn dmi_icon_states(path) { byond_fn!(fn dmi_read_metadata(path) { match read_metadata(path) { Ok(metadata) => Some(metadata), - Err(error) => Some(format!("\"{error:?}\"")), + Err(error) => Some(serde_json::to_string(&error.to_string()).unwrap()), } });