Skip to content

Commit da863cb

Browse files
authored
DMI metadata reading and injection (#234)
1 parent e166a2f commit da863cb

File tree

5 files changed

+210
-3
lines changed

5 files changed

+210
-3
lines changed

Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ redis = { version = "0.32", optional = true, features = ["ahash"] }
4747
ureq = { version = "2.12", optional = true }
4848
serde = { version = "1.0", optional = true, features = ["derive"] }
4949
serde_json = { version = "1.0", optional = true, features = ["preserve_order"] }
50+
serde_repr = { version = "0.1", optional = true }
5051
once_cell = { version = "1.21", optional = true }
5152
mysql = { git = "https://github.com/ZeWaka/rust-mysql-simple.git", tag = "v26.0.0", default-features = false, optional = true }
5253
dashmap = { version = "6.1", optional = true, features = ["rayon", "serde"] }
@@ -66,7 +67,11 @@ fast_poisson = { version = "1.0.2", optional = true, features = [
6667
] }
6768
symphonia = { version = "0.5.4", optional = true, features = ["all-codecs"] }
6869
caith = { version = "4.2.4", optional = true }
69-
uuid = { version = "1.17", optional = true, features = ["v4", "v7", "fast-rng"] }
70+
uuid = { version = "1.17", optional = true, features = [
71+
"v4",
72+
"v7",
73+
"fast-rng",
74+
] }
7075
cuid2 = { version = "0.1.4", optional = true }
7176
indexmap = { version = "2.9.0", optional = true, features = ["serde", "rayon"] }
7277
ordered-float = { version = "5.0.0", optional = true, features = ["serde"] }
@@ -129,7 +134,7 @@ all = [
129134
acreplace = ["aho-corasick"]
130135
batchnoise = ["dbpnoise"]
131136
cellularnoise = ["rand", "rayon"]
132-
dmi = ["png", "image"]
137+
dmi = ["png", "image", "serde_repr"]
133138
file = []
134139
git = ["gix", "chrono"]
135140
hash = [

dmsrc/dmi.dm

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,33 @@
77
* output: json_encode'd list. json_decode to get a flat list with icon states in the order they're in inside the .dmi
88
*/
99
#define rustg_dmi_icon_states(fname) RUSTG_CALL(RUST_G, "dmi_icon_states")(fname)
10+
11+
/**
12+
* The below functions involve dmi metadata represented in the following format:
13+
* list(
14+
* "width": number,
15+
* "height": number,
16+
* "states": list([STATE_DATA], ...)
17+
* )
18+
*
19+
* STATE_DATA format:
20+
* list(
21+
* "name": string,
22+
* "dirs": 1 | 4 | 8,
23+
* "delays"?: list(number, ...),
24+
* "rewind"?: TRUE | FALSE,
25+
* "movement"?: TRUE | FALSE,
26+
* "loop"?: number
27+
* )
28+
*/
29+
30+
/**
31+
* Get the dmi metadata of the file located at `fname`.
32+
* Returns a list in the metadata format listed above, or an error message.
33+
*/
34+
#define rustg_dmi_read_metadata(fname) json_decode(RUSTG_CALL(RUST_G, "dmi_read_metadata")(fname))
35+
/**
36+
* Inject dmi metadata into a png file located at `path`.
37+
* `metadata` must be a json_encode'd list in the metadata format listed above.
38+
*/
39+
#define rustg_dmi_inject_metadata(path, metadata) RUSTG_CALL(RUST_G, "dmi_inject_metadata")(path, metadata)

src/dmi.rs

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
use crate::error::{Error, Result};
2-
use png::{Decoder, Encoder, OutputInfo, Reader};
2+
use dmi::{
3+
error::DmiError,
4+
icon::{Icon, Looping},
5+
};
6+
use png::{text_metadata::ZTXtChunk, Decoder, Encoder, OutputInfo, Reader};
7+
use serde::{Deserialize, Serialize};
8+
use serde_repr::{Deserialize_repr, Serialize_repr};
39
use std::{
10+
fmt::Write,
411
fs::{create_dir_all, File},
512
io::BufReader,
13+
num::NonZeroU32,
614
path::Path,
715
};
816

@@ -30,6 +38,17 @@ byond_fn!(fn dmi_icon_states(path) {
3038
read_states(path).ok()
3139
});
3240

41+
byond_fn!(fn dmi_read_metadata(path) {
42+
match read_metadata(path) {
43+
Ok(metadata) => Some(metadata),
44+
Err(error) => Some(serde_json::to_string(&error.to_string()).unwrap()),
45+
}
46+
});
47+
48+
byond_fn!(fn dmi_inject_metadata(path, metadata) {
49+
inject_metadata(path, metadata).err()
50+
});
51+
3352
fn strip_metadata(path: &str) -> Result<()> {
3453
let (reader, frame_info, image) = read_png(path)?;
3554
write_png(path, &reader, &frame_info, &image, true)
@@ -146,3 +165,140 @@ fn read_states(path: &str) -> Result<String> {
146165
}
147166
Ok(serde_json::to_string(&states)?)
148167
}
168+
169+
#[derive(Serialize_repr, Deserialize_repr, Clone, Copy)]
170+
#[repr(u8)]
171+
enum DmiStateDirCount {
172+
One = 1,
173+
Four = 4,
174+
Eight = 8,
175+
}
176+
177+
impl TryFrom<u8> for DmiStateDirCount {
178+
type Error = u8;
179+
fn try_from(value: u8) -> std::result::Result<Self, Self::Error> {
180+
match value {
181+
1 => Ok(Self::One),
182+
4 => Ok(Self::Four),
183+
8 => Ok(Self::Eight),
184+
n => Err(n),
185+
}
186+
}
187+
}
188+
189+
#[derive(Serialize, Deserialize)]
190+
struct DmiState {
191+
name: String,
192+
dirs: DmiStateDirCount,
193+
#[serde(default)]
194+
delay: Option<Vec<f32>>,
195+
#[serde(default)]
196+
rewind: Option<u8>,
197+
#[serde(default)]
198+
movement: Option<u8>,
199+
#[serde(default)]
200+
loop_count: Option<NonZeroU32>,
201+
#[serde(default)]
202+
hotspot: Option<(u32, u32, u32)>,
203+
}
204+
205+
#[derive(Serialize, Deserialize)]
206+
struct DmiMetadata {
207+
width: u32,
208+
height: u32,
209+
states: Vec<DmiState>,
210+
}
211+
212+
fn read_metadata(path: &str) -> Result<String> {
213+
let dmi = Icon::load(File::open(path).map(BufReader::new)?)?;
214+
let metadata = DmiMetadata {
215+
width: dmi.width,
216+
height: dmi.height,
217+
states: dmi
218+
.states
219+
.iter()
220+
.map(|state| {
221+
Ok(DmiState {
222+
name: state.name.clone(),
223+
dirs: DmiStateDirCount::try_from(state.dirs).map_err(|n| {
224+
DmiError::IconState(format!(
225+
"State \"{}\" has invalid dir count (expected 1, 4, or 8, got {})",
226+
state.name, n
227+
))
228+
})?,
229+
delay: state.delay.clone(),
230+
movement: state.movement.then_some(1),
231+
rewind: state.rewind.then_some(1),
232+
loop_count: match state.loop_flag {
233+
Looping::Indefinitely => None,
234+
Looping::NTimes(n) => Some(n),
235+
},
236+
hotspot: state.hotspot.map(|hotspot| (hotspot.x, hotspot.y, 1)),
237+
})
238+
})
239+
.collect::<Result<Vec<DmiState>>>()?,
240+
};
241+
Ok(serde_json::to_string(&metadata)?)
242+
}
243+
244+
fn inject_metadata(path: &str, metadata: &str) -> Result<()> {
245+
let read_file = File::open(path).map(BufReader::new)?;
246+
let decoder = png::Decoder::new(read_file);
247+
let mut reader = decoder.read_info().map_err(|_| Error::InvalidPngData)?;
248+
let new_dmi_metadata: DmiMetadata = serde_json::from_str(metadata)?;
249+
let mut new_metadata_string = String::new();
250+
writeln!(new_metadata_string, "# BEGIN DMI")?;
251+
writeln!(new_metadata_string, "version = 4.0")?;
252+
writeln!(new_metadata_string, "\twidth = {}", new_dmi_metadata.width)?;
253+
writeln!(
254+
new_metadata_string,
255+
"\theight = {}",
256+
new_dmi_metadata.height
257+
)?;
258+
for state in new_dmi_metadata.states {
259+
writeln!(new_metadata_string, "state = \"{}\"", state.name)?;
260+
writeln!(new_metadata_string, "\tdirs = {}", state.dirs as u8)?;
261+
writeln!(
262+
new_metadata_string,
263+
"\tframes = {}",
264+
state.delay.as_ref().map_or(1, Vec::len)
265+
)?;
266+
if let Some(delay) = state.delay {
267+
writeln!(
268+
new_metadata_string,
269+
"\tdelay = {}",
270+
delay
271+
.iter()
272+
.map(f32::to_string)
273+
.collect::<Vec<_>>()
274+
.join(",")
275+
)?;
276+
}
277+
if state.rewind.is_some_and(|r| r != 0) {
278+
writeln!(new_metadata_string, "\trewind = 1")?;
279+
}
280+
if state.movement.is_some_and(|m| m != 0) {
281+
writeln!(new_metadata_string, "\tmovement = 1")?;
282+
}
283+
if let Some(loop_count) = state.loop_count {
284+
writeln!(new_metadata_string, "\tloop = {loop_count}")?;
285+
}
286+
if let Some((hotspot_x, hotspot_y, hotspot_frame)) = state.hotspot {
287+
writeln!(
288+
new_metadata_string,
289+
"\totspot = {hotspot_x},{hotspot_y},{hotspot_frame}"
290+
)?;
291+
}
292+
}
293+
writeln!(new_metadata_string, "# END DMI")?;
294+
let mut info = reader.info().clone();
295+
info.compressed_latin1_text
296+
.push(ZTXtChunk::new("Description", new_metadata_string));
297+
let mut raw_image_data: Vec<u8> = vec![];
298+
while let Some(row) = reader.next_row()? {
299+
raw_image_data.append(&mut row.data().to_vec());
300+
}
301+
let encoder = png::Encoder::with_info(File::create(path)?, info)?;
302+
encoder.write_header()?.write_image_data(&raw_image_data)?;
303+
Ok(())
304+
}

src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ pub enum Error {
7171
#[cfg(feature = "dice")]
7272
#[error(transparent)]
7373
DiceRoll(#[from] caith::RollError),
74+
#[error(transparent)]
75+
Formatting(#[from] std::fmt::Error),
76+
#[error(transparent)]
77+
Dmi(#[from] dmi::error::DmiError),
7478
#[error("Panic during function execution: {0}")]
7579
Panic(String),
7680
}

0 commit comments

Comments
 (0)