From 76c29fd707086457ba550123e71aff34536ddad4 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Wed, 30 Jul 2025 05:29:15 -0700 Subject: [PATCH 01/25] Add load_meta, skipping all image parsing --- src/icon.rs | 259 +++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 86 +++++++++++++++- tests/dmi_ops.rs | 31 +++++- 3 files changed, 373 insertions(+), 3 deletions(-) diff --git a/src/icon.rs b/src/icon.rs index f728b6b..56b3a9d 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -304,6 +304,265 @@ impl Icon { }) } + /// Equivalent of load, but only parses the DMI header and leaves all image data empty. + pub fn load_meta(reader: R) -> Result { + let raw_dmi = RawDmi::load_meta(reader)?; + let chunk_ztxt = match &raw_dmi.chunk_ztxt { + Some(chunk) => chunk.clone(), + None => { + return Err(DmiError::Generic( + "Error loading icon: no zTXt chunk found.".to_string(), + )) + } + }; + let decompressed_text = chunk_ztxt.data.decode()?; + let decompressed_text = String::from_utf8(decompressed_text)?; + let mut decompressed_text = decompressed_text.lines(); + + let current_line = decompressed_text.next(); + if current_line != Some("# BEGIN DMI") { + return Err(DmiError::Generic(format!( + "Error loading icon: no DMI header found. Beginning: {:#?}", + current_line + ))); + }; + + let current_line = match decompressed_text.next() { + Some(thing) => thing, + None => { + return Err(DmiError::Generic( + "Error loading icon: no version header found.".to_string(), + )) + } + }; + let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); + if split_version.len() != 2 || split_version[0] != "version" { + return Err(DmiError::Generic(format!( + "Error loading icon: improper version header found: {:#?}", + split_version + ))); + }; + let version = split_version[1].to_string(); + + let current_line = match decompressed_text.next() { + Some(thing) => thing, + None => { + return Err(DmiError::Generic( + "Error loading icon: no width found.".to_string(), + )) + } + }; + let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); + if split_version.len() != 2 || split_version[0] != "\twidth" { + return Err(DmiError::Generic(format!( + "Error loading icon: improper width found: {:#?}", + split_version + ))); + }; + let width = split_version[1].parse::()?; + + let current_line = match decompressed_text.next() { + Some(thing) => thing, + None => { + return Err(DmiError::Generic( + "Error loading icon: no height found.".to_string(), + )) + } + }; + let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); + if split_version.len() != 2 || split_version[0] != "\theight" { + return Err(DmiError::Generic(format!( + "Error loading icon: improper height found: {:#?}", + split_version + ))); + }; + let height = split_version[1].parse::()?; + + if width == 0 || height == 0 { + return Err(DmiError::Generic(format!( + "Error loading icon: invalid width ({}) / height ({}) values.", + width, height + ))); + }; + + // Image time. + let mut reader = vec![]; + raw_dmi.save(&mut reader)?; + + let img_width = u32::from_be_bytes([ + raw_dmi.chunk_ihdr.data[0], + raw_dmi.chunk_ihdr.data[1], + raw_dmi.chunk_ihdr.data[2], + raw_dmi.chunk_ihdr.data[3], + ]); + let img_height = u32::from_be_bytes([ + raw_dmi.chunk_ihdr.data[4], + raw_dmi.chunk_ihdr.data[5], + raw_dmi.chunk_ihdr.data[6], + raw_dmi.chunk_ihdr.data[7], + ]); + + if img_width == 0 || img_height == 0 || img_width % width != 0 || img_height % height != 0 { + return Err(DmiError::Generic(format!("Error loading icon: invalid image width ({}) / height ({}) values. Missmatch with metadata width ({}) / height ({}).", img_width, img_height, width, height))); + }; + + let width_in_states = img_width / width; + let height_in_states = img_height / height; + let max_possible_states = width_in_states * height_in_states; + + let mut index = 0; + + let mut current_line = match decompressed_text.next() { + Some(thing) => thing, + None => { + return Err(DmiError::Generic( + "Error loading icon: no DMI trailer nor states found.".to_string(), + )) + } + }; + + let mut states = vec![]; + + loop { + if current_line.contains("# END DMI") { + break; + }; + + let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); + if split_version.len() != 2 || split_version[0] != "state" { + return Err(DmiError::Generic(format!( + "Error loading icon: improper state found: {:#?}", + split_version + ))); + }; + + let name = split_version[1].as_bytes(); + if !name.starts_with(b"\"") || !name.ends_with(b"\"") { + return Err(DmiError::Generic(format!("Error loading icon: invalid name icon_state found in metadata, should be preceded and succeeded by double-quotes (\"): {:#?}", name))); + }; + let name = match name.len() { + 0 | 1 => { + return Err(DmiError::Generic(format!( + "Error loading icon: invalid name icon_state found in metadata, improper size: {:#?}", + name + ))) + } + 2 => String::new(), //Only the quotes, empty name otherwise. + length => String::from_utf8(name[1..(length - 1)].to_vec())?, //Hacky way to trim. Blame the cool methods being nightly experimental. + }; + + let mut dirs = None; + let mut frames = None; + let mut delay = None; + let mut loop_flag = Looping::Indefinitely; + let mut rewind = false; + let mut movement = false; + let mut hotspot = None; + let mut unknown_settings = None; + + loop { + current_line = match decompressed_text.next() { + Some(thing) => thing, + None => { + return Err(DmiError::Generic( + "Error loading icon: no DMI trailer found.".to_string(), + )) + } + }; + + if current_line.contains("# END DMI") || current_line.contains("state = \"") { + break; + }; + let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); + if split_version.len() != 2 { + return Err(DmiError::Generic(format!( + "Error loading icon: improper state found: {:#?}", + split_version + ))); + }; + + match split_version[0] { + "\tdirs" => dirs = Some(split_version[1].parse::()?), + "\tframes" => frames = Some(split_version[1].parse::()?), + "\tdelay" => { + let mut delay_vector = vec![]; + let text_delays = split_version[1].split_terminator(','); + for text_entry in text_delays { + delay_vector.push(text_entry.parse::()?); + } + delay = Some(delay_vector); + } + "\tloop" => loop_flag = Looping::new(split_version[1].parse::()?), + "\trewind" => rewind = split_version[1].parse::()? != 0, + "\tmovement" => movement = split_version[1].parse::()? != 0, + "\thotspot" => { + let text_coordinates: Vec<&str> = split_version[1].split_terminator(',').collect(); + // Hotspot includes a mysterious 3rd parameter that always seems to be 1. + if text_coordinates.len() != 3 { + return Err(DmiError::Generic(format!( + "Error loading icon: improper hotspot found: {:#?}", + split_version + ))); + }; + hotspot = Some(Hotspot { + x: text_coordinates[0].parse::()?, + y: text_coordinates[1].parse::()?, + }); + } + _ => { + unknown_settings = match unknown_settings { + None => { + let mut new_map = HashMap::new(); + new_map.insert(split_version[0].to_string(), split_version[1].to_string()); + Some(new_map) + } + Some(mut thing) => { + thing.insert(split_version[0].to_string(), split_version[1].to_string()); + Some(thing) + } + }; + } + }; + } + + if dirs.is_none() || frames.is_none() { + return Err(DmiError::Generic(format!( + "Error loading icon: state lacks essential settings. dirs: {:#?}. frames: {:#?}.", + dirs, frames + ))); + }; + let dirs = dirs.unwrap(); + let frames = frames.unwrap(); + + let next_index = index + (dirs as u32 * frames); + if next_index > max_possible_states { + return Err(DmiError::Generic(format!("Error loading icon: metadata settings exceeded the maximum number of states possible ({}).", max_possible_states))); + }; + + index = next_index; + + states.push(IconState { + name, + dirs, + frames, + images: Vec::new(), + delay, + loop_flag, + rewind, + movement, + hotspot, + unknown_settings, + }); + } + + Ok(Icon { + version: DmiVersion(version), + width, + height, + states, + }) + } + pub fn save(&self, mut writter: &mut W) -> Result { let mut sprites = vec![]; let mut signature = format!( diff --git a/src/lib.rs b/src/lib.rs index e97250d..855046c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,10 +6,11 @@ pub mod icon; pub mod iend; pub mod ztxt; -use std::io::{Read, Write}; +use std::io::{Read, Seek, Write}; /// The PNG magic header pub const PNG_HEADER: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10]; +pub const IHDR_HEADER: [u8; 8] = [0, 0, 0, 13, 73, 72, 68, 82]; #[derive(Clone, Eq, PartialEq, Debug, Default)] pub struct RawDmi { @@ -55,7 +56,7 @@ impl RawDmi { let mut chunk_ihdr = None; let mut chunk_ztxt = None; let mut chunk_plte = None; - let mut chunks_idat = vec![]; + let mut chunks_idat: Vec = vec![]; let chunk_iend; let mut other_chunks = vec![]; @@ -121,6 +122,87 @@ impl RawDmi { }) } + /// Equivalent of load, but only parses IHDR and zTXt. May not catch an improperly formatted PNG file, because it only reads those headers. + pub fn load_meta(mut reader: R) -> Result { + // 8 bytes for the PNG file signature. + let mut png_header = [0u8; 8]; + reader.read_exact(&mut png_header)?; + if png_header != PNG_HEADER { + return Err(error::DmiError::Generic(format!( + "PNG header mismatch (expected {:#?}, found {:#?})", + PNG_HEADER, png_header + ))); + }; + // 4 (size) + 4 (type) + 13 (data) + 4 (crc) for the IHDR chunk. + let mut ihdr = [0u8; 25]; + reader.read_exact(&mut ihdr)?; + if ihdr[0..8] != IHDR_HEADER { + return Err(error::DmiError::Generic( + "Failed to load DMI. IHDR chunk is not in the correct location (1st chunk), has an invalid size, or an invalid identifier.".to_string(), + )); + } + let chunk_ihdr = chunk::RawGenericChunk::load(&mut &ihdr[0..25])?; + + let mut chunk_ztxt = None; + + loop { + // Read len + let mut chunk_len_be: [u8; 4] = [0u8; 4]; + reader.read_exact(&mut chunk_len_be)?; + let chunk_len = u32::from_be_bytes(chunk_len_be) as usize; + + // Create vec for full chunk data + let mut chunk_full: Vec = Vec::with_capacity(chunk_len + 12); + chunk_full.extend_from_slice(&chunk_len_be); + + // Read header into full chunk data + let mut chunk_header = [0u8; 4]; + reader.read_exact(&mut chunk_header)?; + chunk_full.extend_from_slice(&chunk_header); + + // Skip non-zTXt chunks + if &chunk_header != b"zTXt" { + // If we encounter IDAT or IEND we can just break because the zTXt header aint happening + if &chunk_header == b"IDAT" || &chunk_header == b"IEND" { + break; + } + reader.seek_relative((chunk_len + 4) as i64)?; + continue; + } + + // Read actual chunk data and append + let mut chunk_data = vec![0; chunk_len]; + reader.read_exact(&mut chunk_data)?; + chunk_full.extend_from_slice(&chunk_data); + + // Read CRC into full chunk data + let mut chunk_crc = [0u8; 4]; + reader.read_exact(&mut chunk_crc)?; + chunk_full.extend_from_slice(&chunk_crc); + + let raw_chunk = chunk::RawGenericChunk::load(&mut &*chunk_full)?; + + chunk_ztxt = Some(ztxt::RawZtxtChunk::try_from(raw_chunk)?); + } + + if chunk_ztxt.is_none() { + return Err(error::DmiError::Generic( + "Failed to load DMI. zTXt chunk was not found or is after the first IDAT chunk." + .to_string(), + )); + } + + Ok(RawDmi { + header: PNG_HEADER, + chunk_ihdr, + chunk_ztxt, + chunk_plte: None, + other_chunks: None, + chunks_idat: Vec::new(), + chunk_iend: iend::RawIendChunk::new(), + }) + } + pub fn save(&self, mut writter: &mut W) -> Result { let bytes_written = writter.write(&self.header)?; let mut total_bytes_written = bytes_written; diff --git a/tests/dmi_ops.rs b/tests/dmi_ops.rs index cc2e2a6..915a94f 100644 --- a/tests/dmi_ops.rs +++ b/tests/dmi_ops.rs @@ -1,4 +1,4 @@ -use dmi::icon::Icon; +use dmi::icon::{DmiVersion, Icon, IconState, Looping}; use std::fs::File; use std::path::PathBuf; @@ -16,3 +16,32 @@ fn load_and_save_dmi() { .save(&mut write_file) .expect("Failed to save lights dmi"); } + +#[test] +fn load_dmi_meta() { + let mut load_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + load_path.push("tests/resources/load_test.dmi"); + let load_file = + File::open(load_path.as_path()).unwrap_or_else(|_| panic!("No lights dmi: {load_path:?}")); + let lights_icon = Icon::load_meta(&load_file).expect("Unable to load lights dmi metadata"); + + assert_eq!(lights_icon.version, DmiVersion::default()); + assert_eq!(lights_icon.width, 160); + assert_eq!(lights_icon.height, 160); + assert_eq!(lights_icon.states.len(), 2); + + assert_default_state(&lights_icon.states[0], "0_1"); + assert_default_state(&lights_icon.states[1], "1_1"); +} + +fn assert_default_state(state: &IconState, name: &'static str) { + assert_eq!(state.name, name); + assert_eq!(state.dirs, 1); + assert_eq!(state.frames, 1); + assert_eq!(state.delay, None); + assert_eq!(state.loop_flag, Looping::Indefinitely); + assert_eq!(state.rewind, false); + assert_eq!(state.movement, false); + assert_eq!(state.hotspot, None); + assert_eq!(state.unknown_settings, None); +} From ba9b04a737024bbbb65c1807f115ae61fa8d6ba2 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Wed, 30 Jul 2025 05:54:04 -0700 Subject: [PATCH 02/25] oh clippy, spare me --- src/chunk.rs | 22 ++++------ src/icon.rs | 114 +++++++++++++++++++++------------------------------ src/iend.rs | 24 +++++------ src/lib.rs | 54 ++++++++++-------------- src/ztxt.rs | 41 +++++++----------- 5 files changed, 103 insertions(+), 152 deletions(-) diff --git a/src/chunk.rs b/src/chunk.rs index 039c82f..8efe04c 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -23,7 +23,7 @@ impl RawGenericChunk { let chunk_length = chunk_bytes.len(); if chunk_length < 12 { - return Err(error::DmiError::Generic(format!("Failed to load Chunk. Supplied reader contained size of {} bytes, lower than the required 12.", chunk_length))); + return Err(error::DmiError::Generic(format!("Failed to load Chunk. Supplied reader contained size of {chunk_length} bytes, lower than the required 12."))); }; let data_length = [ @@ -46,8 +46,7 @@ impl RawGenericChunk { .all(|c| (b'A' <= *c && *c <= b'Z') || (b'a' <= *c && *c <= b'z')) { return Err(error::DmiError::Generic(format!( - "Failed to load Chunk. Type contained unlawful characters: {:#?}", - chunk_type + "Failed to load Chunk. Type contained unlawful characters: {chunk_type:#?}", ))); }; @@ -61,9 +60,10 @@ impl RawGenericChunk { ]; let recalculated_crc = crc::calculate_chunk_data_crc(chunk_type, &data); - if u32::from_be_bytes(crc) != recalculated_crc { + let crc_le = u32::from_be_bytes(crc); + if crc_le != recalculated_crc { let chunk_name = String::from_utf8(chunk_type.to_vec())?; - return Err(error::DmiError::Generic(format!("Failed to load Chunk of type {}. Supplied CRC invalid: {:#?}. Its value ({}) does not match the recalculated one ({}).", chunk_name, crc, u32::from_be_bytes(crc), recalculated_crc))); + return Err(error::DmiError::Generic(format!("Failed to load Chunk of type {chunk_name}. Supplied CRC invalid: {crc:#?}. Its value ({crc_le}) does not match the recalculated one ({recalculated_crc})."))); } Ok(RawGenericChunk { @@ -79,8 +79,7 @@ impl RawGenericChunk { let mut total_bytes_written = bytes_written; if bytes_written < self.data_length.len() { return Err(error::DmiError::Generic(format!( - "Failed to save Chunk. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save Chunk. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; @@ -88,8 +87,7 @@ impl RawGenericChunk { total_bytes_written += bytes_written; if bytes_written < self.chunk_type.len() { return Err(error::DmiError::Generic(format!( - "Failed to save Chunk. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save Chunk. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; @@ -97,8 +95,7 @@ impl RawGenericChunk { total_bytes_written += bytes_written; if bytes_written < self.data.len() { return Err(error::DmiError::Generic(format!( - "Failed to save Chunk. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save Chunk. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; @@ -106,8 +103,7 @@ impl RawGenericChunk { total_bytes_written += bytes_written; if bytes_written < self.crc.len() { return Err(error::DmiError::Generic(format!( - "Failed to save Chunk. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save Chunk. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; diff --git a/src/icon.rs b/src/icon.rs index 56b3a9d..0a997c9 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -62,24 +62,22 @@ impl Icon { let current_line = decompressed_text.next(); if current_line != Some("# BEGIN DMI") { return Err(DmiError::Generic(format!( - "Error loading icon: no DMI header found. Beginning: {:#?}", - current_line + "Error loading icon: no DMI header found. Beginning: {current_line:#?}" ))); }; let current_line = match decompressed_text.next() { Some(thing) => thing, None => { - return Err(DmiError::Generic( - "Error loading icon: no version header found.".to_string(), - )) + return Err(DmiError::Generic(String::from( + "Error loading icon: no version header found.", + ))) } }; let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); if split_version.len() != 2 || split_version[0] != "version" { return Err(DmiError::Generic(format!( - "Error loading icon: improper version header found: {:#?}", - split_version + "Error loading icon: improper version header found: {split_version:#?}" ))); }; let version = split_version[1].to_string(); @@ -95,8 +93,7 @@ impl Icon { let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); if split_version.len() != 2 || split_version[0] != "\twidth" { return Err(DmiError::Generic(format!( - "Error loading icon: improper width found: {:#?}", - split_version + "Error loading icon: improper width found: {split_version:#?}" ))); }; let width = split_version[1].parse::()?; @@ -112,16 +109,14 @@ impl Icon { let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); if split_version.len() != 2 || split_version[0] != "\theight" { return Err(DmiError::Generic(format!( - "Error loading icon: improper height found: {:#?}", - split_version + "Error loading icon: improper height found: {split_version:#?}" ))); }; let height = split_version[1].parse::()?; if width == 0 || height == 0 { return Err(DmiError::Generic(format!( - "Error loading icon: invalid width ({}) / height ({}) values.", - width, height + "Error loading icon: invalid width ({width}) / height ({height}) values." ))); }; @@ -135,7 +130,7 @@ impl Icon { let img_height = dimensions.1; if img_width == 0 || img_height == 0 || img_width % width != 0 || img_height % height != 0 { - return Err(DmiError::Generic(format!("Error loading icon: invalid image width ({}) / height ({}) values. Missmatch with metadata width ({}) / height ({}).", img_width, img_height, width, height))); + return Err(DmiError::Generic(format!("Error loading icon: invalid image width ({img_width}) / height ({img_height}) values. Missmatch with metadata width ({width}) / height ({height})."))); }; let width_in_states = img_width / width; @@ -163,20 +158,18 @@ impl Icon { let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); if split_version.len() != 2 || split_version[0] != "state" { return Err(DmiError::Generic(format!( - "Error loading icon: improper state found: {:#?}", - split_version + "Error loading icon: improper state found: {split_version:#?}" ))); }; let name = split_version[1].as_bytes(); if !name.starts_with(b"\"") || !name.ends_with(b"\"") { - return Err(DmiError::Generic(format!("Error loading icon: invalid name icon_state found in metadata, should be preceded and succeeded by double-quotes (\"): {:#?}", name))); + return Err(DmiError::Generic(format!("Error loading icon: invalid name icon_state found in metadata, should be preceded and succeeded by double-quotes (\"): {name:#?}"))); }; let name = match name.len() { 0 | 1 => { return Err(DmiError::Generic(format!( - "Error loading icon: invalid name icon_state found in metadata, improper size: {:#?}", - name + "Error loading icon: invalid name icon_state found in metadata, improper size: {name:#?}" ))) } 2 => String::new(), //Only the quotes, empty name otherwise. @@ -208,8 +201,7 @@ impl Icon { let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); if split_version.len() != 2 { return Err(DmiError::Generic(format!( - "Error loading icon: improper state found: {:#?}", - split_version + "Error loading icon: improper state found: {split_version:#?}" ))); }; @@ -232,8 +224,7 @@ impl Icon { // Hotspot includes a mysterious 3rd parameter that always seems to be 1. if text_coordinates.len() != 3 { return Err(DmiError::Generic(format!( - "Error loading icon: improper hotspot found: {:#?}", - split_version + "Error loading icon: improper hotspot found: {split_version:#?}" ))); }; hotspot = Some(Hotspot { @@ -259,15 +250,14 @@ impl Icon { if dirs.is_none() || frames.is_none() { return Err(DmiError::Generic(format!( - "Error loading icon: state lacks essential settings. dirs: {:#?}. frames: {:#?}.", - dirs, frames + "Error loading icon: state lacks essential settings. dirs: {dirs:#?}. frames: {frames:#?}." ))); }; let dirs = dirs.unwrap(); let frames = frames.unwrap(); if index + (dirs as u32 * frames) > max_possible_states { - return Err(DmiError::Generic(format!("Error loading icon: metadata settings exceeded the maximum number of states possible ({}).", max_possible_states))); + return Err(DmiError::Generic(format!("Error loading icon: metadata settings exceeded the maximum number of states possible ({max_possible_states})."))); }; let mut images = vec![]; @@ -322,24 +312,22 @@ impl Icon { let current_line = decompressed_text.next(); if current_line != Some("# BEGIN DMI") { return Err(DmiError::Generic(format!( - "Error loading icon: no DMI header found. Beginning: {:#?}", - current_line + "Error loading icon: no DMI header found. Beginning: {current_line:#?}" ))); }; let current_line = match decompressed_text.next() { Some(thing) => thing, None => { - return Err(DmiError::Generic( - "Error loading icon: no version header found.".to_string(), - )) + return Err(DmiError::Generic(String::from( + "Error loading icon: no version header found.", + ))) } }; let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); if split_version.len() != 2 || split_version[0] != "version" { return Err(DmiError::Generic(format!( - "Error loading icon: improper version header found: {:#?}", - split_version + "Error loading icon: improper version header found: {split_version:#?}" ))); }; let version = split_version[1].to_string(); @@ -347,16 +335,15 @@ impl Icon { let current_line = match decompressed_text.next() { Some(thing) => thing, None => { - return Err(DmiError::Generic( - "Error loading icon: no width found.".to_string(), - )) + return Err(DmiError::Generic(String::from( + "Error loading icon: no width found.", + ))) } }; let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); if split_version.len() != 2 || split_version[0] != "\twidth" { return Err(DmiError::Generic(format!( - "Error loading icon: improper width found: {:#?}", - split_version + "Error loading icon: improper width found: {split_version:#?}" ))); }; let width = split_version[1].parse::()?; @@ -364,24 +351,22 @@ impl Icon { let current_line = match decompressed_text.next() { Some(thing) => thing, None => { - return Err(DmiError::Generic( - "Error loading icon: no height found.".to_string(), - )) + return Err(DmiError::Generic(String::from( + "Error loading icon: no height found.", + ))) } }; let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); if split_version.len() != 2 || split_version[0] != "\theight" { return Err(DmiError::Generic(format!( - "Error loading icon: improper height found: {:#?}", - split_version + "Error loading icon: improper height found: {split_version:#?}" ))); }; let height = split_version[1].parse::()?; if width == 0 || height == 0 { return Err(DmiError::Generic(format!( - "Error loading icon: invalid width ({}) / height ({}) values.", - width, height + "Error loading icon: invalid width ({width}) / height ({height}) values." ))); }; @@ -403,7 +388,7 @@ impl Icon { ]); if img_width == 0 || img_height == 0 || img_width % width != 0 || img_height % height != 0 { - return Err(DmiError::Generic(format!("Error loading icon: invalid image width ({}) / height ({}) values. Missmatch with metadata width ({}) / height ({}).", img_width, img_height, width, height))); + return Err(DmiError::Generic(format!("Error loading icon: invalid image width ({img_width}) / height ({img_height}) values. Missmatch with metadata width ({width}) / height ({height})."))); }; let width_in_states = img_width / width; @@ -415,9 +400,9 @@ impl Icon { let mut current_line = match decompressed_text.next() { Some(thing) => thing, None => { - return Err(DmiError::Generic( - "Error loading icon: no DMI trailer nor states found.".to_string(), - )) + return Err(DmiError::Generic(String::from( + "Error loading icon: no DMI trailer nor states found.", + ))) } }; @@ -431,20 +416,18 @@ impl Icon { let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); if split_version.len() != 2 || split_version[0] != "state" { return Err(DmiError::Generic(format!( - "Error loading icon: improper state found: {:#?}", - split_version + "Error loading icon: improper state found: {split_version:#?}" ))); }; let name = split_version[1].as_bytes(); if !name.starts_with(b"\"") || !name.ends_with(b"\"") { - return Err(DmiError::Generic(format!("Error loading icon: invalid name icon_state found in metadata, should be preceded and succeeded by double-quotes (\"): {:#?}", name))); + return Err(DmiError::Generic(format!("Error loading icon: invalid name icon_state found in metadata, should be preceded and succeeded by double-quotes (\"): {name:#?}"))); }; let name = match name.len() { 0 | 1 => { return Err(DmiError::Generic(format!( - "Error loading icon: invalid name icon_state found in metadata, improper size: {:#?}", - name + "Error loading icon: invalid name icon_state found in metadata, improper size: {name:#?}" ))) } 2 => String::new(), //Only the quotes, empty name otherwise. @@ -464,9 +447,9 @@ impl Icon { current_line = match decompressed_text.next() { Some(thing) => thing, None => { - return Err(DmiError::Generic( - "Error loading icon: no DMI trailer found.".to_string(), - )) + return Err(DmiError::Generic(String::from( + "Error loading icon: no DMI trailer found.", + ))) } }; @@ -476,8 +459,7 @@ impl Icon { let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); if split_version.len() != 2 { return Err(DmiError::Generic(format!( - "Error loading icon: improper state found: {:#?}", - split_version + "Error loading icon: improper state found: {split_version:#?}" ))); }; @@ -500,8 +482,7 @@ impl Icon { // Hotspot includes a mysterious 3rd parameter that always seems to be 1. if text_coordinates.len() != 3 { return Err(DmiError::Generic(format!( - "Error loading icon: improper hotspot found: {:#?}", - split_version + "Error loading icon: improper hotspot found: {split_version:#?}" ))); }; hotspot = Some(Hotspot { @@ -527,8 +508,7 @@ impl Icon { if dirs.is_none() || frames.is_none() { return Err(DmiError::Generic(format!( - "Error loading icon: state lacks essential settings. dirs: {:#?}. frames: {:#?}.", - dirs, frames + "Error loading icon: state lacks essential settings. dirs: {dirs:#?}. frames: {frames:#?}." ))); }; let dirs = dirs.unwrap(); @@ -536,7 +516,7 @@ impl Icon { let next_index = index + (dirs as u32 * frames); if next_index > max_possible_states { - return Err(DmiError::Generic(format!("Error loading icon: metadata settings exceeded the maximum number of states possible ({}).", max_possible_states))); + return Err(DmiError::Generic(format!("Error loading icon: metadata settings exceeded the maximum number of states possible ({max_possible_states})."))); }; index = next_index; @@ -584,7 +564,7 @@ impl Icon { match &icon_state.delay { Some(delay) => { if delay.len() as u32 != icon_state.frames { - return Err(DmiError::Generic(format!("Error saving Icon: number of frames ({}) differs from the delay entry ({:3?}). Name: \"{}\".", icon_state.frames, delay, icon_state.name))) + return Err(DmiError::Generic(format!("Error saving Icon: number of frames ({}) differs from the delay entry ({delay:3?}). Name: \"{}\".", icon_state.frames, icon_state.name))) }; let delay: Vec= delay.iter().map(|&c| c.to_string()).collect(); signature.push_str(&format!("\tdelay = {}\n", delay.join(","))); @@ -592,7 +572,7 @@ impl Icon { None => return Err(DmiError::Generic(format!("Error saving Icon: number of frames ({}) larger than one without a delay entry in icon state of name \"{}\".", icon_state.frames, icon_state.name))) }; if let Looping::NTimes(flag) = icon_state.loop_flag { - signature.push_str(&format!("\tloop = {}\n", flag)) + signature.push_str(&format!("\tloop = {flag}\n")) } if icon_state.rewind { signature.push_str("\trewind = 1\n"); @@ -612,7 +592,7 @@ impl Icon { if let Some(hashmap) = &icon_state.unknown_settings { for (setting, value) in hashmap.iter() { - signature.push_str(&format!("\t{} = {}\n", setting, value)); + signature.push_str(&format!("\t{setting} = {value}\n")); } }; diff --git a/src/iend.rs b/src/iend.rs index 62344c9..a245369 100644 --- a/src/iend.rs +++ b/src/iend.rs @@ -45,8 +45,8 @@ impl RawIendChunk { ]; if data_length != default_iend_chunk.data_length { return Err(error::DmiError::Generic(format!( - "Failed to load RawIendChunk from reader. Lengh field value: {:#?}. Expected: {:#?}.", - data_length, default_iend_chunk.data_length + "Failed to load RawIendChunk from reader. Lengh field value: {data_length:#?}. Expected: {:#?}.", + default_iend_chunk.data_length ))); } @@ -58,8 +58,8 @@ impl RawIendChunk { ]; if chunk_type != default_iend_chunk.chunk_type { return Err(error::DmiError::Generic(format!( - "Failed to load RawIendChunk from reader. Chunk type: {:#?}. Expected {:#?}.", - chunk_type, default_iend_chunk.chunk_type + "Failed to load RawIendChunk from reader. Chunk type: {chunk_type:#?}. Expected {:#?}.", + default_iend_chunk.chunk_type ))); } @@ -71,8 +71,8 @@ impl RawIendChunk { ]; if crc != default_iend_chunk.crc { return Err(error::DmiError::Generic(format!( - "Failed to load RawIendChunk from reader. CRC: {:#?}. Expected {:#?}.", - crc, default_iend_chunk.crc + "Failed to load RawIendChunk from reader. CRC: {crc:#?}. Expected {:#?}.", + default_iend_chunk.crc ))); } @@ -84,8 +84,7 @@ impl RawIendChunk { let mut total_bytes_written = bytes_written; if bytes_written < self.data_length.len() { return Err(error::DmiError::Generic(format!( - "Failed to save IEND chunk. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save IEND chunk. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; @@ -93,8 +92,7 @@ impl RawIendChunk { total_bytes_written += bytes_written; if bytes_written < self.chunk_type.len() { return Err(error::DmiError::Generic(format!( - "Failed to save IEND chunk. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save IEND chunk. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; @@ -102,8 +100,7 @@ impl RawIendChunk { total_bytes_written += bytes_written; if bytes_written < self.crc.len() { return Err(error::DmiError::Generic(format!( - "Failed to save IEND chunk. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save IEND chunk. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; @@ -129,8 +126,7 @@ impl TryFrom for RawIendChunk { fn try_from(raw_generic_chunk: chunk::RawGenericChunk) -> Result { if !raw_generic_chunk.data.is_empty() { return Err(error::DmiError::Generic(format!( - "Failed to convert RawGenericChunk into RawIendChunk. Non-empty data field. Chunk: {:#?}.", - raw_generic_chunk + "Failed to convert RawGenericChunk into RawIendChunk. Non-empty data field. Chunk: {raw_generic_chunk:#?}." ))); }; diff --git a/src/lib.rs b/src/lib.rs index 855046c..a63b0ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,8 +48,7 @@ impl RawDmi { let header = &dmi_bytes[0..8]; if dmi_bytes[0..8] != PNG_HEADER { return Err(error::DmiError::Generic(format!( - "PNG header mismatch (expected {:#?}, found {:#?})", - PNG_HEADER, header + "PNG header mismatch (expected {PNG_HEADER:#?}, found {header:#?})" ))); }; let header = PNG_HEADER; @@ -65,9 +64,9 @@ impl RawDmi { loop { if index + 12 > dmi_bytes.len() { - return Err(error::DmiError::Generic( - "Failed to load DMI. Buffer end reached without finding an IEND chunk.".to_string(), - )); + return Err(error::DmiError::Generic(String::from( + "Failed to load DMI. Buffer end reached without finding an IEND chunk.", + ))); } let chunk_data_length = u32::from_be_bytes([ @@ -95,14 +94,14 @@ impl RawDmi { } } if chunk_ihdr.is_none() { - return Err(error::DmiError::Generic( - "Failed to load DMI. Buffer end reached without finding an IHDR chunk.".to_string(), - )); + return Err(error::DmiError::Generic(String::from( + "Failed to load DMI. Buffer end reached without finding an IHDR chunk.", + ))); }; if chunks_idat.is_empty() { - return Err(error::DmiError::Generic( - "Failed to load DMI. Buffer end reached without finding an IDAT chunk.".to_string(), - )); + return Err(error::DmiError::Generic(String::from( + "Failed to load DMI. Buffer end reached without finding an IDAT chunk.", + ))); } let other_chunks = match other_chunks.len() { 0 => None, @@ -129,8 +128,7 @@ impl RawDmi { reader.read_exact(&mut png_header)?; if png_header != PNG_HEADER { return Err(error::DmiError::Generic(format!( - "PNG header mismatch (expected {:#?}, found {:#?})", - PNG_HEADER, png_header + "PNG header mismatch (expected {PNG_HEADER:#?}, found {png_header:#?})" ))); }; // 4 (size) + 4 (type) + 13 (data) + 4 (crc) for the IHDR chunk. @@ -138,7 +136,7 @@ impl RawDmi { reader.read_exact(&mut ihdr)?; if ihdr[0..8] != IHDR_HEADER { return Err(error::DmiError::Generic( - "Failed to load DMI. IHDR chunk is not in the correct location (1st chunk), has an invalid size, or an invalid identifier.".to_string(), + String::from("Failed to load DMI. IHDR chunk is not in the correct location (1st chunk), has an invalid size, or an invalid identifier."), )); } let chunk_ihdr = chunk::RawGenericChunk::load(&mut &ihdr[0..25])?; @@ -186,10 +184,9 @@ impl RawDmi { } if chunk_ztxt.is_none() { - return Err(error::DmiError::Generic( - "Failed to load DMI. zTXt chunk was not found or is after the first IDAT chunk." - .to_string(), - )); + return Err(error::DmiError::Generic(String::from( + "Failed to load DMI. zTXt chunk was not found or is after the first IDAT chunk.", + ))); } Ok(RawDmi { @@ -208,8 +205,7 @@ impl RawDmi { let mut total_bytes_written = bytes_written; if bytes_written < 8 { return Err(error::DmiError::Generic(format!( - "Failed to save DMI. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save DMI. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; @@ -217,8 +213,7 @@ impl RawDmi { total_bytes_written += bytes_written; if bytes_written < u32::from_be_bytes(self.chunk_ihdr.data_length) as usize + 12 { return Err(error::DmiError::Generic(format!( - "Failed to save DMI. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save DMI. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; @@ -227,8 +222,7 @@ impl RawDmi { total_bytes_written += bytes_written; if bytes_written < u32::from_be_bytes(chunk_ztxt.data_length) as usize + 12 { return Err(error::DmiError::Generic(format!( - "Failed to save DMI. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save DMI. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; }; @@ -238,8 +232,7 @@ impl RawDmi { total_bytes_written += bytes_written; if bytes_written < u32::from_be_bytes(chunk_plte.data_length) as usize + 12 { return Err(error::DmiError::Generic(format!( - "Failed to save DMI. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save DMI. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; }; @@ -250,8 +243,7 @@ impl RawDmi { total_bytes_written += bytes_written; if bytes_written < u32::from_be_bytes(chunk.data_length) as usize + 12 { return Err(error::DmiError::Generic(format!( - "Failed to save DMI. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save DMI. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; } @@ -262,8 +254,7 @@ impl RawDmi { total_bytes_written += bytes_written; if bytes_written < u32::from_be_bytes(chunk.data_length) as usize + 12 { return Err(error::DmiError::Generic(format!( - "Failed to save DMI. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save DMI. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; } @@ -272,8 +263,7 @@ impl RawDmi { total_bytes_written += bytes_written; if bytes_written < u32::from_be_bytes(self.chunk_iend.data_length) as usize + 12 { return Err(error::DmiError::Generic(format!( - "Failed to save DMI. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save DMI. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; diff --git a/src/ztxt.rs b/src/ztxt.rs index ecbd48a..9196770 100644 --- a/src/ztxt.rs +++ b/src/ztxt.rs @@ -61,8 +61,7 @@ impl RawZtxtChunk { ]; if chunk_type != ZTXT_TYPE { return Err(error::DmiError::Generic(format!( - "Failed to load RawZtxtChunk from reader. Chunk type is not zTXt: {:#?}. Should be {:#?}.", - chunk_type, ZTXT_TYPE + "Failed to load RawZtxtChunk from reader. Chunk type is not zTXt: {chunk_type:#?}. Should be {ZTXT_TYPE:#?}.", ))); } let data_bytes = &raw_chunk_bytes[8..(total_bytes_length - 4)].to_vec(); @@ -74,8 +73,9 @@ impl RawZtxtChunk { raw_chunk_bytes[total_bytes_length - 1], ]; let calculated_crc = crc::calculate_chunk_data_crc(chunk_type, data_bytes); - if u32::from_be_bytes(crc) != calculated_crc { - return Err(error::DmiError::Generic(format!("Failed to load RawZtxtChunk from reader. Given CRC ({}) does not match the calculated one ({}).", u32::from_be_bytes(crc), calculated_crc))); + let crc_le = u32::from_be_bytes(crc); + if crc_le != calculated_crc { + return Err(error::DmiError::Generic(format!("Failed to load RawZtxtChunk from reader. Given CRC ({crc_le}) does not match the calculated one ({calculated_crc})."))); } Ok(RawZtxtChunk { data_length, @@ -90,8 +90,7 @@ impl RawZtxtChunk { let mut total_bytes_written = bytes_written; if bytes_written < self.data_length.len() { return Err(error::DmiError::Generic(format!( - "Failed to save zTXt chunk. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save zTXt chunk. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; @@ -99,8 +98,7 @@ impl RawZtxtChunk { total_bytes_written += bytes_written; if bytes_written < self.chunk_type.len() { return Err(error::DmiError::Generic(format!( - "Failed to save zTXt chunk. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save zTXt chunk. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; @@ -108,8 +106,7 @@ impl RawZtxtChunk { total_bytes_written += bytes_written; if bytes_written < u32::from_be_bytes(self.data_length) as usize { return Err(error::DmiError::Generic(format!( - "Failed to save zTXt chunk. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save zTXt chunk. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; @@ -117,8 +114,7 @@ impl RawZtxtChunk { total_bytes_written += bytes_written; if bytes_written < self.crc.len() { return Err(error::DmiError::Generic(format!( - "Failed to save zTXt chunk. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save zTXt chunk. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; @@ -162,8 +158,7 @@ impl TryFrom for RawZtxtChunk { let chunk_type = raw_generic_chunk.chunk_type; if chunk_type != ZTXT_TYPE { return Err(error::DmiError::Generic(format!( - "Failed to convert RawGenericChunk into RawZtxtChunk. Wrong type: {:#?}. Expected: {:#?}.", - chunk_type, ZTXT_TYPE + "Failed to convert RawGenericChunk into RawZtxtChunk. Wrong type: {chunk_type:#?}. Expected: {ZTXT_TYPE:#?}." ))); }; let chunk_data = &raw_generic_chunk.data; @@ -228,8 +223,7 @@ impl RawZtxtData { let null_separator = 0; let compression_method = data_bytes_iter.next().ok_or_else(|| { error::DmiError::Generic(format!( - "Failed to load RawZtxtData from reader, during compression method reading.\nVector: {:#?}", - data_bytes + "Failed to load RawZtxtData from reader, during compression method reading.\nVector: {data_bytes:#?}" )) })?; //let compressed_text = RawCompressedText::try_from(back_to_vector)?; @@ -249,8 +243,7 @@ impl RawZtxtData { let mut total_bytes_written = bytes_written; if bytes_written < self.keyword.len() { return Err(error::DmiError::Generic(format!( - "Failed to save zTXt data. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save zTXt data. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; @@ -258,8 +251,7 @@ impl RawZtxtData { total_bytes_written += bytes_written; if bytes_written < 1 { return Err(error::DmiError::Generic(format!( - "Failed to save zTXt data. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save zTXt data. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; @@ -267,8 +259,7 @@ impl RawZtxtData { total_bytes_written += bytes_written; if bytes_written < 1 { return Err(error::DmiError::Generic(format!( - "Failed to save zTXt data. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save zTXt data. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; @@ -276,8 +267,7 @@ impl RawZtxtData { total_bytes_written += bytes_written; if bytes_written < self.compressed_text.len() { return Err(error::DmiError::Generic(format!( - "Failed to save zTXt data. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save zTXt data. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; @@ -288,8 +278,7 @@ impl RawZtxtData { match inflate::inflate_bytes_zlib(&self.compressed_text) { Ok(decompressed_text) => Ok(decompressed_text), Err(text) => Err(error::DmiError::Generic(format!( - "Failed to read compressed text. Error: {}", - text + "Failed to read compressed text. Error: {text}" ))), } } From 946a2eb14d4f87eae7d82c19ec40bbe0e10877c7 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Wed, 30 Jul 2025 08:10:06 -0700 Subject: [PATCH 03/25] Rewrite load to not duplicate code. Add support for width/height being excluded and looping being 0 --- src/icon.rs | 394 ++++++++++++++++------------------------------------ src/lib.rs | 60 ++++++-- 2 files changed, 169 insertions(+), 285 deletions(-) diff --git a/src/icon.rs b/src/icon.rs index 0a997c9..f58ef1c 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -1,7 +1,6 @@ use crate::dirs::{Dirs, ALL_DIRS, CARDINAL_DIRS}; use crate::{error::DmiError, ztxt, RawDmi}; use image::codecs::png; -use image::GenericImageView; use image::{imageops, DynamicImage}; use std::collections::HashMap; use std::io::prelude::*; @@ -44,21 +43,16 @@ pub fn dir_to_dmi_index(dir: &Dirs) -> Option { } } -impl Icon { - pub fn load(reader: R) -> Result { - let raw_dmi = RawDmi::load(reader)?; - let chunk_ztxt = match &raw_dmi.chunk_ztxt { - Some(chunk) => chunk.clone(), - None => { - return Err(DmiError::Generic( - "Error loading icon: no zTXt chunk found.".to_string(), - )) - } - }; - let decompressed_text = chunk_ztxt.data.decode()?; - let decompressed_text = String::from_utf8(decompressed_text)?; - let mut decompressed_text = decompressed_text.lines(); +struct DmiHeaders { + version: String, + width: Option, + height: Option, +} +impl Icon { + fn read_dmi_headers( + decompressed_text: &mut std::iter::Peekable>, + ) -> Result { let current_line = decompressed_text.next(); if current_line != Some("# BEGIN DMI") { return Err(DmiError::Generic(format!( @@ -82,221 +76,101 @@ impl Icon { }; let version = split_version[1].to_string(); - let current_line = match decompressed_text.next() { - Some(thing) => thing, + let mut width = None; + let mut height = None; + + let current_line = match decompressed_text.peek() { + Some(thing) => *thing, None => { return Err(DmiError::Generic( - "Error loading icon: no width found.".to_string(), + "Error loading icon: DMI definition abruptly ends.".to_string(), )) } }; let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); - if split_version.len() != 2 || split_version[0] != "\twidth" { + + if split_version.len() != 2 { return Err(DmiError::Generic(format!( - "Error loading icon: improper width found: {split_version:#?}" + "Error loading icon: improper line entry found: {split_version:#?}" ))); - }; - let width = split_version[1].parse::()?; + } - let current_line = match decompressed_text.next() { - Some(thing) => thing, + match split_version[0] { + "\twidth" => { + width = Some(split_version[1].parse::()?); + decompressed_text.next(); // consume the peeked value + } + "\theight" => { + height = Some(split_version[1].parse::()?); + decompressed_text.next(); // consume the peeked value + } + _ => { + return Ok(DmiHeaders { + version, + width, + height, + }) + } + } + + let current_line = match decompressed_text.peek() { + Some(thing) => *thing, None => { return Err(DmiError::Generic( - "Error loading icon: no height found.".to_string(), + "Error loading icon: DMI definition abruptly ends.".to_string(), )) } }; let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); - if split_version.len() != 2 || split_version[0] != "\theight" { - return Err(DmiError::Generic(format!( - "Error loading icon: improper height found: {split_version:#?}" - ))); - }; - let height = split_version[1].parse::()?; - if width == 0 || height == 0 { + if split_version.len() != 2 { return Err(DmiError::Generic(format!( - "Error loading icon: invalid width ({width}) / height ({height}) values." + "Error loading icon: improper line entry found: {split_version:#?}" ))); - }; - - // Image time. - let mut reader = vec![]; - raw_dmi.save(&mut reader)?; - let base_image = image::load_from_memory_with_format(&reader, image::ImageFormat::Png)?; - - let dimensions = base_image.dimensions(); - let img_width = dimensions.0; - let img_height = dimensions.1; - - if img_width == 0 || img_height == 0 || img_width % width != 0 || img_height % height != 0 { - return Err(DmiError::Generic(format!("Error loading icon: invalid image width ({img_width}) / height ({img_height}) values. Missmatch with metadata width ({width}) / height ({height})."))); - }; - - let width_in_states = img_width / width; - let height_in_states = img_height / height; - let max_possible_states = width_in_states * height_in_states; - - let mut index = 0; + } - let mut current_line = match decompressed_text.next() { - Some(thing) => thing, - None => { - return Err(DmiError::Generic( - "Error loading icon: no DMI trailer nor states found.".to_string(), - )) + match split_version[0] { + "\twidth" => { + width = Some(split_version[1].parse::()?); + decompressed_text.next(); // consume the peeked value } - }; - - let mut states = vec![]; - - loop { - if current_line.contains("# END DMI") { - break; - }; - - let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); - if split_version.len() != 2 || split_version[0] != "state" { - return Err(DmiError::Generic(format!( - "Error loading icon: improper state found: {split_version:#?}" - ))); - }; - - let name = split_version[1].as_bytes(); - if !name.starts_with(b"\"") || !name.ends_with(b"\"") { - return Err(DmiError::Generic(format!("Error loading icon: invalid name icon_state found in metadata, should be preceded and succeeded by double-quotes (\"): {name:#?}"))); - }; - let name = match name.len() { - 0 | 1 => { - return Err(DmiError::Generic(format!( - "Error loading icon: invalid name icon_state found in metadata, improper size: {name:#?}" - ))) - } - 2 => String::new(), //Only the quotes, empty name otherwise. - length => String::from_utf8(name[1..(length - 1)].to_vec())?, //Hacky way to trim. Blame the cool methods being nightly experimental. - }; - - let mut dirs = None; - let mut frames = None; - let mut delay = None; - let mut loop_flag = Looping::Indefinitely; - let mut rewind = false; - let mut movement = false; - let mut hotspot = None; - let mut unknown_settings = None; - - loop { - current_line = match decompressed_text.next() { - Some(thing) => thing, - None => { - return Err(DmiError::Generic( - "Error loading icon: no DMI trailer found.".to_string(), - )) - } - }; - - if current_line.contains("# END DMI") || current_line.contains("state = \"") { - break; - }; - let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); - if split_version.len() != 2 { - return Err(DmiError::Generic(format!( - "Error loading icon: improper state found: {split_version:#?}" - ))); - }; - - match split_version[0] { - "\tdirs" => dirs = Some(split_version[1].parse::()?), - "\tframes" => frames = Some(split_version[1].parse::()?), - "\tdelay" => { - let mut delay_vector = vec![]; - let text_delays = split_version[1].split_terminator(','); - for text_entry in text_delays { - delay_vector.push(text_entry.parse::()?); - } - delay = Some(delay_vector); - } - "\tloop" => loop_flag = Looping::new(split_version[1].parse::()?), - "\trewind" => rewind = split_version[1].parse::()? != 0, - "\tmovement" => movement = split_version[1].parse::()? != 0, - "\thotspot" => { - let text_coordinates: Vec<&str> = split_version[1].split_terminator(',').collect(); - // Hotspot includes a mysterious 3rd parameter that always seems to be 1. - if text_coordinates.len() != 3 { - return Err(DmiError::Generic(format!( - "Error loading icon: improper hotspot found: {split_version:#?}" - ))); - }; - hotspot = Some(Hotspot { - x: text_coordinates[0].parse::()?, - y: text_coordinates[1].parse::()?, - }); - } - _ => { - unknown_settings = match unknown_settings { - None => { - let mut new_map = HashMap::new(); - new_map.insert(split_version[0].to_string(), split_version[1].to_string()); - Some(new_map) - } - Some(mut thing) => { - thing.insert(split_version[0].to_string(), split_version[1].to_string()); - Some(thing) - } - }; - } - }; + "\theight" => { + height = Some(split_version[1].parse::()?); + decompressed_text.next(); // consume the peeked value } - - if dirs.is_none() || frames.is_none() { - return Err(DmiError::Generic(format!( - "Error loading icon: state lacks essential settings. dirs: {dirs:#?}. frames: {frames:#?}." - ))); - }; - let dirs = dirs.unwrap(); - let frames = frames.unwrap(); - - if index + (dirs as u32 * frames) > max_possible_states { - return Err(DmiError::Generic(format!("Error loading icon: metadata settings exceeded the maximum number of states possible ({max_possible_states})."))); - }; - - let mut images = vec![]; - - for _frame in 0..frames { - for _dir in 0..dirs { - let x = (index % width_in_states) * width; - //This operation rounds towards zero, truncating any fractional part of the exact result, essentially a floor() function. - let y = (index / width_in_states) * height; - images.push(base_image.crop_imm(x, y, width, height)); - index += 1; - } + _ => { + return Ok(DmiHeaders { + version, + width, + height, + }) } - - states.push(IconState { - name, - dirs, - frames, - images, - delay, - loop_flag, - rewind, - movement, - hotspot, - unknown_settings, - }); } - Ok(Icon { - version: DmiVersion(version), + if width == Some(0) || height == Some(0) { + return Err(DmiError::Generic(format!( + "Error loading icon: invalid width ({width:#?}) / height ({height:#?}) values." + ))); + }; + + Ok(DmiHeaders { + version, width, height, - states, }) } - /// Equivalent of load, but only parses the DMI header and leaves all image data empty. - pub fn load_meta(reader: R) -> Result { - let raw_dmi = RawDmi::load_meta(reader)?; + pub fn load(reader: R) -> Result { + Self::load_internal(reader, true) + } + + pub fn load_meta(reader: R) -> Result { + Self::load_internal(reader, false) + } + + fn load_internal(reader: R, load_images: bool) -> Result { + let raw_dmi = RawDmi::load(reader)?; + let chunk_ztxt = match &raw_dmi.chunk_ztxt { Some(chunk) => chunk.clone(), None => { @@ -307,72 +181,14 @@ impl Icon { }; let decompressed_text = chunk_ztxt.data.decode()?; let decompressed_text = String::from_utf8(decompressed_text)?; - let mut decompressed_text = decompressed_text.lines(); - - let current_line = decompressed_text.next(); - if current_line != Some("# BEGIN DMI") { - return Err(DmiError::Generic(format!( - "Error loading icon: no DMI header found. Beginning: {current_line:#?}" - ))); - }; + let mut decompressed_text = decompressed_text.lines().peekable(); - let current_line = match decompressed_text.next() { - Some(thing) => thing, - None => { - return Err(DmiError::Generic(String::from( - "Error loading icon: no version header found.", - ))) - } - }; - let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); - if split_version.len() != 2 || split_version[0] != "version" { - return Err(DmiError::Generic(format!( - "Error loading icon: improper version header found: {split_version:#?}" - ))); - }; - let version = split_version[1].to_string(); + let dmi_headers = Self::read_dmi_headers(&mut decompressed_text)?; + let version = dmi_headers.version; - let current_line = match decompressed_text.next() { - Some(thing) => thing, - None => { - return Err(DmiError::Generic(String::from( - "Error loading icon: no width found.", - ))) - } - }; - let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); - if split_version.len() != 2 || split_version[0] != "\twidth" { - return Err(DmiError::Generic(format!( - "Error loading icon: improper width found: {split_version:#?}" - ))); - }; - let width = split_version[1].parse::()?; - - let current_line = match decompressed_text.next() { - Some(thing) => thing, - None => { - return Err(DmiError::Generic(String::from( - "Error loading icon: no height found.", - ))) - } - }; - let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); - if split_version.len() != 2 || split_version[0] != "\theight" { - return Err(DmiError::Generic(format!( - "Error loading icon: improper height found: {split_version:#?}" - ))); - }; - let height = split_version[1].parse::()?; - - if width == 0 || height == 0 { - return Err(DmiError::Generic(format!( - "Error loading icon: invalid width ({width}) / height ({height}) values." - ))); - }; - - // Image time. - let mut reader = vec![]; - raw_dmi.save(&mut reader)?; + // yes you can make a DMI without a width or height. it defaults to 32x32 + let width = dmi_headers.width.unwrap_or(32); + let height = dmi_headers.height.unwrap_or(32); let img_width = u32::from_be_bytes([ raw_dmi.chunk_ihdr.data[0], @@ -387,6 +203,17 @@ impl Icon { raw_dmi.chunk_ihdr.data[7], ]); + let base_image = if load_images { + let mut reader = vec![]; + raw_dmi.save(&mut reader)?; + Some(image::load_from_memory_with_format( + &reader, + image::ImageFormat::Png, + )?) + } else { + None + }; + if img_width == 0 || img_height == 0 || img_width % width != 0 || img_height % height != 0 { return Err(DmiError::Generic(format!("Error loading icon: invalid image width ({img_width}) / height ({img_height}) values. Missmatch with metadata width ({width}) / height ({height})."))); }; @@ -400,9 +227,9 @@ impl Icon { let mut current_line = match decompressed_text.next() { Some(thing) => thing, None => { - return Err(DmiError::Generic(String::from( - "Error loading icon: no DMI trailer nor states found.", - ))) + return Err(DmiError::Generic( + "Error loading icon: no DMI trailer nor states found.".to_string(), + )) } }; @@ -447,9 +274,9 @@ impl Icon { current_line = match decompressed_text.next() { Some(thing) => thing, None => { - return Err(DmiError::Generic(String::from( - "Error loading icon: no DMI trailer found.", - ))) + return Err(DmiError::Generic( + "Error loading icon: no DMI trailer found.".to_string(), + )) } }; @@ -474,7 +301,12 @@ impl Icon { } delay = Some(delay_vector); } - "\tloop" => loop_flag = Looping::new(split_version[1].parse::()?), + "\tloop" => { + let loop_raw = split_version[1].parse::()?; + if loop_raw != 0 { + loop_flag = Looping::new(loop_raw); + } + } "\trewind" => rewind = split_version[1].parse::()? != 0, "\tmovement" => movement = split_version[1].parse::()? != 0, "\thotspot" => { @@ -519,13 +351,29 @@ impl Icon { return Err(DmiError::Generic(format!("Error loading icon: metadata settings exceeded the maximum number of states possible ({max_possible_states})."))); }; + let mut images = vec![]; + + if let Some(full_image) = base_image.as_ref() { + let mut image_offset = 0; + for _frame in 0..frames { + for _dir in 0..dirs { + let full_image_offset = index + image_offset; + let x = (full_image_offset % width_in_states) * width; + //This operation rounds towards zero, truncating any fractional part of the exact result, essentially a floor() function. + let y = (full_image_offset / width_in_states) * height; + images.push(full_image.crop_imm(x, y, width, height)); + image_offset += 1; + } + } + } + index = next_index; states.push(IconState { name, dirs, frames, - images: Vec::new(), + images, delay, loop_flag, rewind, diff --git a/src/lib.rs b/src/lib.rs index a63b0ea..d42f56d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,11 +6,12 @@ pub mod icon; pub mod iend; pub mod ztxt; -use std::io::{Read, Seek, Write}; +use std::io::{Cursor, Read, Seek, Write}; /// The PNG magic header pub const PNG_HEADER: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10]; pub const IHDR_HEADER: [u8; 8] = [0, 0, 0, 13, 73, 72, 68, 82]; +const ASSUMED_ZTXT_MAX: usize = 500; #[derive(Clone, Eq, PartialEq, Debug, Default)] pub struct RawDmi { @@ -123,9 +124,21 @@ impl RawDmi { /// Equivalent of load, but only parses IHDR and zTXt. May not catch an improperly formatted PNG file, because it only reads those headers. pub fn load_meta(mut reader: R) -> Result { + let mut dmi_bytes = vec![0u8; ASSUMED_ZTXT_MAX]; + + // Since we only want the zTXt it's unlikely to be any longer than ASSUMED_ZTXT_MAX bytes when combined with headers until we encounter it + // If the zTxt is especially long and its length exceeds our index we can read extra bytes later. + let mut dmi_bytes_read = reader.read(&mut dmi_bytes)?; + + if dmi_bytes_read < 72 { + return Err(error::DmiError::Generic(format!("Failed to load DMI. Supplied reader contained size of {} bytes, lower than the required 72.", dmi_bytes.len()))); + }; + + let mut dmi_reader = Cursor::new(dmi_bytes); + // 8 bytes for the PNG file signature. let mut png_header = [0u8; 8]; - reader.read_exact(&mut png_header)?; + dmi_reader.read_exact(&mut png_header)?; if png_header != PNG_HEADER { return Err(error::DmiError::Generic(format!( "PNG header mismatch (expected {PNG_HEADER:#?}, found {png_header:#?})" @@ -133,7 +146,7 @@ impl RawDmi { }; // 4 (size) + 4 (type) + 13 (data) + 4 (crc) for the IHDR chunk. let mut ihdr = [0u8; 25]; - reader.read_exact(&mut ihdr)?; + dmi_reader.read_exact(&mut ihdr)?; if ihdr[0..8] != IHDR_HEADER { return Err(error::DmiError::Generic( String::from("Failed to load DMI. IHDR chunk is not in the correct location (1st chunk), has an invalid size, or an invalid identifier."), @@ -146,7 +159,7 @@ impl RawDmi { loop { // Read len let mut chunk_len_be: [u8; 4] = [0u8; 4]; - reader.read_exact(&mut chunk_len_be)?; + dmi_reader.read_exact(&mut chunk_len_be)?; let chunk_len = u32::from_be_bytes(chunk_len_be) as usize; // Create vec for full chunk data @@ -155,27 +168,50 @@ impl RawDmi { // Read header into full chunk data let mut chunk_header = [0u8; 4]; - reader.read_exact(&mut chunk_header)?; + dmi_reader.read_exact(&mut chunk_header)?; chunk_full.extend_from_slice(&chunk_header); + // If we encounter IDAT or IEND we can just break because the zTXt header aint happening + if &chunk_header == b"IDAT" || &chunk_header == b"IEND" { + break; + } + + // We will overread the file's buffer. + let original_position = dmi_reader.position(); + if original_position + chunk_len as u64 > dmi_bytes_read as u64 { + // Read the remainder of the chunk + 4 bytes for CRC + 8 bytes for the next header. + // There will always be a next header because IEND headers break before this check. + let mut new_dmi_bytes = vec![0u8; chunk_len + 12]; + println!( + "reader pos {} bytes {dmi_bytes_read}", + dmi_reader.position() + ); + reader.read_exact(&mut new_dmi_bytes)?; + println!("adding {}", new_dmi_bytes.len()); + // Append all the new bytes to our cursor and go back to our old spot + dmi_reader.seek_relative(dmi_bytes_read as i64 - original_position as i64)?; + println!("write reader pos {}", dmi_reader.position()); + dmi_reader.write_all(&new_dmi_bytes)?; + dmi_bytes_read += new_dmi_bytes.len(); + dmi_reader.seek_relative(original_position as i64 - dmi_bytes_read as i64)?; + println!("new reader pos {}", dmi_reader.position()); + println!("num {}", dmi_reader.clone().read_to_end(&mut Vec::new())?); + } + // Skip non-zTXt chunks if &chunk_header != b"zTXt" { - // If we encounter IDAT or IEND we can just break because the zTXt header aint happening - if &chunk_header == b"IDAT" || &chunk_header == b"IEND" { - break; - } - reader.seek_relative((chunk_len + 4) as i64)?; + dmi_reader.seek_relative((chunk_len + 4) as i64)?; continue; } // Read actual chunk data and append let mut chunk_data = vec![0; chunk_len]; - reader.read_exact(&mut chunk_data)?; + dmi_reader.read_exact(&mut chunk_data)?; chunk_full.extend_from_slice(&chunk_data); // Read CRC into full chunk data let mut chunk_crc = [0u8; 4]; - reader.read_exact(&mut chunk_crc)?; + dmi_reader.read_exact(&mut chunk_crc)?; chunk_full.extend_from_slice(&chunk_crc); let raw_chunk = chunk::RawGenericChunk::load(&mut &*chunk_full)?; From f4dc9a484805d8b210aeebabfd009ddbf1c50518 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Wed, 30 Jul 2025 08:17:00 -0700 Subject: [PATCH 04/25] Remove debug statements --- src/lib.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d42f56d..6cae4db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -182,20 +182,12 @@ impl RawDmi { // Read the remainder of the chunk + 4 bytes for CRC + 8 bytes for the next header. // There will always be a next header because IEND headers break before this check. let mut new_dmi_bytes = vec![0u8; chunk_len + 12]; - println!( - "reader pos {} bytes {dmi_bytes_read}", - dmi_reader.position() - ); reader.read_exact(&mut new_dmi_bytes)?; - println!("adding {}", new_dmi_bytes.len()); // Append all the new bytes to our cursor and go back to our old spot dmi_reader.seek_relative(dmi_bytes_read as i64 - original_position as i64)?; - println!("write reader pos {}", dmi_reader.position()); dmi_reader.write_all(&new_dmi_bytes)?; dmi_bytes_read += new_dmi_bytes.len(); dmi_reader.seek_relative(original_position as i64 - dmi_bytes_read as i64)?; - println!("new reader pos {}", dmi_reader.position()); - println!("num {}", dmi_reader.clone().read_to_end(&mut Vec::new())?); } // Skip non-zTXt chunks From c1827aa57914890d1a05fbffcc38123b2f6686c1 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Wed, 30 Jul 2025 08:21:27 -0700 Subject: [PATCH 05/25] Woops I was not using load_meta --- src/icon.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/icon.rs b/src/icon.rs index f58ef1c..0832e62 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -160,16 +160,16 @@ impl Icon { }) } - pub fn load(reader: R) -> Result { + pub fn load(reader: R) -> Result { Self::load_internal(reader, true) } - pub fn load_meta(reader: R) -> Result { + pub fn load_meta(reader: R) -> Result { Self::load_internal(reader, false) } - fn load_internal(reader: R, load_images: bool) -> Result { - let raw_dmi = RawDmi::load(reader)?; + fn load_internal(reader: R, load_images: bool) -> Result { + let raw_dmi = if load_images { RawDmi::load(reader)? } else { RawDmi::load_meta(reader)? }; let chunk_ztxt = match &raw_dmi.chunk_ztxt { Some(chunk) => chunk.clone(), From 50cfe937cee26e1a8df60f4433b57297830c4c0d Mon Sep 17 00:00:00 2001 From: itsmeow Date: Wed, 30 Jul 2025 08:34:59 -0700 Subject: [PATCH 06/25] Fix logic error --- src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 6cae4db..b34e745 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -157,6 +157,7 @@ impl RawDmi { let mut chunk_ztxt = None; loop { + // Read len let mut chunk_len_be: [u8; 4] = [0u8; 4]; dmi_reader.read_exact(&mut chunk_len_be)?; @@ -178,7 +179,7 @@ impl RawDmi { // We will overread the file's buffer. let original_position = dmi_reader.position(); - if original_position + chunk_len as u64 > dmi_bytes_read as u64 { + if original_position + chunk_len as u64 + 12 > dmi_bytes_read as u64 { // Read the remainder of the chunk + 4 bytes for CRC + 8 bytes for the next header. // There will always be a next header because IEND headers break before this check. let mut new_dmi_bytes = vec![0u8; chunk_len + 12]; From b9fa6811e7e9e7c84872d16db934564516bf0126 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Wed, 30 Jul 2025 08:37:29 -0700 Subject: [PATCH 07/25] Format --- src/icon.rs | 6 +++++- src/lib.rs | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/icon.rs b/src/icon.rs index 0832e62..aacb8c1 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -169,7 +169,11 @@ impl Icon { } fn load_internal(reader: R, load_images: bool) -> Result { - let raw_dmi = if load_images { RawDmi::load(reader)? } else { RawDmi::load_meta(reader)? }; + let raw_dmi = if load_images { + RawDmi::load(reader)? + } else { + RawDmi::load_meta(reader)? + }; let chunk_ztxt = match &raw_dmi.chunk_ztxt { Some(chunk) => chunk.clone(), diff --git a/src/lib.rs b/src/lib.rs index b34e745..4e86279 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -157,7 +157,6 @@ impl RawDmi { let mut chunk_ztxt = None; loop { - // Read len let mut chunk_len_be: [u8; 4] = [0u8; 4]; dmi_reader.read_exact(&mut chunk_len_be)?; From e2996791678565ff055efb14343601791404c4a9 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Fri, 1 Aug 2025 01:22:30 -0700 Subject: [PATCH 08/25] Minor code improvements --- src/icon.rs | 123 ++++++++++++++++++---------------------------------- 1 file changed, 41 insertions(+), 82 deletions(-) diff --git a/src/icon.rs b/src/icon.rs index aacb8c1..ee958f2 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -78,72 +78,35 @@ impl Icon { let mut width = None; let mut height = None; + for _ in 0..2 { + let current_line = match decompressed_text.peek() { + Some(thing) => *thing, + None => { + return Err(DmiError::Generic( + "Error loading icon: DMI definition abruptly ends.".to_string(), + )) + } + }; + let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); - let current_line = match decompressed_text.peek() { - Some(thing) => *thing, - None => { - return Err(DmiError::Generic( - "Error loading icon: DMI definition abruptly ends.".to_string(), - )) - } - }; - let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); - - if split_version.len() != 2 { - return Err(DmiError::Generic(format!( - "Error loading icon: improper line entry found: {split_version:#?}" - ))); - } - - match split_version[0] { - "\twidth" => { - width = Some(split_version[1].parse::()?); - decompressed_text.next(); // consume the peeked value - } - "\theight" => { - height = Some(split_version[1].parse::()?); - decompressed_text.next(); // consume the peeked value - } - _ => { - return Ok(DmiHeaders { - version, - width, - height, - }) - } - } - - let current_line = match decompressed_text.peek() { - Some(thing) => *thing, - None => { - return Err(DmiError::Generic( - "Error loading icon: DMI definition abruptly ends.".to_string(), - )) + if split_version.len() != 2 { + return Err(DmiError::Generic(format!( + "Error loading icon: improper line entry found: {split_version:#?}" + ))); } - }; - let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); - - if split_version.len() != 2 { - return Err(DmiError::Generic(format!( - "Error loading icon: improper line entry found: {split_version:#?}" - ))); - } - match split_version[0] { - "\twidth" => { - width = Some(split_version[1].parse::()?); - decompressed_text.next(); // consume the peeked value - } - "\theight" => { - height = Some(split_version[1].parse::()?); - decompressed_text.next(); // consume the peeked value - } - _ => { - return Ok(DmiHeaders { - version, - width, - height, - }) + match split_version[0] { + "\twidth" => { + width = Some(split_version[1].parse::()?); + decompressed_text.next(); // consume the peeked value + } + "\theight" => { + height = Some(split_version[1].parse::()?); + decompressed_text.next(); // consume the peeked value + } + _ => { + break; + } } } @@ -164,6 +127,9 @@ impl Icon { Self::load_internal(reader, true) } + /// Returns an Icon {} without any images inside of the IconStates and with less error validation. + /// This is suitable for reading DMI metadata without caring about the actual images within. + /// Can load a full DMI about 10x faster than Icon::load. pub fn load_meta(reader: R) -> Result { Self::load_internal(reader, false) } @@ -176,7 +142,7 @@ impl Icon { }; let chunk_ztxt = match &raw_dmi.chunk_ztxt { - Some(chunk) => chunk.clone(), + Some(chunk) => chunk, None => { return Err(DmiError::Generic( "Error loading icon: no zTXt chunk found.".to_string(), @@ -194,7 +160,7 @@ impl Icon { let width = dmi_headers.width.unwrap_or(32); let height = dmi_headers.height.unwrap_or(32); - let img_width = u32::from_be_bytes([ + let img_width: u32 = u32::from_be_bytes([ raw_dmi.chunk_ihdr.data[0], raw_dmi.chunk_ihdr.data[1], raw_dmi.chunk_ihdr.data[2], @@ -305,12 +271,7 @@ impl Icon { } delay = Some(delay_vector); } - "\tloop" => { - let loop_raw = split_version[1].parse::()?; - if loop_raw != 0 { - loop_flag = Looping::new(loop_raw); - } - } + "\tloop" => loop_flag = Looping::new(split_version[1].parse::()?), "\trewind" => rewind = split_version[1].parse::()? != 0, "\tmovement" => movement = split_version[1].parse::()? != 0, "\thotspot" => { @@ -358,16 +319,11 @@ impl Icon { let mut images = vec![]; if let Some(full_image) = base_image.as_ref() { - let mut image_offset = 0; - for _frame in 0..frames { - for _dir in 0..dirs { - let full_image_offset = index + image_offset; - let x = (full_image_offset % width_in_states) * width; - //This operation rounds towards zero, truncating any fractional part of the exact result, essentially a floor() function. - let y = (full_image_offset / width_in_states) * height; - images.push(full_image.crop_imm(x, y, width, height)); - image_offset += 1; - } + for image_idx in index..(index + (frames*dirs as u32)) { + let x = (image_idx % width_in_states) * width; + //This operation rounds towards zero, truncating any fractional part of the exact result, essentially a floor() function. + let y = (image_idx / width_in_states) * height; + images.push(full_image.crop_imm(x, y, width, height)); } } @@ -512,7 +468,10 @@ pub enum Looping { impl Looping { /// Creates a new `NTimes` variant with `x` number of times to loop pub fn new(x: u32) -> Self { - Self::NTimes(NonZeroU32::new(x).unwrap()) + match x { + 0 => Self::default(), + _ => Self::NTimes(NonZeroU32::new(x).unwrap()) + } } /// Unwraps the Looping yielding the `u32` if the `Looping` is a `Looping::NTimes` From 208cf78f0e107d74ef19004f646059ef5d4b2305 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Fri, 1 Aug 2025 02:11:51 -0700 Subject: [PATCH 09/25] Reduce allocations for load_meta --- src/icon.rs | 69 +++++++++++++++++++++++++---------------------------- src/lib.rs | 40 ++++++++++++++++--------------- 2 files changed, 53 insertions(+), 56 deletions(-) diff --git a/src/icon.rs b/src/icon.rs index ee958f2..c928c8a 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -1,5 +1,5 @@ use crate::dirs::{Dirs, ALL_DIRS, CARDINAL_DIRS}; -use crate::{error::DmiError, ztxt, RawDmi}; +use crate::{error::DmiError, ztxt, RawDmi, RawDmiMetadata}; use image::codecs::png; use image::{imageops, DynamicImage}; use std::collections::HashMap; @@ -135,20 +135,33 @@ impl Icon { } fn load_internal(reader: R, load_images: bool) -> Result { - let raw_dmi = if load_images { - RawDmi::load(reader)? + let (base_image, dmi_meta) = if load_images { + let raw_dmi = RawDmi::load(reader)?; + let mut rawdmi_temp = vec![]; + raw_dmi.save(&mut rawdmi_temp)?; + let chunk_ztxt = match raw_dmi.chunk_ztxt { + Some(chunk) => chunk, + None => { + return Err(DmiError::Generic(String::from( + "Error loading icon: no zTXt chunk found.", + ))) + } + }; + ( + Some(image::load_from_memory_with_format( + &rawdmi_temp, + image::ImageFormat::Png, + )?), + RawDmiMetadata { + chunk_ihdr: raw_dmi.chunk_ihdr, + chunk_ztxt: chunk_ztxt, + }, + ) } else { - RawDmi::load_meta(reader)? + (None, RawDmi::load_meta(reader)?) }; - let chunk_ztxt = match &raw_dmi.chunk_ztxt { - Some(chunk) => chunk, - None => { - return Err(DmiError::Generic( - "Error loading icon: no zTXt chunk found.".to_string(), - )) - } - }; + let chunk_ztxt = &dmi_meta.chunk_ztxt; let decompressed_text = chunk_ztxt.data.decode()?; let decompressed_text = String::from_utf8(decompressed_text)?; let mut decompressed_text = decompressed_text.lines().peekable(); @@ -160,29 +173,11 @@ impl Icon { let width = dmi_headers.width.unwrap_or(32); let height = dmi_headers.height.unwrap_or(32); - let img_width: u32 = u32::from_be_bytes([ - raw_dmi.chunk_ihdr.data[0], - raw_dmi.chunk_ihdr.data[1], - raw_dmi.chunk_ihdr.data[2], - raw_dmi.chunk_ihdr.data[3], - ]); - let img_height = u32::from_be_bytes([ - raw_dmi.chunk_ihdr.data[4], - raw_dmi.chunk_ihdr.data[5], - raw_dmi.chunk_ihdr.data[6], - raw_dmi.chunk_ihdr.data[7], - ]); - - let base_image = if load_images { - let mut reader = vec![]; - raw_dmi.save(&mut reader)?; - Some(image::load_from_memory_with_format( - &reader, - image::ImageFormat::Png, - )?) - } else { - None - }; + let ihdr_data = dmi_meta.chunk_ihdr.data; + + let img_width: u32 = + u32::from_be_bytes([ihdr_data[0], ihdr_data[1], ihdr_data[2], ihdr_data[3]]); + let img_height = u32::from_be_bytes([ihdr_data[4], ihdr_data[5], ihdr_data[6], ihdr_data[7]]); if img_width == 0 || img_height == 0 || img_width % width != 0 || img_height % height != 0 { return Err(DmiError::Generic(format!("Error loading icon: invalid image width ({img_width}) / height ({img_height}) values. Missmatch with metadata width ({width}) / height ({height})."))); @@ -319,7 +314,7 @@ impl Icon { let mut images = vec![]; if let Some(full_image) = base_image.as_ref() { - for image_idx in index..(index + (frames*dirs as u32)) { + for image_idx in index..(index + (frames * dirs as u32)) { let x = (image_idx % width_in_states) * width; //This operation rounds towards zero, truncating any fractional part of the exact result, essentially a floor() function. let y = (image_idx / width_in_states) * height; @@ -470,7 +465,7 @@ impl Looping { pub fn new(x: u32) -> Self { match x { 0 => Self::default(), - _ => Self::NTimes(NonZeroU32::new(x).unwrap()) + _ => Self::NTimes(NonZeroU32::new(x).unwrap()), } } diff --git a/src/lib.rs b/src/lib.rs index 4e86279..c10f8ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,12 @@ pub struct RawDmi { pub chunk_iend: iend::RawIendChunk, } +#[derive(Clone, Eq, PartialEq, Debug, Default)] +pub struct RawDmiMetadata { + pub chunk_ihdr: chunk::RawGenericChunk, + pub chunk_ztxt: ztxt::RawZtxtChunk, +} + impl RawDmi { pub fn new() -> RawDmi { RawDmi { @@ -123,7 +129,7 @@ impl RawDmi { } /// Equivalent of load, but only parses IHDR and zTXt. May not catch an improperly formatted PNG file, because it only reads those headers. - pub fn load_meta(mut reader: R) -> Result { + pub fn load_meta(mut reader: R) -> Result { let mut dmi_bytes = vec![0u8; ASSUMED_ZTXT_MAX]; // Since we only want the zTXt it's unlikely to be any longer than ASSUMED_ZTXT_MAX bytes when combined with headers until we encounter it @@ -134,11 +140,11 @@ impl RawDmi { return Err(error::DmiError::Generic(format!("Failed to load DMI. Supplied reader contained size of {} bytes, lower than the required 72.", dmi_bytes.len()))); }; - let mut dmi_reader = Cursor::new(dmi_bytes); + let mut buffered_dmi_bytes = Cursor::new(dmi_bytes); // 8 bytes for the PNG file signature. let mut png_header = [0u8; 8]; - dmi_reader.read_exact(&mut png_header)?; + buffered_dmi_bytes.read_exact(&mut png_header)?; if png_header != PNG_HEADER { return Err(error::DmiError::Generic(format!( "PNG header mismatch (expected {PNG_HEADER:#?}, found {png_header:#?})" @@ -146,7 +152,7 @@ impl RawDmi { }; // 4 (size) + 4 (type) + 13 (data) + 4 (crc) for the IHDR chunk. let mut ihdr = [0u8; 25]; - dmi_reader.read_exact(&mut ihdr)?; + buffered_dmi_bytes.read_exact(&mut ihdr)?; if ihdr[0..8] != IHDR_HEADER { return Err(error::DmiError::Generic( String::from("Failed to load DMI. IHDR chunk is not in the correct location (1st chunk), has an invalid size, or an invalid identifier."), @@ -159,7 +165,7 @@ impl RawDmi { loop { // Read len let mut chunk_len_be: [u8; 4] = [0u8; 4]; - dmi_reader.read_exact(&mut chunk_len_be)?; + buffered_dmi_bytes.read_exact(&mut chunk_len_be)?; let chunk_len = u32::from_be_bytes(chunk_len_be) as usize; // Create vec for full chunk data @@ -168,7 +174,7 @@ impl RawDmi { // Read header into full chunk data let mut chunk_header = [0u8; 4]; - dmi_reader.read_exact(&mut chunk_header)?; + buffered_dmi_bytes.read_exact(&mut chunk_header)?; chunk_full.extend_from_slice(&chunk_header); // If we encounter IDAT or IEND we can just break because the zTXt header aint happening @@ -177,33 +183,33 @@ impl RawDmi { } // We will overread the file's buffer. - let original_position = dmi_reader.position(); + let original_position = buffered_dmi_bytes.position(); if original_position + chunk_len as u64 + 12 > dmi_bytes_read as u64 { // Read the remainder of the chunk + 4 bytes for CRC + 8 bytes for the next header. // There will always be a next header because IEND headers break before this check. let mut new_dmi_bytes = vec![0u8; chunk_len + 12]; reader.read_exact(&mut new_dmi_bytes)?; // Append all the new bytes to our cursor and go back to our old spot - dmi_reader.seek_relative(dmi_bytes_read as i64 - original_position as i64)?; - dmi_reader.write_all(&new_dmi_bytes)?; + buffered_dmi_bytes.seek_relative(dmi_bytes_read as i64 - original_position as i64)?; + buffered_dmi_bytes.write_all(&new_dmi_bytes)?; dmi_bytes_read += new_dmi_bytes.len(); - dmi_reader.seek_relative(original_position as i64 - dmi_bytes_read as i64)?; + buffered_dmi_bytes.seek_relative(original_position as i64 - dmi_bytes_read as i64)?; } // Skip non-zTXt chunks if &chunk_header != b"zTXt" { - dmi_reader.seek_relative((chunk_len + 4) as i64)?; + buffered_dmi_bytes.seek_relative((chunk_len + 4) as i64)?; continue; } // Read actual chunk data and append let mut chunk_data = vec![0; chunk_len]; - dmi_reader.read_exact(&mut chunk_data)?; + buffered_dmi_bytes.read_exact(&mut chunk_data)?; chunk_full.extend_from_slice(&chunk_data); // Read CRC into full chunk data let mut chunk_crc = [0u8; 4]; - dmi_reader.read_exact(&mut chunk_crc)?; + buffered_dmi_bytes.read_exact(&mut chunk_crc)?; chunk_full.extend_from_slice(&chunk_crc); let raw_chunk = chunk::RawGenericChunk::load(&mut &*chunk_full)?; @@ -216,15 +222,11 @@ impl RawDmi { "Failed to load DMI. zTXt chunk was not found or is after the first IDAT chunk.", ))); } + let chunk_ztxt = chunk_ztxt.unwrap(); - Ok(RawDmi { - header: PNG_HEADER, + Ok(RawDmiMetadata { chunk_ihdr, chunk_ztxt, - chunk_plte: None, - other_chunks: None, - chunks_idat: Vec::new(), - chunk_iend: iend::RawIendChunk::new(), }) } From 56d71df0898321d3d234aff93eb2b4816e860ab0 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Fri, 1 Aug 2025 02:16:30 -0700 Subject: [PATCH 10/25] Use == instead of contains --- src/icon.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/icon.rs b/src/icon.rs index c928c8a..8045a36 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -201,7 +201,7 @@ impl Icon { let mut states = vec![]; loop { - if current_line.contains("# END DMI") { + if current_line == "# END DMI" { break; }; @@ -245,7 +245,7 @@ impl Icon { } }; - if current_line.contains("# END DMI") || current_line.contains("state = \"") { + if current_line == "# END DMI" || current_line.starts_with("state = \"") { break; }; let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); From 8f76ab3cdc60bbcaaf41417c59ccce83ea0106e9 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Fri, 1 Aug 2025 02:42:18 -0700 Subject: [PATCH 11/25] clippy --- src/icon.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/icon.rs b/src/icon.rs index 8045a36..9af02a3 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -154,7 +154,7 @@ impl Icon { )?), RawDmiMetadata { chunk_ihdr: raw_dmi.chunk_ihdr, - chunk_ztxt: chunk_ztxt, + chunk_ztxt, }, ) } else { From f3e8f0f647db5cbae44993d3a7911ac2321659ff Mon Sep 17 00:00:00 2001 From: itsmeow Date: Fri, 1 Aug 2025 05:03:46 -0700 Subject: [PATCH 12/25] Fix ability to read quotes and backslashes in icon state names, include in tests --- .gitignore | 1 + src/error.rs | 2 + src/icon.rs | 175 ++++++++++++++++++++++++---------- tests/bench_dmi_load.rs | 57 +++++++++++ tests/dmi_ops.rs | 37 ++++--- tests/resources/load_test.dmi | Bin 4650 -> 5353 bytes tests/resources/save_test.dmi | Bin 4530 -> 0 bytes 7 files changed, 209 insertions(+), 63 deletions(-) create mode 100644 tests/bench_dmi_load.rs delete mode 100644 tests/resources/save_test.dmi diff --git a/.gitignore b/.gitignore index 96ef6c0..29e9259 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target Cargo.lock +tests/resources/save_test.dmi diff --git a/src/error.rs b/src/error.rs index a45e3d0..16e164d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -19,6 +19,8 @@ pub enum DmiError { CrcMismatch { stated: u32, calculated: u32 }, #[error("Dmi error: {0}")] Generic(String), + #[error("Dmi block entry error: {0}")] + BlockEntry(String), #[error("Dmi IconState error: {0}")] IconState(String), #[error("Encoding error: {0}")] diff --git a/src/icon.rs b/src/icon.rs index 9af02a3..d27f734 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -49,7 +49,107 @@ struct DmiHeaders { height: Option, } +/// Splits the line of a DMI entry into a key/value pair on the equals sign. +/// Removes spaces or equals signs that are not inside quotes. +/// Tabs are left intact only prior to the first equals sign, and only as the first character parsed. +/// Removes quotes around values and escape characters for quotes inside the quotes. +/// The second string cannot be empty (a value must exist), or a DmiError is returned. +/// Only one set of quotes is allowed if allow_quotes is true, and it must wrap the entire value. +/// If require_quotes is set, will error if there are not quotes around the value. +fn parse_dmi_line(line: &str, allow_quotes: bool, require_quotes: bool) -> Result<(String, String), DmiError> { + let mut prior_equals = String::with_capacity(9); // 'movement' is the longest DMI key + let mut post_equals = String::with_capacity(line.len() - 3); + let mut equals_encountered = false; + let mut quoted_post_equals = false; + let mut escape_quotes = false; + let mut quotes_ended = false; + let num_chars = line.len(); + let line_bytes = line.as_bytes(); + for char_idx in 0..num_chars { + let char = line_bytes[char_idx] as char; + if equals_encountered { + let escape_this_quote = escape_quotes; + escape_quotes = false; + match char { + '\\' => { + if !quoted_post_equals { + return Err(DmiError::Generic(format!("Backslash found in line with value '{line}' after first equals without quotes."))); + } + if !escape_this_quote { + escape_quotes = true; + continue; + } + } + '"' => { + if !allow_quotes { + return Err(DmiError::Generic(format!("Quote found in line with value '{line}' after first equals where they are not allowed."))); + } + if !escape_this_quote { + if quoted_post_equals && char_idx + 1 != num_chars { + return Err(DmiError::BlockEntry(format!("Line with value '{line}' ends quotes prior to the last character on the line. This is not allowed."))); + } else if !quoted_post_equals && !post_equals.is_empty() { + return Err(DmiError::BlockEntry(format!("Line with value '{line}' starts quotes after the first character in its value. This is not allowed."))); + } + quoted_post_equals = !quoted_post_equals; + if !quoted_post_equals { + quotes_ended = true; + } + continue; + } + } + '\t' | '=' => { + if !quoted_post_equals { + return Err(DmiError::BlockEntry(format!("Invalid character {char} found in line with value '{line}' after first equals without quotes."))); + } + } + ' ' => { + if !quoted_post_equals { + if post_equals.is_empty() { + continue; + } else { + return Err(DmiError::BlockEntry(format!("Space found in line with value '{line}' after first equals without quotes. Only one space is allowed directly after the equals sign."))); + } + } + } + _ => {} + } + if allow_quotes && require_quotes && !quoted_post_equals { + return Err(DmiError::Generic(format!("Line with value '{line}' is required to have quotes after the equals sign, but does not quote all its contents!"))); + } + post_equals.push(char); + } else { + // Keys (prior to equals) are almost always checked against a value, so there's no point in doing extensive checks ourselves. + match char { + '=' => { + equals_encountered = true; + continue; + } + ' ' => { + if char_idx + 1 == num_chars { + return Err(DmiError::BlockEntry(format!("Line with value '{line}' abruptly ends on a space with no equals after it."))); + } + let next_char = line_bytes[char_idx + 1] as char; + if next_char != '=' { + return Err(DmiError::BlockEntry(format!("Line with value '{line}' contains a space not directly prior to an equals sign before the first equals sign was encountered."))); + } else { + continue; + } + } + _ => {} + } + prior_equals.push(char); + } + } + if post_equals.is_empty() && !quotes_ended { + return Err(DmiError::BlockEntry(format!( + "No value was found for line: '{line}'!" + ))); + }; + return Ok((prior_equals, post_equals)); +} + impl Icon { + fn read_dmi_headers( decompressed_text: &mut std::iter::Peekable>, ) -> Result { @@ -68,13 +168,13 @@ impl Icon { ))) } }; - let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); - if split_version.len() != 2 || split_version[0] != "version" { + let (key, value) = parse_dmi_line(current_line, false, false)?; + if key != "version" { return Err(DmiError::Generic(format!( - "Error loading icon: improper version header found: {split_version:#?}" + "Error loading icon: improper version header found: {key} = {value} ('{current_line}')" ))); }; - let version = split_version[1].to_string(); + let version = value; let mut width = None; let mut height = None; @@ -83,25 +183,18 @@ impl Icon { Some(thing) => *thing, None => { return Err(DmiError::Generic( - "Error loading icon: DMI definition abruptly ends.".to_string(), + String::from("Error loading icon: DMI definition abruptly ends."), )) } }; - let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); - - if split_version.len() != 2 { - return Err(DmiError::Generic(format!( - "Error loading icon: improper line entry found: {split_version:#?}" - ))); - } - - match split_version[0] { + let (key, value) = parse_dmi_line(current_line, false, false)?; + match key.as_str() { "\twidth" => { - width = Some(split_version[1].parse::()?); + width = Some(value.parse::()?); decompressed_text.next(); // consume the peeked value } "\theight" => { - height = Some(split_version[1].parse::()?); + height = Some(value.parse::()?); decompressed_text.next(); // consume the peeked value } _ => { @@ -205,26 +298,14 @@ impl Icon { break; }; - let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); - if split_version.len() != 2 || split_version[0] != "state" { + let (key, value) = parse_dmi_line(current_line, true, true)?; + if key != "state" { return Err(DmiError::Generic(format!( - "Error loading icon: improper state found: {split_version:#?}" + "Error loading icon: Was expecting the next line's entry to have a key of 'state', but encountered '{key}'! The full line contents are as follows: '{current_line}'" ))); }; - let name = split_version[1].as_bytes(); - if !name.starts_with(b"\"") || !name.ends_with(b"\"") { - return Err(DmiError::Generic(format!("Error loading icon: invalid name icon_state found in metadata, should be preceded and succeeded by double-quotes (\"): {name:#?}"))); - }; - let name = match name.len() { - 0 | 1 => { - return Err(DmiError::Generic(format!( - "Error loading icon: invalid name icon_state found in metadata, improper size: {name:#?}" - ))) - } - 2 => String::new(), //Only the quotes, empty name otherwise. - length => String::from_utf8(name[1..(length - 1)].to_vec())?, //Hacky way to trim. Blame the cool methods being nightly experimental. - }; + let name: String = value; let mut dirs = None; let mut frames = None; @@ -245,36 +326,31 @@ impl Icon { } }; - if current_line == "# END DMI" || current_line.starts_with("state = \"") { + if current_line == "# END DMI" || !current_line.starts_with('\t') { break; }; - let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); - if split_version.len() != 2 { - return Err(DmiError::Generic(format!( - "Error loading icon: improper state found: {split_version:#?}" - ))); - }; + let (key, value) = parse_dmi_line(current_line, false, false)?; - match split_version[0] { - "\tdirs" => dirs = Some(split_version[1].parse::()?), - "\tframes" => frames = Some(split_version[1].parse::()?), + match key.as_str() { + "\tdirs" => dirs = Some(value.parse::()?), + "\tframes" => frames = Some(value.parse::()?), "\tdelay" => { let mut delay_vector = vec![]; - let text_delays = split_version[1].split_terminator(','); + let text_delays = value.split_terminator(','); for text_entry in text_delays { delay_vector.push(text_entry.parse::()?); } delay = Some(delay_vector); } - "\tloop" => loop_flag = Looping::new(split_version[1].parse::()?), - "\trewind" => rewind = split_version[1].parse::()? != 0, - "\tmovement" => movement = split_version[1].parse::()? != 0, + "\tloop" => loop_flag = Looping::new(value.parse::()?), + "\trewind" => rewind = value.parse::()? != 0, + "\tmovement" => movement = value.parse::()? != 0, "\thotspot" => { - let text_coordinates: Vec<&str> = split_version[1].split_terminator(',').collect(); + let text_coordinates: Vec<&str> = value.split_terminator(',').collect(); // Hotspot includes a mysterious 3rd parameter that always seems to be 1. if text_coordinates.len() != 3 { return Err(DmiError::Generic(format!( - "Error loading icon: improper hotspot found: {split_version:#?}" + "Error loading icon: improper hotspot found: {current_line:#?}" ))); }; hotspot = Some(Hotspot { @@ -283,6 +359,7 @@ impl Icon { }); } _ => { + let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); unknown_settings = match unknown_settings { None => { let mut new_map = HashMap::new(); @@ -360,7 +437,7 @@ impl Icon { signature.push_str(&format!( "state = \"{}\"\n\tdirs = {}\n\tframes = {}\n", - icon_state.name, icon_state.dirs, icon_state.frames + icon_state.name.replace("\\", "\\\\").replace("\"", "\\\""), icon_state.dirs, icon_state.frames )); if icon_state.frames > 1 { diff --git a/tests/bench_dmi_load.rs b/tests/bench_dmi_load.rs new file mode 100644 index 0000000..08824ca --- /dev/null +++ b/tests/bench_dmi_load.rs @@ -0,0 +1,57 @@ +/* +use dmi::icon::Icon; +use std::fs::File; +use std::path::Path; +use std::time::Instant; + +#[test] +fn bench_dmi_load() { + println!("Icon::load_meta bench"); + + let mut num_calls = 0; + let mut microsec_calls = 0; + for _ in 0..10 { + recurse_process(Path::new("C:\\Users\\itsmeow\\Desktop\\Development\\SS13\\tgstation\\icons"), &mut num_calls, &mut microsec_calls, false); + } + println!("Num calls: {num_calls}"); + println!("Total Call Duration (μs): {microsec_calls}"); + let mtpc = microsec_calls / num_calls as u128; + println!("MTPC (μs): {mtpc}"); + + println!("Icon::load bench"); + + num_calls = 0; + microsec_calls = 0; + for _ in 0..10 { + recurse_process(Path::new("C:\\Users\\itsmeow\\Desktop\\Development\\SS13\\tgstation\\icons"), &mut num_calls, &mut microsec_calls, false); + } + println!("Num calls: {num_calls}"); + println!("Total Call Duration (μs): {microsec_calls}"); + let mtpc = microsec_calls / num_calls as u128; + println!("MTPC (μs): {mtpc}"); +} + +fn recurse_process(path: &Path, num_calls: &mut u32, microsec_calls: &mut u128, load_images: bool) { + if path.is_dir() { + if let Ok(entries) = std::fs::read_dir(path) { + for entry in entries.flatten() { + let path = entry.path(); + recurse_process(&path, num_calls, microsec_calls, load_images); + } + } + } else if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + if ext != "dmi" { + return; + } + let load_file = File::open(path).unwrap_or_else(|_| panic!("No dmi: {path:?}")); + let start = Instant::now(); + if load_images { + let _ = Icon::load(&load_file).expect("Unable to dmi metadata"); + } else { + let _ = Icon::load_meta(&load_file).expect("Unable to dmi metadata"); + } + *microsec_calls += start.elapsed().as_micros(); + *num_calls += 1; + } +} +*/ diff --git a/tests/dmi_ops.rs b/tests/dmi_ops.rs index 915a94f..2e38f14 100644 --- a/tests/dmi_ops.rs +++ b/tests/dmi_ops.rs @@ -9,29 +9,38 @@ fn load_and_save_dmi() { let load_file = File::open(load_path.as_path()).unwrap_or_else(|_| panic!("No lights dmi: {load_path:?}")); let lights_icon = Icon::load(&load_file).expect("Unable to load lights dmi"); + + + assert_eq!(lights_icon.version, DmiVersion::default()); + assert_eq!(lights_icon.width, 160); + assert_eq!(lights_icon.height, 160); + assert_eq!(lights_icon.states.len(), 4); + + assert_default_state(&lights_icon.states[0], "0_1"); + assert_default_state(&lights_icon.states[1], "1_1"); + assert_default_state(&lights_icon.states[2], ""); + assert_default_state(&lights_icon.states[3], "\\\\ \\ \\\"\\t\\st\\\\\\T+e=5235=!\""); + let mut write_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); write_path.push("tests/resources/save_test.dmi"); let mut write_file = File::create(write_path.as_path()).expect("Failed to create dmi file"); let _written_dmi = lights_icon .save(&mut write_file) .expect("Failed to save lights dmi"); -} -#[test] -fn load_dmi_meta() { - let mut load_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - load_path.push("tests/resources/load_test.dmi"); - let load_file = - File::open(load_path.as_path()).unwrap_or_else(|_| panic!("No lights dmi: {load_path:?}")); - let lights_icon = Icon::load_meta(&load_file).expect("Unable to load lights dmi metadata"); + let load_write_file = + File::open(write_path.as_path()).unwrap_or_else(|_| panic!("No lights dmi: {load_path:?}")); + let reloaded_lights_icon = Icon::load_meta(&load_write_file).expect("Unable to load lights dmi"); - assert_eq!(lights_icon.version, DmiVersion::default()); - assert_eq!(lights_icon.width, 160); - assert_eq!(lights_icon.height, 160); - assert_eq!(lights_icon.states.len(), 2); + assert_eq!(reloaded_lights_icon.version, DmiVersion::default()); + assert_eq!(reloaded_lights_icon.width, 160); + assert_eq!(reloaded_lights_icon.height, 160); + assert_eq!(reloaded_lights_icon.states.len(), 4); - assert_default_state(&lights_icon.states[0], "0_1"); - assert_default_state(&lights_icon.states[1], "1_1"); + assert_default_state(&reloaded_lights_icon.states[0], "0_1"); + assert_default_state(&reloaded_lights_icon.states[1], "1_1"); + assert_default_state(&reloaded_lights_icon.states[2], ""); + assert_default_state(&reloaded_lights_icon.states[3], "\\\\ \\ \\\"\\t\\st\\\\\\T+e=5235=!\""); } fn assert_default_state(state: &IconState, name: &'static str) { diff --git a/tests/resources/load_test.dmi b/tests/resources/load_test.dmi index 5fe7793ba684f36d21cfba95dc22688e39f85865..b61121ef751a755b72709d2b5bd8cad1e807f60e 100644 GIT binary patch literal 5353 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 VuXppZ=<^3~?u_&4`cnaE{{uZ8EK&df diff --git a/tests/resources/save_test.dmi b/tests/resources/save_test.dmi deleted file mode 100644 index dd84169e5ff2c48b52a3f2ca3b3c0db33bd5f68a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4530 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 sWJ&0z(~0wd7YFwJzw&=P2)l=V-N#Q+{XPTi_@% From 41794216b125fb43af0eb078f44e66a84ce57da5 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Fri, 1 Aug 2025 05:04:12 -0700 Subject: [PATCH 13/25] clippy + format --- src/icon.rs | 31 ++++++++++++++++++++----------- tests/dmi_ops.rs | 11 ++++++++--- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/icon.rs b/src/icon.rs index d27f734..352329c 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -56,7 +56,11 @@ struct DmiHeaders { /// The second string cannot be empty (a value must exist), or a DmiError is returned. /// Only one set of quotes is allowed if allow_quotes is true, and it must wrap the entire value. /// If require_quotes is set, will error if there are not quotes around the value. -fn parse_dmi_line(line: &str, allow_quotes: bool, require_quotes: bool) -> Result<(String, String), DmiError> { +fn parse_dmi_line( + line: &str, + allow_quotes: bool, + require_quotes: bool, +) -> Result<(String, String), DmiError> { let mut prior_equals = String::with_capacity(9); // 'movement' is the longest DMI key let mut post_equals = String::with_capacity(line.len() - 3); let mut equals_encountered = false; @@ -73,7 +77,9 @@ fn parse_dmi_line(line: &str, allow_quotes: bool, require_quotes: bool) -> Resul match char { '\\' => { if !quoted_post_equals { - return Err(DmiError::Generic(format!("Backslash found in line with value '{line}' after first equals without quotes."))); + return Err(DmiError::Generic(format!( + "Backslash found in line with value '{line}' after first equals without quotes." + ))); } if !escape_this_quote { escape_quotes = true; @@ -102,7 +108,7 @@ fn parse_dmi_line(line: &str, allow_quotes: bool, require_quotes: bool) -> Resul return Err(DmiError::BlockEntry(format!("Invalid character {char} found in line with value '{line}' after first equals without quotes."))); } } - ' ' => { + ' ' => { if !quoted_post_equals { if post_equals.is_empty() { continue; @@ -124,9 +130,11 @@ fn parse_dmi_line(line: &str, allow_quotes: bool, require_quotes: bool) -> Resul equals_encountered = true; continue; } - ' ' => { + ' ' => { if char_idx + 1 == num_chars { - return Err(DmiError::BlockEntry(format!("Line with value '{line}' abruptly ends on a space with no equals after it."))); + return Err(DmiError::BlockEntry(format!( + "Line with value '{line}' abruptly ends on a space with no equals after it." + ))); } let next_char = line_bytes[char_idx + 1] as char; if next_char != '=' { @@ -145,11 +153,10 @@ fn parse_dmi_line(line: &str, allow_quotes: bool, require_quotes: bool) -> Resul "No value was found for line: '{line}'!" ))); }; - return Ok((prior_equals, post_equals)); + Ok((prior_equals, post_equals)) } impl Icon { - fn read_dmi_headers( decompressed_text: &mut std::iter::Peekable>, ) -> Result { @@ -182,9 +189,9 @@ impl Icon { let current_line = match decompressed_text.peek() { Some(thing) => *thing, None => { - return Err(DmiError::Generic( - String::from("Error loading icon: DMI definition abruptly ends."), - )) + return Err(DmiError::Generic(String::from( + "Error loading icon: DMI definition abruptly ends.", + ))) } }; let (key, value) = parse_dmi_line(current_line, false, false)?; @@ -437,7 +444,9 @@ impl Icon { signature.push_str(&format!( "state = \"{}\"\n\tdirs = {}\n\tframes = {}\n", - icon_state.name.replace("\\", "\\\\").replace("\"", "\\\""), icon_state.dirs, icon_state.frames + icon_state.name.replace("\\", "\\\\").replace("\"", "\\\""), + icon_state.dirs, + icon_state.frames )); if icon_state.frames > 1 { diff --git a/tests/dmi_ops.rs b/tests/dmi_ops.rs index 2e38f14..66da4ec 100644 --- a/tests/dmi_ops.rs +++ b/tests/dmi_ops.rs @@ -10,7 +10,6 @@ fn load_and_save_dmi() { File::open(load_path.as_path()).unwrap_or_else(|_| panic!("No lights dmi: {load_path:?}")); let lights_icon = Icon::load(&load_file).expect("Unable to load lights dmi"); - assert_eq!(lights_icon.version, DmiVersion::default()); assert_eq!(lights_icon.width, 160); assert_eq!(lights_icon.height, 160); @@ -19,7 +18,10 @@ fn load_and_save_dmi() { assert_default_state(&lights_icon.states[0], "0_1"); assert_default_state(&lights_icon.states[1], "1_1"); assert_default_state(&lights_icon.states[2], ""); - assert_default_state(&lights_icon.states[3], "\\\\ \\ \\\"\\t\\st\\\\\\T+e=5235=!\""); + assert_default_state( + &lights_icon.states[3], + "\\\\ \\ \\\"\\t\\st\\\\\\T+e=5235=!\"", + ); let mut write_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); write_path.push("tests/resources/save_test.dmi"); @@ -40,7 +42,10 @@ fn load_and_save_dmi() { assert_default_state(&reloaded_lights_icon.states[0], "0_1"); assert_default_state(&reloaded_lights_icon.states[1], "1_1"); assert_default_state(&reloaded_lights_icon.states[2], ""); - assert_default_state(&reloaded_lights_icon.states[3], "\\\\ \\ \\\"\\t\\st\\\\\\T+e=5235=!\""); + assert_default_state( + &reloaded_lights_icon.states[3], + "\\\\ \\ \\\"\\t\\st\\\\\\T+e=5235=!\"", + ); } fn assert_default_state(state: &IconState, name: &'static str) { From b80a6d945d4f84c44eacc0bb3fcac2dec86e235d Mon Sep 17 00:00:00 2001 From: itsmeow Date: Fri, 1 Aug 2025 05:33:41 -0700 Subject: [PATCH 14/25] Improve the comment --- src/icon.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/icon.rs b/src/icon.rs index 352329c..76ac9c0 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -50,12 +50,17 @@ struct DmiHeaders { } /// Splits the line of a DMI entry into a key/value pair on the equals sign. -/// Removes spaces or equals signs that are not inside quotes. -/// Tabs are left intact only prior to the first equals sign, and only as the first character parsed. -/// Removes quotes around values and escape characters for quotes inside the quotes. /// The second string cannot be empty (a value must exist), or a DmiError is returned. /// Only one set of quotes is allowed if allow_quotes is true, and it must wrap the entire value. /// If require_quotes is set, will error if there are not quotes around the value. +/// +/// Other details about this function: +/// +/// Keys have very little validation and are meant to be checked against a known value in most cases. +/// Spaces are only allowed in the value if they are inside quotes or directly after the equals sign (where they are removed). +/// Tabs and equals signs are only allowed in the value if they are not inside quotes. +/// Removes quotes around values and removes backslashes for quotes inside the quotes. +/// Removes backslashes used to escape other backslashes. fn parse_dmi_line( line: &str, allow_quotes: bool, From a7abfbb728511b5bcfead06fbd3cbe5982eea32d Mon Sep 17 00:00:00 2001 From: itsmeow Date: Fri, 1 Aug 2025 05:35:24 -0700 Subject: [PATCH 15/25] Move read_dmi_headers off Icon impl --- src/icon.rs | 118 ++++++++++++++++++++++++++-------------------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/src/icon.rs b/src/icon.rs index 76ac9c0..4f6e3ad 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -53,11 +53,11 @@ struct DmiHeaders { /// The second string cannot be empty (a value must exist), or a DmiError is returned. /// Only one set of quotes is allowed if allow_quotes is true, and it must wrap the entire value. /// If require_quotes is set, will error if there are not quotes around the value. -/// +/// /// Other details about this function: -/// +/// /// Keys have very little validation and are meant to be checked against a known value in most cases. -/// Spaces are only allowed in the value if they are inside quotes or directly after the equals sign (where they are removed). +/// Spaces are only allowed in the value if they are inside quotes or directly after the equals sign (where they are removed). /// Tabs and equals signs are only allowed in the value if they are not inside quotes. /// Removes quotes around values and removes backslashes for quotes inside the quotes. /// Removes backslashes used to escape other backslashes. @@ -161,73 +161,73 @@ fn parse_dmi_line( Ok((prior_equals, post_equals)) } -impl Icon { - fn read_dmi_headers( - decompressed_text: &mut std::iter::Peekable>, - ) -> Result { - let current_line = decompressed_text.next(); - if current_line != Some("# BEGIN DMI") { - return Err(DmiError::Generic(format!( - "Error loading icon: no DMI header found. Beginning: {current_line:#?}" - ))); - }; +fn read_dmi_headers( + decompressed_text: &mut std::iter::Peekable>, +) -> Result { + let current_line = decompressed_text.next(); + if current_line != Some("# BEGIN DMI") { + return Err(DmiError::Generic(format!( + "Error loading icon: no DMI header found. Beginning: {current_line:#?}" + ))); + }; - let current_line = match decompressed_text.next() { - Some(thing) => thing, + let current_line = match decompressed_text.next() { + Some(thing) => thing, + None => { + return Err(DmiError::Generic(String::from( + "Error loading icon: no version header found.", + ))) + } + }; + let (key, value) = parse_dmi_line(current_line, false, false)?; + if key != "version" { + return Err(DmiError::Generic(format!( + "Error loading icon: improper version header found: {key} = {value} ('{current_line}')" + ))); + }; + let version = value; + + let mut width = None; + let mut height = None; + for _ in 0..2 { + let current_line = match decompressed_text.peek() { + Some(thing) => *thing, None => { return Err(DmiError::Generic(String::from( - "Error loading icon: no version header found.", + "Error loading icon: DMI definition abruptly ends.", ))) } }; let (key, value) = parse_dmi_line(current_line, false, false)?; - if key != "version" { - return Err(DmiError::Generic(format!( - "Error loading icon: improper version header found: {key} = {value} ('{current_line}')" - ))); - }; - let version = value; - - let mut width = None; - let mut height = None; - for _ in 0..2 { - let current_line = match decompressed_text.peek() { - Some(thing) => *thing, - None => { - return Err(DmiError::Generic(String::from( - "Error loading icon: DMI definition abruptly ends.", - ))) - } - }; - let (key, value) = parse_dmi_line(current_line, false, false)?; - match key.as_str() { - "\twidth" => { - width = Some(value.parse::()?); - decompressed_text.next(); // consume the peeked value - } - "\theight" => { - height = Some(value.parse::()?); - decompressed_text.next(); // consume the peeked value - } - _ => { - break; - } + match key.as_str() { + "\twidth" => { + width = Some(value.parse::()?); + decompressed_text.next(); // consume the peeked value + } + "\theight" => { + height = Some(value.parse::()?); + decompressed_text.next(); // consume the peeked value + } + _ => { + break; } } + } - if width == Some(0) || height == Some(0) { - return Err(DmiError::Generic(format!( - "Error loading icon: invalid width ({width:#?}) / height ({height:#?}) values." - ))); - }; + if width == Some(0) || height == Some(0) { + return Err(DmiError::Generic(format!( + "Error loading icon: invalid width ({width:#?}) / height ({height:#?}) values." + ))); + }; - Ok(DmiHeaders { - version, - width, - height, - }) - } + Ok(DmiHeaders { + version, + width, + height, + }) +} +impl Icon { pub fn load(reader: R) -> Result { Self::load_internal(reader, true) } @@ -271,7 +271,7 @@ impl Icon { let decompressed_text = String::from_utf8(decompressed_text)?; let mut decompressed_text = decompressed_text.lines().peekable(); - let dmi_headers = Self::read_dmi_headers(&mut decompressed_text)?; + let dmi_headers = read_dmi_headers(&mut decompressed_text)?; let version = dmi_headers.version; // yes you can make a DMI without a width or height. it defaults to 32x32 From 3945d84a5586c9fdc1f397e146c4ec19264f6c2b Mon Sep 17 00:00:00 2001 From: itsmeow Date: Fri, 1 Aug 2025 06:40:18 -0700 Subject: [PATCH 16/25] Speed up by returning the key as a borrowed slice --- src/icon.rs | 159 ++++++++++++++++++++-------------------------------- 1 file changed, 62 insertions(+), 97 deletions(-) diff --git a/src/icon.rs b/src/icon.rs index 4f6e3ad..3488366 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -65,100 +65,64 @@ fn parse_dmi_line( line: &str, allow_quotes: bool, require_quotes: bool, -) -> Result<(String, String), DmiError> { - let mut prior_equals = String::with_capacity(9); // 'movement' is the longest DMI key - let mut post_equals = String::with_capacity(line.len() - 3); - let mut equals_encountered = false; - let mut quoted_post_equals = false; - let mut escape_quotes = false; - let mut quotes_ended = false; - let num_chars = line.len(); - let line_bytes = line.as_bytes(); +) -> Result<(&str, String), DmiError> { + let line_split = line.split_once(" = "); + if line_split.is_none() { + return Err(DmiError::BlockEntry(format!("No value was found for line: '{line}' (must contain ' = ')!"))); + } + let line_split = line_split.unwrap(); + // Now we need to parse after the equals + let num_chars = line_split.1.len(); + let mut post_equals = String::with_capacity(num_chars - if require_quotes { 2 } else { 0 }); + + // Flags + let mut quoted = false; + let mut escaped = false; + + let value_bytes = line_split.1.as_bytes(); for char_idx in 0..num_chars { - let char = line_bytes[char_idx] as char; - if equals_encountered { - let escape_this_quote = escape_quotes; - escape_quotes = false; - match char { - '\\' => { - if !quoted_post_equals { - return Err(DmiError::Generic(format!( - "Backslash found in line with value '{line}' after first equals without quotes." - ))); - } - if !escape_this_quote { - escape_quotes = true; - continue; - } + let char: char = value_bytes[char_idx] as char; + let escape_this_char = escaped; + escaped = false; + match char { + '\\' => { + if !quoted { + return Err(DmiError::Generic(format!( + "Backslash found in line with value '{line}' after first equals without quotes." + ))); } - '"' => { - if !allow_quotes { - return Err(DmiError::Generic(format!("Quote found in line with value '{line}' after first equals where they are not allowed."))); - } - if !escape_this_quote { - if quoted_post_equals && char_idx + 1 != num_chars { - return Err(DmiError::BlockEntry(format!("Line with value '{line}' ends quotes prior to the last character on the line. This is not allowed."))); - } else if !quoted_post_equals && !post_equals.is_empty() { - return Err(DmiError::BlockEntry(format!("Line with value '{line}' starts quotes after the first character in its value. This is not allowed."))); - } - quoted_post_equals = !quoted_post_equals; - if !quoted_post_equals { - quotes_ended = true; - } - continue; - } + if !escape_this_char { + escaped = true; + continue; } - '\t' | '=' => { - if !quoted_post_equals { - return Err(DmiError::BlockEntry(format!("Invalid character {char} found in line with value '{line}' after first equals without quotes."))); - } + } + '"' => { + if !allow_quotes { + return Err(DmiError::Generic(format!("Quote found in line with value '{line}' after first equals where they are not allowed."))); } - ' ' => { - if !quoted_post_equals { - if post_equals.is_empty() { - continue; - } else { - return Err(DmiError::BlockEntry(format!("Space found in line with value '{line}' after first equals without quotes. Only one space is allowed directly after the equals sign."))); - } + if !escape_this_char { + if quoted && char_idx + 1 != num_chars { + return Err(DmiError::BlockEntry(format!("Line with value '{line}' ends quotes prior to the last character on the line. This is not allowed."))); + } else if !quoted && !post_equals.is_empty() { + return Err(DmiError::BlockEntry(format!("Line with value '{line}' starts quotes after the first character in its value. This is not allowed."))); } - } - _ => {} - } - if allow_quotes && require_quotes && !quoted_post_equals { - return Err(DmiError::Generic(format!("Line with value '{line}' is required to have quotes after the equals sign, but does not quote all its contents!"))); - } - post_equals.push(char); - } else { - // Keys (prior to equals) are almost always checked against a value, so there's no point in doing extensive checks ourselves. - match char { - '=' => { - equals_encountered = true; + quoted = !quoted; continue; } - ' ' => { - if char_idx + 1 == num_chars { - return Err(DmiError::BlockEntry(format!( - "Line with value '{line}' abruptly ends on a space with no equals after it." - ))); - } - let next_char = line_bytes[char_idx + 1] as char; - if next_char != '=' { - return Err(DmiError::BlockEntry(format!("Line with value '{line}' contains a space not directly prior to an equals sign before the first equals sign was encountered."))); - } else { - continue; - } + } + '\t' | '=' | ' ' => { + if !quoted { + return Err(DmiError::BlockEntry(format!("Invalid character {char} found in line with value '{line}' after first equals without quotes."))); } - _ => {} } - prior_equals.push(char); + _ => {} + } + if allow_quotes && require_quotes && !quoted { + return Err(DmiError::Generic(format!("Line with value '{line}' is required to have quotes after the equals sign, but does not quote all its contents!"))); } + post_equals.push(char); } - if post_equals.is_empty() && !quotes_ended { - return Err(DmiError::BlockEntry(format!( - "No value was found for line: '{line}'!" - ))); - }; - Ok((prior_equals, post_equals)) + Ok((line_split.0, post_equals)) } fn read_dmi_headers( @@ -199,7 +163,7 @@ fn read_dmi_headers( } }; let (key, value) = parse_dmi_line(current_line, false, false)?; - match key.as_str() { + match key { "\twidth" => { width = Some(value.parse::()?); decompressed_text.next(); // consume the peeked value @@ -343,7 +307,7 @@ impl Icon { }; let (key, value) = parse_dmi_line(current_line, false, false)?; - match key.as_str() { + match key { "\tdirs" => dirs = Some(value.parse::()?), "\tframes" => frames = Some(value.parse::()?), "\tdelay" => { @@ -371,18 +335,19 @@ impl Icon { }); } _ => { - let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); - unknown_settings = match unknown_settings { - None => { - let mut new_map = HashMap::new(); - new_map.insert(split_version[0].to_string(), split_version[1].to_string()); - Some(new_map) - } - Some(mut thing) => { - thing.insert(split_version[0].to_string(), split_version[1].to_string()); - Some(thing) - } - }; + if let Some((key, value)) = current_line.split_once(" = ") { + unknown_settings = match unknown_settings { + None => { + let mut new_map = HashMap::new(); + new_map.insert(key.to_string(), value.to_string()); + Some(new_map) + } + Some(mut thing) => { + thing.insert(key.to_string(), value.to_string()); + Some(thing) + } + }; + } } }; } From 7065a5e771c69b16cd1a9e0d92b9be1866b783c3 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Fri, 1 Aug 2025 06:44:45 -0700 Subject: [PATCH 17/25] clippy + fmt + microop --- src/icon.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/icon.rs b/src/icon.rs index 3488366..3fe6fd8 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -68,7 +68,9 @@ fn parse_dmi_line( ) -> Result<(&str, String), DmiError> { let line_split = line.split_once(" = "); if line_split.is_none() { - return Err(DmiError::BlockEntry(format!("No value was found for line: '{line}' (must contain ' = ')!"))); + return Err(DmiError::BlockEntry(format!( + "No value was found for line: '{line}' (must contain ' = ')!" + ))); } let line_split = line_split.unwrap(); // Now we need to parse after the equals @@ -78,10 +80,11 @@ fn parse_dmi_line( // Flags let mut quoted = false; let mut escaped = false; + let mut used_quotes = false; let value_bytes = line_split.1.as_bytes(); - for char_idx in 0..num_chars { - let char: char = value_bytes[char_idx] as char; + for (char_idx, char) in value_bytes.iter().enumerate() { + let char = *char as char; let escape_this_char = escaped; escaped = false; match char { @@ -107,6 +110,7 @@ fn parse_dmi_line( return Err(DmiError::BlockEntry(format!("Line with value '{line}' starts quotes after the first character in its value. This is not allowed."))); } quoted = !quoted; + used_quotes = true; continue; } } @@ -117,11 +121,11 @@ fn parse_dmi_line( } _ => {} } - if allow_quotes && require_quotes && !quoted { - return Err(DmiError::Generic(format!("Line with value '{line}' is required to have quotes after the equals sign, but does not quote all its contents!"))); - } post_equals.push(char); } + if allow_quotes && require_quotes && !used_quotes { + return Err(DmiError::Generic(format!("Line with value '{line}' is required to have quotes after the equals sign, but does not wrap its contents in quotes!"))); + } Ok((line_split.0, post_equals)) } From 08826e9e7bf637666f8b918e0d43f0d169ea7af8 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Fri, 1 Aug 2025 06:55:54 -0700 Subject: [PATCH 18/25] Update example benchmark to be more useful to others --- tests/bench_dmi_load.rs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/tests/bench_dmi_load.rs b/tests/bench_dmi_load.rs index 08824ca..0cacae8 100644 --- a/tests/bench_dmi_load.rs +++ b/tests/bench_dmi_load.rs @@ -4,26 +4,41 @@ use std::fs::File; use std::path::Path; use std::time::Instant; +const ICONS_FOLDER: &'static str = "UPDATEME"; + #[test] fn bench_dmi_load() { - println!("Icon::load_meta bench"); + let icons_folder_path = Path::new(ICONS_FOLDER); + + println!("Icon::load_meta bench\n---"); let mut num_calls = 0; let mut microsec_calls = 0; - for _ in 0..10 { - recurse_process(Path::new("C:\\Users\\itsmeow\\Desktop\\Development\\SS13\\tgstation\\icons"), &mut num_calls, &mut microsec_calls, false); + for _ in 0..25 { + recurse_process( + icons_folder_path, + &mut num_calls, + &mut microsec_calls, + false, + ); } println!("Num calls: {num_calls}"); println!("Total Call Duration (μs): {microsec_calls}"); let mtpc = microsec_calls / num_calls as u128; println!("MTPC (μs): {mtpc}"); - println!("Icon::load bench"); + println!("Icon::load bench\n---"); num_calls = 0; microsec_calls = 0; - for _ in 0..10 { - recurse_process(Path::new("C:\\Users\\itsmeow\\Desktop\\Development\\SS13\\tgstation\\icons"), &mut num_calls, &mut microsec_calls, false); + // this is seriously slow. 2 iterations max or you'll be waiting all day + for _ in 0..1 { + recurse_process( + icons_folder_path, + &mut num_calls, + &mut microsec_calls, + false, + ); } println!("Num calls: {num_calls}"); println!("Total Call Duration (μs): {microsec_calls}"); From 36bc2f927c6bab7f694d0208a9aa7113b3ae3af9 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Fri, 1 Aug 2025 08:19:53 -0700 Subject: [PATCH 19/25] Super mega fast image crop by doing direct PNG decoding --- Cargo.toml | 1 + src/error.rs | 3 + src/icon.rs | 152 ++++++++++++++++++++++------ tests/dmi_ops.rs | 13 +++ tests/resources/empty.dmi | Bin 0 -> 216 bytes tests/resources/greyscale_alpha.dmi | Bin 0 -> 252 bytes 6 files changed, 140 insertions(+), 29 deletions(-) create mode 100644 tests/resources/empty.dmi create mode 100644 tests/resources/greyscale_alpha.dmi diff --git a/Cargo.toml b/Cargo.toml index 238648e..875114e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,4 +16,5 @@ crc32fast = "1" deflate = "1" image = { version = "0.25", default-features = false, features = ["png"] } inflate = "0.4" +png = "0.17.16" thiserror = "2" diff --git a/src/error.rs b/src/error.rs index 16e164d..58b370b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,4 @@ +use png::DecodingError; use std::io; use thiserror::Error; @@ -5,6 +6,8 @@ use thiserror::Error; pub enum DmiError { #[error("IO error: {0}")] Io(#[from] io::Error), + #[error("PNG decoding error: {0}")] + PngDecoding(#[from] DecodingError), #[error("Image-processing error: {0}")] Image(#[from] image::error::ImageError), #[error("FromUtf8 error: {0}")] diff --git a/src/icon.rs b/src/icon.rs index 3fe6fd8..492b2b3 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -1,7 +1,8 @@ use crate::dirs::{Dirs, ALL_DIRS, CARDINAL_DIRS}; use crate::{error::DmiError, ztxt, RawDmi, RawDmiMetadata}; +use ::png::{ColorType, Decoder, Transformations}; use image::codecs::png; -use image::{imageops, DynamicImage}; +use image::{imageops, RgbaImage}; use std::collections::HashMap; use std::io::prelude::*; use std::io::Cursor; @@ -208,30 +209,105 @@ impl Icon { } fn load_internal(reader: R, load_images: bool) -> Result { - let (base_image, dmi_meta) = if load_images { + let (dmi_meta, rgba_bytes) = if load_images { let raw_dmi = RawDmi::load(reader)?; - let mut rawdmi_temp = vec![]; - raw_dmi.save(&mut rawdmi_temp)?; - let chunk_ztxt = match raw_dmi.chunk_ztxt { - Some(chunk) => chunk, - None => { - return Err(DmiError::Generic(String::from( - "Error loading icon: no zTXt chunk found.", - ))) + + let mut total_bytes = 45; + if let Some(chunk_plte) = &raw_dmi.chunk_plte { + total_bytes += chunk_plte.data.len() + 12 + } + if let Some(other_chunks) = &raw_dmi.other_chunks { + for chunk in other_chunks { + total_bytes += chunk.data.len() + 12 + } + } + for idat in &raw_dmi.chunks_idat { + total_bytes += idat.data.len() + 12; + } + + // Reconstruct the PNG + let mut png_data = Vec::with_capacity(total_bytes); + png_data.extend_from_slice(&raw_dmi.header); + png_data.extend_from_slice(&raw_dmi.chunk_ihdr.data_length); + png_data.extend_from_slice(&raw_dmi.chunk_ihdr.chunk_type); + png_data.extend_from_slice(&raw_dmi.chunk_ihdr.data); + png_data.extend_from_slice(&raw_dmi.chunk_ihdr.crc); + if let Some(plte) = &raw_dmi.chunk_plte { + png_data.extend_from_slice(&plte.data_length); + png_data.extend_from_slice(&plte.chunk_type); + png_data.extend_from_slice(&plte.data); + png_data.extend_from_slice(&plte.crc); + } + if let Some(other_chunks) = &raw_dmi.other_chunks { + for chunk in other_chunks { + png_data.extend_from_slice(&chunk.data_length); + png_data.extend_from_slice(&chunk.chunk_type); + png_data.extend_from_slice(&chunk.data); + png_data.extend_from_slice(&chunk.crc); + } + } + for idat in &raw_dmi.chunks_idat { + png_data.extend_from_slice(&idat.data_length); + png_data.extend_from_slice(&idat.chunk_type); + png_data.extend_from_slice(&idat.data); + png_data.extend_from_slice(&idat.crc); + } + png_data.extend_from_slice(&raw_dmi.chunk_iend.data_length); + png_data.extend_from_slice(&raw_dmi.chunk_iend.chunk_type); + png_data.extend_from_slice(&raw_dmi.chunk_iend.crc); + + let mut png_decoder = Decoder::new(std::io::Cursor::new(png_data)); + png_decoder.set_transformations(Transformations::EXPAND | Transformations::ALPHA); + let mut png_reader = png_decoder.read_info()?; + let mut rgba_buf = vec![0u8; png_reader.output_buffer_size()]; + let info = png_reader.next_frame(&mut rgba_buf)?; + + // EXPAND and ALPHA do not expand grayscale images into RGBA. We can just do this manually. + match info.color_type { + ColorType::GrayscaleAlpha => { + if rgba_buf.len() as u32 != info.width * info.height * 2 { + return Err(DmiError::Generic(String::from("GrayscaleAlpha buffer length mismatch"))); + } + let mut new_buf = Vec::with_capacity((info.width * info.height * 4) as usize); + for chunk in rgba_buf.chunks(2) { + let gray = chunk[0]; + let alpha = chunk[1]; + new_buf.push(gray); + new_buf.push(gray); + new_buf.push(gray); + new_buf.push(alpha); + } + rgba_buf = new_buf; + } + ColorType::Grayscale => { + if rgba_buf.len() as u32 != info.width * info.height { + return Err(DmiError::Generic(String::from("Grayscale buffer length mismatch"))); + } + let mut new_buf = Vec::with_capacity((info.width * info.height * 4) as usize); + for gray in rgba_buf { + new_buf.push(gray); + new_buf.push(gray); + new_buf.push(gray); + new_buf.push(255); + } + rgba_buf = new_buf; + } + ColorType::Rgba => {} + _ => { + return Err(DmiError::Generic(format!("Unsupported ColorType (must be RGBA or convertible to RGBA): {:#?}", info.color_type))); } + } + + let dmi_meta = RawDmiMetadata { + chunk_ihdr: raw_dmi.chunk_ihdr, + chunk_ztxt: raw_dmi.chunk_ztxt.ok_or_else(|| { + DmiError::Generic(String::from("Error loading icon: no zTXt chunk found.")) + })?, }; - ( - Some(image::load_from_memory_with_format( - &rawdmi_temp, - image::ImageFormat::Png, - )?), - RawDmiMetadata { - chunk_ihdr: raw_dmi.chunk_ihdr, - chunk_ztxt, - }, - ) + + (dmi_meta, Some(rgba_buf)) } else { - (None, RawDmi::load_meta(reader)?) + (RawDmi::load_meta(reader)?, None) }; let chunk_ztxt = &dmi_meta.chunk_ztxt; @@ -315,7 +391,7 @@ impl Icon { "\tdirs" => dirs = Some(value.parse::()?), "\tframes" => frames = Some(value.parse::()?), "\tdelay" => { - let mut delay_vector = vec![]; + let mut delay_vector = Vec::with_capacity(frames.unwrap_or(0) as usize); let text_delays = value.split_terminator(','); for text_entry in text_delays { delay_vector.push(text_entry.parse::()?); @@ -369,14 +445,32 @@ impl Icon { return Err(DmiError::Generic(format!("Error loading icon: metadata settings exceeded the maximum number of states possible ({max_possible_states})."))); }; - let mut images = vec![]; + let mut images = Vec::with_capacity((frames * dirs as u32) as usize); + + if let Some(rgba_bytes) = &rgba_bytes { + const RGBA_PIXEL_STRIDE: usize = 4; + let row_stride = img_width as usize * RGBA_PIXEL_STRIDE; + let expected_buffer_len = row_stride * (img_height as usize); + if rgba_bytes.len() != expected_buffer_len { + panic!("{} != {}", rgba_bytes.len(), expected_buffer_len); + } - if let Some(full_image) = base_image.as_ref() { - for image_idx in index..(index + (frames * dirs as u32)) { + for image_idx in index..next_index { let x = (image_idx % width_in_states) * width; - //This operation rounds towards zero, truncating any fractional part of the exact result, essentially a floor() function. let y = (image_idx / width_in_states) * height; - images.push(full_image.crop_imm(x, y, width, height)); + + let mut cropped = + Vec::with_capacity((width * height * RGBA_PIXEL_STRIDE as u32) as usize); + for row in y..(y + height) { + let start = (row as usize * row_stride) + (x as usize * RGBA_PIXEL_STRIDE); + let end = start + (width as usize * RGBA_PIXEL_STRIDE); + cropped.extend_from_slice(&rgba_bytes[start..end]); + } + + let tile = image::ImageBuffer::, _>::from_raw(width, height, cropped) + .ok_or_else(|| DmiError::Generic("Failed to create image tile".to_string()))?; + + images.push(tile); } } @@ -593,7 +687,7 @@ pub struct IconState { pub name: String, pub dirs: u8, pub frames: u32, - pub images: Vec, + pub images: Vec, pub delay: Option>, pub loop_flag: Looping, pub rewind: bool, @@ -605,7 +699,7 @@ pub struct IconState { impl IconState { /// Gets a specific DynamicImage from `images`, given a dir and frame. /// If the dir or frame is invalid, returns a DmiError. - pub fn get_image(&self, dir: &Dirs, frame: u32) -> Result<&DynamicImage, DmiError> { + pub fn get_image(&self, dir: &Dirs, frame: u32) -> Result<&RgbaImage, DmiError> { if self.frames < frame { return Err(DmiError::IconState(format!( "Specified frame \"{frame}\" is larger than the number of frames ({}) for icon_state \"{}\"", diff --git a/tests/dmi_ops.rs b/tests/dmi_ops.rs index 66da4ec..dae389e 100644 --- a/tests/dmi_ops.rs +++ b/tests/dmi_ops.rs @@ -4,6 +4,19 @@ use std::path::PathBuf; #[test] fn load_and_save_dmi() { + + let mut load_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + load_path.push("tests/resources/empty.dmi"); + let load_file = + File::open(load_path.as_path()).unwrap_or_else(|_| panic!("No empty dmi: {load_path:?}")); + let _ = Icon::load(&load_file).expect("Unable to load empty dmi"); + + let mut load_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + load_path.push("tests/resources/greyscale_alpha.dmi"); + let load_file = + File::open(load_path.as_path()).unwrap_or_else(|_| panic!("No greyscale_alpha dmi: {load_path:?}")); + let _ = Icon::load(&load_file).expect("Unable to greyscale_alpha dmi"); + let mut load_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); load_path.push("tests/resources/load_test.dmi"); let load_file = diff --git a/tests/resources/empty.dmi b/tests/resources/empty.dmi new file mode 100644 index 0000000000000000000000000000000000000000..6c4f2b33e0fee9d3f99ff3febc0760cfd078aed2 GIT binary patch literal 216 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnL3?x0byx0z;m;-!5Tn`*LkmkKF1;}MA3GxeO zaCmkj4amu^3W+FjNi9w;$}A|!%+F(BsF)KRR!~&>{Y!Ac$FEPcymhtCojD)8A=Kca z@q&JF>8>?V=-0C=2JR&a84_w-Y6@%7{?OD!tS%+FJ>RWQ*r;NmRLOex6#axAzwGg33t zGfIGLQ?BBY#FA7XODQQQF)v$*i!&v&s2IpM5F1N1#}0sn!_^a$=#kH;63|AvRb?7EWx0000 Date: Fri, 1 Aug 2025 08:27:14 -0700 Subject: [PATCH 20/25] fmt --- src/icon.rs | 63 +++++++++++++++++++++++++++--------------------- tests/dmi_ops.rs | 5 ++-- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/icon.rs b/src/icon.rs index 492b2b3..31be95a 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -265,36 +265,43 @@ impl Icon { // EXPAND and ALPHA do not expand grayscale images into RGBA. We can just do this manually. match info.color_type { ColorType::GrayscaleAlpha => { - if rgba_buf.len() as u32 != info.width * info.height * 2 { - return Err(DmiError::Generic(String::from("GrayscaleAlpha buffer length mismatch"))); - } - let mut new_buf = Vec::with_capacity((info.width * info.height * 4) as usize); - for chunk in rgba_buf.chunks(2) { - let gray = chunk[0]; - let alpha = chunk[1]; - new_buf.push(gray); - new_buf.push(gray); - new_buf.push(gray); - new_buf.push(alpha); - } - rgba_buf = new_buf; - } - ColorType::Grayscale => { - if rgba_buf.len() as u32 != info.width * info.height { - return Err(DmiError::Generic(String::from("Grayscale buffer length mismatch"))); - } - let mut new_buf = Vec::with_capacity((info.width * info.height * 4) as usize); - for gray in rgba_buf { - new_buf.push(gray); - new_buf.push(gray); - new_buf.push(gray); - new_buf.push(255); - } - rgba_buf = new_buf; - } + if rgba_buf.len() as u32 != info.width * info.height * 2 { + return Err(DmiError::Generic(String::from( + "GrayscaleAlpha buffer length mismatch", + ))); + } + let mut new_buf = Vec::with_capacity((info.width * info.height * 4) as usize); + for chunk in rgba_buf.chunks(2) { + let gray = chunk[0]; + let alpha = chunk[1]; + new_buf.push(gray); + new_buf.push(gray); + new_buf.push(gray); + new_buf.push(alpha); + } + rgba_buf = new_buf; + } + ColorType::Grayscale => { + if rgba_buf.len() as u32 != info.width * info.height { + return Err(DmiError::Generic(String::from( + "Grayscale buffer length mismatch", + ))); + } + let mut new_buf = Vec::with_capacity((info.width * info.height * 4) as usize); + for gray in rgba_buf { + new_buf.push(gray); + new_buf.push(gray); + new_buf.push(gray); + new_buf.push(255); + } + rgba_buf = new_buf; + } ColorType::Rgba => {} _ => { - return Err(DmiError::Generic(format!("Unsupported ColorType (must be RGBA or convertible to RGBA): {:#?}", info.color_type))); + return Err(DmiError::Generic(format!( + "Unsupported ColorType (must be RGBA or convertible to RGBA): {:#?}", + info.color_type + ))); } } diff --git a/tests/dmi_ops.rs b/tests/dmi_ops.rs index dae389e..df4fbe1 100644 --- a/tests/dmi_ops.rs +++ b/tests/dmi_ops.rs @@ -4,7 +4,6 @@ use std::path::PathBuf; #[test] fn load_and_save_dmi() { - let mut load_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); load_path.push("tests/resources/empty.dmi"); let load_file = @@ -13,8 +12,8 @@ fn load_and_save_dmi() { let mut load_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); load_path.push("tests/resources/greyscale_alpha.dmi"); - let load_file = - File::open(load_path.as_path()).unwrap_or_else(|_| panic!("No greyscale_alpha dmi: {load_path:?}")); + let load_file = File::open(load_path.as_path()) + .unwrap_or_else(|_| panic!("No greyscale_alpha dmi: {load_path:?}")); let _ = Icon::load(&load_file).expect("Unable to greyscale_alpha dmi"); let mut load_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); From b4e230e9a7e973af4f0a871ea8792799727dc5a4 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Fri, 1 Aug 2025 08:50:45 -0700 Subject: [PATCH 21/25] Update bench_dmi_load.rs (lol) --- tests/bench_dmi_load.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bench_dmi_load.rs b/tests/bench_dmi_load.rs index 0cacae8..47dfc9b 100644 --- a/tests/bench_dmi_load.rs +++ b/tests/bench_dmi_load.rs @@ -37,7 +37,7 @@ fn bench_dmi_load() { icons_folder_path, &mut num_calls, &mut microsec_calls, - false, + true, ); } println!("Num calls: {num_calls}"); From 481fd33e7cbe0b96465aa5d955f7da0a1dcdce54 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Sat, 2 Aug 2025 00:54:22 -0700 Subject: [PATCH 22/25] Improve the readability of RawDmi buffer writing and add output_size_buffer() for preallocation --- src/icon.rs | 54 ++++++++---------------------------------------- src/lib.rs | 59 +++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 52 insertions(+), 61 deletions(-) diff --git a/src/icon.rs b/src/icon.rs index 31be95a..ef0daa8 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -212,51 +212,13 @@ impl Icon { let (dmi_meta, rgba_bytes) = if load_images { let raw_dmi = RawDmi::load(reader)?; - let mut total_bytes = 45; - if let Some(chunk_plte) = &raw_dmi.chunk_plte { - total_bytes += chunk_plte.data.len() + 12 - } - if let Some(other_chunks) = &raw_dmi.other_chunks { - for chunk in other_chunks { - total_bytes += chunk.data.len() + 12 - } - } - for idat in &raw_dmi.chunks_idat { - total_bytes += idat.data.len() + 12; - } - - // Reconstruct the PNG - let mut png_data = Vec::with_capacity(total_bytes); - png_data.extend_from_slice(&raw_dmi.header); - png_data.extend_from_slice(&raw_dmi.chunk_ihdr.data_length); - png_data.extend_from_slice(&raw_dmi.chunk_ihdr.chunk_type); - png_data.extend_from_slice(&raw_dmi.chunk_ihdr.data); - png_data.extend_from_slice(&raw_dmi.chunk_ihdr.crc); - if let Some(plte) = &raw_dmi.chunk_plte { - png_data.extend_from_slice(&plte.data_length); - png_data.extend_from_slice(&plte.chunk_type); - png_data.extend_from_slice(&plte.data); - png_data.extend_from_slice(&plte.crc); - } - if let Some(other_chunks) = &raw_dmi.other_chunks { - for chunk in other_chunks { - png_data.extend_from_slice(&chunk.data_length); - png_data.extend_from_slice(&chunk.chunk_type); - png_data.extend_from_slice(&chunk.data); - png_data.extend_from_slice(&chunk.crc); - } - } - for idat in &raw_dmi.chunks_idat { - png_data.extend_from_slice(&idat.data_length); - png_data.extend_from_slice(&idat.chunk_type); - png_data.extend_from_slice(&idat.data); - png_data.extend_from_slice(&idat.crc); - } - png_data.extend_from_slice(&raw_dmi.chunk_iend.data_length); - png_data.extend_from_slice(&raw_dmi.chunk_iend.chunk_type); - png_data.extend_from_slice(&raw_dmi.chunk_iend.crc); + // Reconstruct the full PNG from memory. Preallocating the size saves a lot of compute here. + let mut png_data = Vec::with_capacity(raw_dmi.output_buffer_size(false)); + raw_dmi.save(&mut png_data, false)?; let mut png_decoder = Decoder::new(std::io::Cursor::new(png_data)); + // this will convert RGB->RGBA and increase bit depth to 8, interpret tRNS chunks, interpret PLTE chunks + // notably does not convert greyscale color types to RGB. png_decoder.set_transformations(Transformations::EXPAND | Transformations::ALPHA); let mut png_reader = png_decoder.read_info()?; let mut rgba_buf = vec![0u8; png_reader.output_buffer_size()]; @@ -336,7 +298,7 @@ impl Icon { let img_height = u32::from_be_bytes([ihdr_data[4], ihdr_data[5], ihdr_data[6], ihdr_data[7]]); if img_width == 0 || img_height == 0 || img_width % width != 0 || img_height % height != 0 { - return Err(DmiError::Generic(format!("Error loading icon: invalid image width ({img_width}) / height ({img_height}) values. Missmatch with metadata width ({width}) / height ({height})."))); + return Err(DmiError::Generic(format!("Error loading icon: invalid image width ({img_width}) / height ({img_height}) values. Mismatch with metadata width ({width}) / height ({height})."))); }; let width_in_states = img_width / width; @@ -505,7 +467,7 @@ impl Icon { }) } - pub fn save(&self, mut writter: &mut W) -> Result { + pub fn save(&self, mut writer: &mut W) -> Result { let mut sprites = vec![]; let mut signature = format!( "# BEGIN DMI\nversion = {}\n\twidth = {}\n\theight = {}\n", @@ -598,7 +560,7 @@ impl Icon { new_dmi.chunk_ztxt = Some(new_ztxt); - new_dmi.save(&mut writter) + new_dmi.save(&mut writer, true) } } diff --git a/src/lib.rs b/src/lib.rs index c10f8ee..09832fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -230,8 +230,35 @@ impl RawDmi { }) } - pub fn save(&self, mut writter: &mut W) -> Result { - let bytes_written = writter.write(&self.header)?; + /// Calculates the size of a buffer needed to save this DMI with RawDmi::save. + pub fn output_buffer_size(&self, include_ztxt: bool) -> usize { + let mut total_bytes = 45; + if include_ztxt { + if let Some(chunk_ztxt) = &self.chunk_ztxt { + total_bytes += chunk_ztxt.data.keyword.len() + chunk_ztxt.data.compressed_text.len() + 14 + } + } + if let Some(chunk_plte) = &self.chunk_plte { + total_bytes += chunk_plte.data.len() + 12 + } + if let Some(other_chunks) = &self.other_chunks { + for chunk in other_chunks { + total_bytes += chunk.data.len() + 12 + } + } + for idat in &self.chunks_idat { + total_bytes += idat.data.len() + 12; + } + + total_bytes + } + + pub fn save( + &self, + mut writer: &mut W, + include_ztxt: bool, + ) -> Result { + let bytes_written = writer.write(&self.header)?; let mut total_bytes_written = bytes_written; if bytes_written < 8 { return Err(error::DmiError::Generic(format!( @@ -239,7 +266,7 @@ impl RawDmi { ))); }; - let bytes_written = self.chunk_ihdr.save(&mut writter)?; + let bytes_written = self.chunk_ihdr.save(&mut writer)?; total_bytes_written += bytes_written; if bytes_written < u32::from_be_bytes(self.chunk_ihdr.data_length) as usize + 12 { return Err(error::DmiError::Generic(format!( @@ -247,18 +274,20 @@ impl RawDmi { ))); }; - if let Some(chunk_ztxt) = &self.chunk_ztxt { - let bytes_written = chunk_ztxt.save(&mut writter)?; - total_bytes_written += bytes_written; - if bytes_written < u32::from_be_bytes(chunk_ztxt.data_length) as usize + 12 { - return Err(error::DmiError::Generic(format!( - "Failed to save DMI. Buffer unable to hold the data, only {total_bytes_written} bytes written." - ))); + if include_ztxt { + if let Some(chunk_ztxt) = &self.chunk_ztxt { + let bytes_written = chunk_ztxt.save(&mut writer)?; + total_bytes_written += bytes_written; + if bytes_written < u32::from_be_bytes(chunk_ztxt.data_length) as usize + 12 { + return Err(error::DmiError::Generic(format!( + "Failed to save DMI. Buffer unable to hold the data, only {total_bytes_written} bytes written." + ))); + }; }; - }; + } if let Some(chunk_plte) = &self.chunk_plte { - let bytes_written = chunk_plte.save(&mut writter)?; + let bytes_written = chunk_plte.save(&mut writer)?; total_bytes_written += bytes_written; if bytes_written < u32::from_be_bytes(chunk_plte.data_length) as usize + 12 { return Err(error::DmiError::Generic(format!( @@ -269,7 +298,7 @@ impl RawDmi { if let Some(other_chunks) = &self.other_chunks { for chunk in other_chunks { - let bytes_written = chunk.save(&mut writter)?; + let bytes_written = chunk.save(&mut writer)?; total_bytes_written += bytes_written; if bytes_written < u32::from_be_bytes(chunk.data_length) as usize + 12 { return Err(error::DmiError::Generic(format!( @@ -280,7 +309,7 @@ impl RawDmi { } for chunk in &self.chunks_idat { - let bytes_written = chunk.save(&mut writter)?; + let bytes_written = chunk.save(&mut writer)?; total_bytes_written += bytes_written; if bytes_written < u32::from_be_bytes(chunk.data_length) as usize + 12 { return Err(error::DmiError::Generic(format!( @@ -289,7 +318,7 @@ impl RawDmi { }; } - let bytes_written = self.chunk_iend.save(&mut writter)?; + let bytes_written = self.chunk_iend.save(&mut writer)?; total_bytes_written += bytes_written; if bytes_written < u32::from_be_bytes(self.chunk_iend.data_length) as usize + 12 { return Err(error::DmiError::Generic(format!( From fee48e55afc6e463ef6b55f07b2e0ae520818a27 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Sat, 2 Aug 2025 00:56:38 -0700 Subject: [PATCH 23/25] BEGGONNE WRITTER --- src/chunk.rs | 10 +++++----- src/iend.rs | 8 ++++---- src/ztxt.rs | 20 ++++++++++---------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/chunk.rs b/src/chunk.rs index 8efe04c..65aafe3 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -74,8 +74,8 @@ impl RawGenericChunk { }) } - pub fn save(&self, writter: &mut W) -> Result { - let bytes_written = writter.write(&self.data_length)?; + pub fn save(&self, writer: &mut W) -> Result { + let bytes_written = writer.write(&self.data_length)?; let mut total_bytes_written = bytes_written; if bytes_written < self.data_length.len() { return Err(error::DmiError::Generic(format!( @@ -83,7 +83,7 @@ impl RawGenericChunk { ))); }; - let bytes_written = writter.write(&self.chunk_type)?; + let bytes_written = writer.write(&self.chunk_type)?; total_bytes_written += bytes_written; if bytes_written < self.chunk_type.len() { return Err(error::DmiError::Generic(format!( @@ -91,7 +91,7 @@ impl RawGenericChunk { ))); }; - let bytes_written = writter.write(&self.data)?; + let bytes_written = writer.write(&self.data)?; total_bytes_written += bytes_written; if bytes_written < self.data.len() { return Err(error::DmiError::Generic(format!( @@ -99,7 +99,7 @@ impl RawGenericChunk { ))); }; - let bytes_written = writter.write(&self.crc)?; + let bytes_written = writer.write(&self.crc)?; total_bytes_written += bytes_written; if bytes_written < self.crc.len() { return Err(error::DmiError::Generic(format!( diff --git a/src/iend.rs b/src/iend.rs index a245369..0b45fad 100644 --- a/src/iend.rs +++ b/src/iend.rs @@ -79,8 +79,8 @@ impl RawIendChunk { Ok(default_iend_chunk) } - pub fn save(&self, writter: &mut W) -> Result { - let bytes_written = writter.write(&self.data_length)?; + pub fn save(&self, writer: &mut W) -> Result { + let bytes_written = writer.write(&self.data_length)?; let mut total_bytes_written = bytes_written; if bytes_written < self.data_length.len() { return Err(error::DmiError::Generic(format!( @@ -88,7 +88,7 @@ impl RawIendChunk { ))); }; - let bytes_written = writter.write(&self.chunk_type)?; + let bytes_written = writer.write(&self.chunk_type)?; total_bytes_written += bytes_written; if bytes_written < self.chunk_type.len() { return Err(error::DmiError::Generic(format!( @@ -96,7 +96,7 @@ impl RawIendChunk { ))); }; - let bytes_written = writter.write(&self.crc)?; + let bytes_written = writer.write(&self.crc)?; total_bytes_written += bytes_written; if bytes_written < self.crc.len() { return Err(error::DmiError::Generic(format!( diff --git a/src/ztxt.rs b/src/ztxt.rs index 9196770..63303a4 100644 --- a/src/ztxt.rs +++ b/src/ztxt.rs @@ -85,8 +85,8 @@ impl RawZtxtChunk { }) } - pub fn save(&self, writter: &mut W) -> Result { - let bytes_written = writter.write(&self.data_length)?; + pub fn save(&self, writer: &mut W) -> Result { + let bytes_written = writer.write(&self.data_length)?; let mut total_bytes_written = bytes_written; if bytes_written < self.data_length.len() { return Err(error::DmiError::Generic(format!( @@ -94,7 +94,7 @@ impl RawZtxtChunk { ))); }; - let bytes_written = writter.write(&self.chunk_type)?; + let bytes_written = writer.write(&self.chunk_type)?; total_bytes_written += bytes_written; if bytes_written < self.chunk_type.len() { return Err(error::DmiError::Generic(format!( @@ -102,7 +102,7 @@ impl RawZtxtChunk { ))); }; - let bytes_written = self.data.save(&mut *writter)?; + let bytes_written = self.data.save(&mut *writer)?; total_bytes_written += bytes_written; if bytes_written < u32::from_be_bytes(self.data_length) as usize { return Err(error::DmiError::Generic(format!( @@ -110,7 +110,7 @@ impl RawZtxtChunk { ))); }; - let bytes_written = writter.write(&self.crc)?; + let bytes_written = writer.write(&self.crc)?; total_bytes_written += bytes_written; if bytes_written < self.crc.len() { return Err(error::DmiError::Generic(format!( @@ -238,8 +238,8 @@ impl RawZtxtData { }) } - pub fn save(&self, writter: &mut W) -> Result { - let bytes_written = writter.write(&self.keyword)?; + pub fn save(&self, writer: &mut W) -> Result { + let bytes_written = writer.write(&self.keyword)?; let mut total_bytes_written = bytes_written; if bytes_written < self.keyword.len() { return Err(error::DmiError::Generic(format!( @@ -247,7 +247,7 @@ impl RawZtxtData { ))); }; - let bytes_written = writter.write(&[self.null_separator])?; + let bytes_written = writer.write(&[self.null_separator])?; total_bytes_written += bytes_written; if bytes_written < 1 { return Err(error::DmiError::Generic(format!( @@ -255,7 +255,7 @@ impl RawZtxtData { ))); }; - let bytes_written = writter.write(&[self.compression_method])?; + let bytes_written = writer.write(&[self.compression_method])?; total_bytes_written += bytes_written; if bytes_written < 1 { return Err(error::DmiError::Generic(format!( @@ -263,7 +263,7 @@ impl RawZtxtData { ))); }; - let bytes_written = writter.write(&self.compressed_text)?; + let bytes_written = writer.write(&self.compressed_text)?; total_bytes_written += bytes_written; if bytes_written < self.compressed_text.len() { return Err(error::DmiError::Generic(format!( From 031fb79ac0ba100ebfb8e9671cc5cc231c75f39c Mon Sep 17 00:00:00 2001 From: itsmeow Date: Sat, 2 Aug 2025 01:59:25 -0700 Subject: [PATCH 24/25] Minor load_meta optimizations and readability improvement --- src/lib.rs | 93 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 34 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 09832fa..01c0864 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,6 +30,25 @@ pub struct RawDmiMetadata { pub chunk_ztxt: ztxt::RawZtxtChunk, } +fn ensure_buffered_bytes( + buffered_bytes: &mut Cursor>, + source_reader: &mut R, + source_amount_read: &mut usize, + additional_length_required: usize, +) -> Result<(), error::DmiError> { + let original_position = buffered_bytes.position(); + if original_position + additional_length_required as u64 > *source_amount_read as u64 { + let mut new_bytes = vec![0u8; additional_length_required]; + source_reader.read_exact(&mut new_bytes)?; + // Append all the new bytes to our cursor and go back to our old spot + buffered_bytes.seek_relative(*source_amount_read as i64 - original_position as i64)?; + buffered_bytes.write_all(&new_bytes)?; + *source_amount_read += new_bytes.len(); + buffered_bytes.seek_relative(original_position as i64 - *source_amount_read as i64)?; + } + Ok(()) +} + impl RawDmi { pub fn new() -> RawDmi { RawDmi { @@ -163,58 +182,64 @@ impl RawDmi { let mut chunk_ztxt = None; loop { - // Read len - let mut chunk_len_be: [u8; 4] = [0u8; 4]; - buffered_dmi_bytes.read_exact(&mut chunk_len_be)?; - let chunk_len = u32::from_be_bytes(chunk_len_be) as usize; - - // Create vec for full chunk data - let mut chunk_full: Vec = Vec::with_capacity(chunk_len + 12); - chunk_full.extend_from_slice(&chunk_len_be); + // Read len[u8; 4] + header[u8; 4] + let mut chunk_header_full: [u8; 8] = [0u8; 8]; + buffered_dmi_bytes.read_exact(&mut chunk_header_full)?; + + let chunk_len = u32::from_be_bytes([ + chunk_header_full[0], + chunk_header_full[1], + chunk_header_full[2], + chunk_header_full[3], + ]) as usize; - // Read header into full chunk data - let mut chunk_header = [0u8; 4]; - buffered_dmi_bytes.read_exact(&mut chunk_header)?; - chunk_full.extend_from_slice(&chunk_header); + // Read header + let chunk_header_type = &chunk_header_full[4..8]; // If we encounter IDAT or IEND we can just break because the zTXt header aint happening - if &chunk_header == b"IDAT" || &chunk_header == b"IEND" { + if chunk_header_type == b"IDAT" || chunk_header_type == b"IEND" { break; } - // We will overread the file's buffer. - let original_position = buffered_dmi_bytes.position(); - if original_position + chunk_len as u64 + 12 > dmi_bytes_read as u64 { + // Skip non-zTXt chunks + if chunk_header_type != b"zTXt" { + // We will overread the file's buffer on our seek. // Read the remainder of the chunk + 4 bytes for CRC + 8 bytes for the next header. // There will always be a next header because IEND headers break before this check. - let mut new_dmi_bytes = vec![0u8; chunk_len + 12]; - reader.read_exact(&mut new_dmi_bytes)?; - // Append all the new bytes to our cursor and go back to our old spot - buffered_dmi_bytes.seek_relative(dmi_bytes_read as i64 - original_position as i64)?; - buffered_dmi_bytes.write_all(&new_dmi_bytes)?; - dmi_bytes_read += new_dmi_bytes.len(); - buffered_dmi_bytes.seek_relative(original_position as i64 - dmi_bytes_read as i64)?; - } - - // Skip non-zTXt chunks - if &chunk_header != b"zTXt" { + ensure_buffered_bytes( + &mut buffered_dmi_bytes, + &mut reader, + &mut dmi_bytes_read, + chunk_len + 12, + )?; buffered_dmi_bytes.seek_relative((chunk_len + 4) as i64)?; continue; } - // Read actual chunk data and append - let mut chunk_data = vec![0; chunk_len]; + // Make sure we have enough bytes to finish the zTXt chunk and nothing else. + ensure_buffered_bytes( + &mut buffered_dmi_bytes, + &mut reader, + &mut dmi_bytes_read, + chunk_len + 4, + )?; + + // Create vec for full chunk data + let mut chunk_full: Vec = Vec::with_capacity(chunk_len + 12); + + // Fill it up with the data we already have + chunk_full.extend_from_slice(&chunk_header_full); + + // Read actual chunk data + CRC and append + let mut chunk_data = vec![0; chunk_len + 4]; buffered_dmi_bytes.read_exact(&mut chunk_data)?; chunk_full.extend_from_slice(&chunk_data); - // Read CRC into full chunk data - let mut chunk_crc = [0u8; 4]; - buffered_dmi_bytes.read_exact(&mut chunk_crc)?; - chunk_full.extend_from_slice(&chunk_crc); - let raw_chunk = chunk::RawGenericChunk::load(&mut &*chunk_full)?; chunk_ztxt = Some(ztxt::RawZtxtChunk::try_from(raw_chunk)?); + // We got all we need, let's gooo + break; } if chunk_ztxt.is_none() { From 32762764effed62c593f8a99a6d212b9af58f7f5 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Wed, 13 Aug 2025 17:17:40 -0700 Subject: [PATCH 25/25] Remove unused bench --- tests/bench_dmi_load.rs | 72 ----------------------------------------- 1 file changed, 72 deletions(-) delete mode 100644 tests/bench_dmi_load.rs diff --git a/tests/bench_dmi_load.rs b/tests/bench_dmi_load.rs deleted file mode 100644 index 47dfc9b..0000000 --- a/tests/bench_dmi_load.rs +++ /dev/null @@ -1,72 +0,0 @@ -/* -use dmi::icon::Icon; -use std::fs::File; -use std::path::Path; -use std::time::Instant; - -const ICONS_FOLDER: &'static str = "UPDATEME"; - -#[test] -fn bench_dmi_load() { - let icons_folder_path = Path::new(ICONS_FOLDER); - - println!("Icon::load_meta bench\n---"); - - let mut num_calls = 0; - let mut microsec_calls = 0; - for _ in 0..25 { - recurse_process( - icons_folder_path, - &mut num_calls, - &mut microsec_calls, - false, - ); - } - println!("Num calls: {num_calls}"); - println!("Total Call Duration (μs): {microsec_calls}"); - let mtpc = microsec_calls / num_calls as u128; - println!("MTPC (μs): {mtpc}"); - - println!("Icon::load bench\n---"); - - num_calls = 0; - microsec_calls = 0; - // this is seriously slow. 2 iterations max or you'll be waiting all day - for _ in 0..1 { - recurse_process( - icons_folder_path, - &mut num_calls, - &mut microsec_calls, - true, - ); - } - println!("Num calls: {num_calls}"); - println!("Total Call Duration (μs): {microsec_calls}"); - let mtpc = microsec_calls / num_calls as u128; - println!("MTPC (μs): {mtpc}"); -} - -fn recurse_process(path: &Path, num_calls: &mut u32, microsec_calls: &mut u128, load_images: bool) { - if path.is_dir() { - if let Ok(entries) = std::fs::read_dir(path) { - for entry in entries.flatten() { - let path = entry.path(); - recurse_process(&path, num_calls, microsec_calls, load_images); - } - } - } else if let Some(ext) = path.extension().and_then(|e| e.to_str()) { - if ext != "dmi" { - return; - } - let load_file = File::open(path).unwrap_or_else(|_| panic!("No dmi: {path:?}")); - let start = Instant::now(); - if load_images { - let _ = Icon::load(&load_file).expect("Unable to dmi metadata"); - } else { - let _ = Icon::load_meta(&load_file).expect("Unable to dmi metadata"); - } - *microsec_calls += start.elapsed().as_micros(); - *num_calls += 1; - } -} -*/