From 6307c1ad333f58da12a3f639d29d43f72a856b23 Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Thu, 23 Nov 2023 19:38:03 -0800 Subject: [PATCH 1/4] Actually compresses dmis when saving them We've been using the write_to wrapper function, which doesn't allow us to pass in a compression level. This means we've been using the default, which is for some reason the fastest/worst for file size. This commit instead "inlines" the write_to function, and uses the Best compression method (slowest/lowest file size) This strategy reduces a large file I tested (screen_cyborg.dmi) from 82kb to 21kb (byond's output if you're curious is about 26kb) I've also added saving to the test suite, to ensure the path gets walked and to allow for visual inspection if changes are made --- src/icon.rs | 9 ++++++++- tests/dmi_ops.rs | 15 +++++++++++++++ tests/load_dmi.rs | 11 ----------- tests/resources/save_test.dmi | Bin 0 -> 4560 bytes 4 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 tests/dmi_ops.rs delete mode 100644 tests/load_dmi.rs create mode 100644 tests/resources/save_test.dmi diff --git a/src/icon.rs b/src/icon.rs index 6a6d430..c9ef402 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -1,6 +1,8 @@ use crate::{error, ztxt, RawDmi}; +use image::codecs::png; use image::imageops; use image::GenericImageView; +use image::ImageEncoder; use std::collections::HashMap; use std::io::prelude::*; use std::io::Cursor; @@ -351,7 +353,12 @@ impl Icon { } let mut dmi_data = Cursor::new(vec![]); - new_png.write_to(&mut dmi_data, image::ImageOutputFormat::Png)?; + // We're futzing around with pngs directly here so we can use the best possible compression + let bytes = new_png.as_bytes(); + let (width, height) = new_png.dimensions(); + let color = new_png.color(); + let encoder = png::PngEncoder::new_with_quality(&mut dmi_data, png::CompressionType::Best, png::FilterType::Adaptive); + encoder.write_image(bytes, width, height, color)?; let mut new_dmi = RawDmi::load(&dmi_data.into_inner()[..])?; let new_ztxt = ztxt::create_ztxt_chunk(signature.as_bytes())?; diff --git a/tests/dmi_ops.rs b/tests/dmi_ops.rs new file mode 100644 index 0000000..a3a7c13 --- /dev/null +++ b/tests/dmi_ops.rs @@ -0,0 +1,15 @@ +use dmi::icon::Icon; +use std::fs::File; +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/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(&load_file).expect("Unable to load lights dmi"); + 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"); +} diff --git a/tests/load_dmi.rs b/tests/load_dmi.rs deleted file mode 100644 index 2aea1b0..0000000 --- a/tests/load_dmi.rs +++ /dev/null @@ -1,11 +0,0 @@ -use dmi::icon::Icon; -use std::fs::File; -use std::path::PathBuf; - -#[test] -fn load_dmi() { - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push("tests/resources/load_test.dmi"); - let file = File::open(path.as_path()).unwrap_or_else(|_| panic!("No lights dmi: {path:?}")); - let _lights_icon = Icon::load(&file).expect("Unable to load lights dmi"); -} diff --git a/tests/resources/save_test.dmi b/tests/resources/save_test.dmi new file mode 100644 index 0000000000000000000000000000000000000000..62ac583d3f565e3eda2fd2e7e490e2a5d73489f4 GIT binary patch literal 4560 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 literal 0 HcmV?d00001 From d5edae4fc3c3378422d2c508711cbb9e8a2804a5 Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Thu, 23 Nov 2023 19:42:14 -0800 Subject: [PATCH 2/4] Improves our image formatting somewhat We, like byond, arrange our saved DMIs in squares. However unlike byond we currently don't remove extra height that we don't use, which leads to odd looking files. This commit fixes that --- src/icon.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/icon.rs b/src/icon.rs index c9ef402..8a39498 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -337,9 +337,13 @@ impl Icon { signature.push_str("# END DMI\n"); - let max_index = (sprites.len() as f64).sqrt().ceil() as u32; + // We try to make a square png as output + let states_rooted = (sprites.len() as f64).sqrt().ceil(); + // Then if it turns out we would have empty rows, we remove them + let cell_width = states_rooted as u32; + let cell_height = ((sprites.len() as f64) / states_rooted).ceil() as u32; let mut new_png = - image::DynamicImage::new_rgba8(max_index * self.width, max_index * self.height); + image::DynamicImage::new_rgba8(cell_width * self.width, cell_height * self.height); for image in sprites.iter().enumerate() { let index = image.0 as u32; @@ -347,8 +351,8 @@ impl Icon { imageops::replace( &mut new_png, *image, - (self.width * (index % max_index)).into(), - (self.height * (index / max_index)).into(), + (self.width * (index % cell_width)).into(), + (self.height * (index / cell_width)).into(), ); } From e182296dbb38722c67abce0c7156f61adfa74970 Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Thu, 23 Nov 2023 20:26:01 -0800 Subject: [PATCH 3/4] runs rustfmt --- src/icon.rs | 6 +++++- tests/dmi_ops.rs | 7 +++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/icon.rs b/src/icon.rs index 8a39498..66c331b 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -361,7 +361,11 @@ impl Icon { let bytes = new_png.as_bytes(); let (width, height) = new_png.dimensions(); let color = new_png.color(); - let encoder = png::PngEncoder::new_with_quality(&mut dmi_data, png::CompressionType::Best, png::FilterType::Adaptive); + let encoder = png::PngEncoder::new_with_quality( + &mut dmi_data, + png::CompressionType::Best, + png::FilterType::Adaptive, + ); encoder.write_image(bytes, width, height, color)?; let mut new_dmi = RawDmi::load(&dmi_data.into_inner()[..])?; diff --git a/tests/dmi_ops.rs b/tests/dmi_ops.rs index a3a7c13..cc2e2a6 100644 --- a/tests/dmi_ops.rs +++ b/tests/dmi_ops.rs @@ -6,10 +6,13 @@ use std::path::PathBuf; fn load_and_save_dmi() { 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 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"); 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"); + let _written_dmi = lights_icon + .save(&mut write_file) + .expect("Failed to save lights dmi"); } From c432dc60a339c895b87646b373e0fcfca190ad72 Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Thu, 23 Nov 2023 21:13:06 -0800 Subject: [PATCH 4/4] pre-emptively swaps to the default strategy --- src/icon.rs | 2 +- tests/resources/save_test.dmi | Bin 4560 -> 4530 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/icon.rs b/src/icon.rs index 66c331b..17ad63e 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -363,7 +363,7 @@ impl Icon { let color = new_png.color(); let encoder = png::PngEncoder::new_with_quality( &mut dmi_data, - png::CompressionType::Best, + png::CompressionType::Default, png::FilterType::Adaptive, ); encoder.write_image(bytes, width, height, color)?; diff --git a/tests/resources/save_test.dmi b/tests/resources/save_test.dmi index 62ac583d3f565e3eda2fd2e7e490e2a5d73489f4..dd84169e5ff2c48b52a3f2ca3b3c0db33bd5f68a 100644 GIT binary patch literal 4530 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 sWJ&0z(~0wd7YFwJzw&=P2)l=V-N#Q+{XPTi_@% literal 4560 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