diff options
-rw-r--r-- | .rustfmt.toml (renamed from squash/src/.rustfmt.toml) | 0 | ||||
-rw-r--r-- | squash/.rustfmt.toml | 1 | ||||
-rw-r--r-- | squash/src/main.rs | 38 | ||||
-rw-r--r-- | src/difference.rs | 4 | ||||
-rw-r--r-- | src/lib.rs | 519 |
5 files changed, 289 insertions, 273 deletions
diff --git a/squash/src/.rustfmt.toml b/.rustfmt.toml index 218e203..218e203 100644 --- a/squash/src/.rustfmt.toml +++ b/.rustfmt.toml diff --git a/squash/.rustfmt.toml b/squash/.rustfmt.toml new file mode 100644 index 0000000..218e203 --- /dev/null +++ b/squash/.rustfmt.toml @@ -0,0 +1 @@ +hard_tabs = true diff --git a/squash/src/main.rs b/squash/src/main.rs index fe7f310..80c7cb0 100644 --- a/squash/src/main.rs +++ b/squash/src/main.rs @@ -51,7 +51,7 @@ fn main() -> Result<(), anyhow::Error> { } }; - let squasher = Squasher::new(color_count, &image.data); + let mut squasher = Squasher::new(color_count, &image.data); let size = squasher.map_over(&mut image.data); image.data.resize(size, 0); @@ -85,28 +85,36 @@ fn get_png<P: AsRef<Utf8Path>>(path: P) -> Result<Image, anyhow::Error> { let decoder = Decoder::new(File::open(path.as_ref())?); let mut reader = decoder.read_info()?; - let mut buf = vec![0; reader.output_buffer_size()]; - let info = reader.next_frame(&mut buf)?; - let data = &buf[..info.buffer_size()]; - - println!( - "{}x{} * 3 = {} | out={}, bs={}", - info.width, - info.height, - info.width as usize * info.height as usize * 3, - buf.len(), - info.buffer_size() - ); + let mut data = vec![0; reader.output_buffer_size()]; + let info = reader.next_frame(&mut data)?; + data.resize(info.buffer_size(), 0); let colors = info.color_type; match colors { - ColorType::Grayscale | ColorType::GrayscaleAlpha | ColorType::Indexed | ColorType::Rgba => { + ColorType::Grayscale | ColorType::GrayscaleAlpha | ColorType::Indexed => { bail!("colortype {colors:?} not supported") } + ColorType::Rgba => { + let pixels = info.width as usize * info.height as usize; + + // the first RGB is fine, we don't need to touch it + for idx in 1..pixels { + data[idx * 3] = data[idx * 4]; + data[idx * 3 + 1] = data[idx * 4 + 1]; + data[idx * 3 + 2] = data[idx * 4 + 2]; + } + data.resize(pixels * 3, 0); + + Ok(Image { + width: info.width as usize, + height: info.height as usize, + data, + }) + } ColorType::Rgb => Ok(Image { width: info.width as usize, height: info.height as usize, - data: data.to_vec(), + data, }), } } diff --git a/src/difference.rs b/src/difference.rs index 56e40a0..42c4c4f 100644 --- a/src/difference.rs +++ b/src/difference.rs @@ -1,6 +1,8 @@ //! A set of difference functions you can use with [SquasherBuilder::difference] -use rgb::RGB8; +// rexport this so people don't need to add the rgb crate to their project. this +// also helps avoid crate version mismatch +pub use rgb::RGB8; /// A naïve comparison just summing the channel differences /// I.E. `|a.red - b.red| + |a.green - b.green| + |a.blue - b.blue|` diff --git a/src/lib.rs b/src/lib.rs index 8a709e9..d4b852b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,283 +6,288 @@ pub mod difference; type DiffFn = dyn Fn(&RGB8, &RGB8) -> f32; pub struct SquasherBuilder<T> { - max_colours: T, - difference_fn: Box<DiffFn>, - tolerance: f32, + max_colours: T, + difference_fn: Box<DiffFn>, + tolerance: f32, } impl<T: Count> SquasherBuilder<T> { - pub fn new() -> Self { - Self::default() - } - - /// The max number of colors selected for the palette, minus one. - /// - /// `max_colors(255)` will attempt to make a 256 color palette - pub fn max_colors(mut self, max_minus_one: T) -> SquasherBuilder<T> { - self.max_colours = max_minus_one; - self - } - - /// The function to use to compare colours. - /// - /// see the [difference] module for functions included with the crate. - pub fn difference(mut self, difference: &'static DiffFn) -> SquasherBuilder<T> { - self.difference_fn = Box::new(difference); - self - } - - /// Percent colours have to differ by to be included into the palette. - /// between and including 0.0 to 100.0 - pub fn tolerance(mut self, percent: f32) -> SquasherBuilder<T> { - self.tolerance = percent; - self - } - - pub fn build(self, image: &[u8]) -> Squasher<T> { - let mut squasher = - Squasher::from_parts(self.max_colours, self.difference_fn, self.tolerance); - squasher.recolor(image); - - squasher - } + pub fn new() -> Self { + Self::default() + } + + /// The max number of colors selected for the palette, minus one. + /// + /// `max_colors(255)` will attempt to make a 256 color palette + pub fn max_colors(mut self, max_minus_one: T) -> SquasherBuilder<T> { + self.max_colours = max_minus_one; + self + } + + /// The function to use to compare colours. + /// + /// see the [difference] module for functions included with the crate. + pub fn difference(mut self, difference: &'static DiffFn) -> SquasherBuilder<T> { + self.difference_fn = Box::new(difference); + self + } + + /// Percent colours have to differ by to be included into the palette. + /// between and including 0.0 to 100.0 + pub fn tolerance(mut self, percent: f32) -> SquasherBuilder<T> { + self.tolerance = percent; + self + } + + pub fn build(self, image: &[u8]) -> Squasher<T> { + let mut squasher = + Squasher::from_parts(self.max_colours, self.difference_fn, self.tolerance); + squasher.recolor(image); + + squasher + } } impl<T: Count> Default for SquasherBuilder<T> { - fn default() -> Self { - Self { - max_colours: T::from_usize(255), - difference_fn: Box::new(difference::rgb_difference), - tolerance: 2.0, - } - } + fn default() -> Self { + Self { + max_colours: T::from_usize(255), + difference_fn: Box::new(difference::rgb_difference), + tolerance: 2.0, + } + } } pub struct Squasher<T> { - // one less than the max colours as you can't have a zero colour image. - max_colours_min1: T, - palette: Vec<RGB8>, - map: Vec<T>, - difference_fn: Box<DiffFn>, - tolerance_percent: f32, + // one less than the max colours as you can't have a zero colour image. + max_colours_min1: T, + palette: Vec<RGB8>, + map: Vec<T>, + difference_fn: Box<DiffFn>, + tolerance_percent: f32, } impl<T: Count> Squasher<T> { - /// Creates a new squasher and allocates a new color map. A color map - /// contains every 24-bit color and ends up with an amount of memory - /// equal to `16MB * std::mem::size_of(T)`. - pub fn new(max_colors_minus_one: T, buffer: &[u8]) -> Self { - let mut this = Self::from_parts( - max_colors_minus_one, - Box::new(difference::rgb_difference), - 2.0, - ); - this.recolor(buffer); - - this - } - - pub fn builder() -> SquasherBuilder<T> { - SquasherBuilder::new() - } - - pub fn set_tolerance(&mut self, percent: f32) { - self.tolerance_percent = percent; - } - - /// Create a new palette from the colours in the given image. - pub fn recolor(&mut self, image: &[u8]) { - let sorted = Self::unique_and_sort(image); - let selected = self.select_colors(sorted); - self.palette = selected; - } - - /// Create a Squasher from parts. Noteably, this leave your palette empty - fn from_parts(max_colours_min1: T, difference_fn: Box<DiffFn>, tolerance: f32) -> Self { - Self { - max_colours_min1, - palette: vec![], - map: vec![T::zero(); 256 * 256 * 256], - difference_fn, - tolerance_percent: tolerance, - } - } - - /// Take an RGB image buffer and an output buffer. The function will fill - /// the output buffer with indexes into the Palette. The output buffer should - /// be a third of the size of the image buffer. - pub fn map(&mut self, image: &[u8], buffer: &mut [T]) { - if buffer.len() * 3 < image.len() { - panic!("outout buffer too small to fit indexed image"); - } - - // We have to map the colours of this image now because it might contain - // colours not present in the first image. - let sorted = Self::unique_and_sort(image); - self.map_selected(&sorted); - - for (idx, color) in image.chunks(3).enumerate() { - let index = self.map[color_index(&RGB8::new(color[0], color[1], color[2]))]; - - buffer[idx] = index; - } - } - - /// Like [Squasher::map] but it doesn't recount the input image. This will - /// cause colors the Squasher hasn't seen before to come out as index 0 which - /// may be incorrect. - //TODO: gen- Better name? - pub fn map_unsafe(&self, image: &[u8], buffer: &mut [T]) { - if buffer.len() * 3 < image.len() { - panic!("outout buffer too small to fit indexed image"); - } - - for (idx, color) in image.chunks(3).enumerate() { - let index = self.map[color_index(&RGB8::new(color[0], color[1], color[2]))]; - - buffer[idx] = index; - } - } - - /// Retrieve the palette this squasher is working from - pub fn palette(&self) -> &[RGB8] { - &self.palette - } - - /// Retrieve the palette as bytes - pub fn palette_bytes(&self) -> Vec<u8> { - self.palette - .clone() - .into_iter() - .flat_map(|rgb| [rgb.r, rgb.g, rgb.b].into_iter()) - .collect() - } - - /// Takes an image buffer of RGB data and fill the color map - fn unique_and_sort(buffer: &[u8]) -> Vec<RGB8> { - let mut colors: HashMap<RGB8, usize> = HashMap::default(); - - //count pixels - for pixel in buffer.chunks(3) { - let rgb = RGB8::new(pixel[0], pixel[1], pixel[2]); - - match colors.get_mut(&rgb) { - None => { - colors.insert(rgb, 1); - } - Some(n) => *n += 1, - } - } - - Self::sort(colors) - } - - fn sort(map: HashMap<RGB8, usize>) -> Vec<RGB8> { - let mut sorted: Vec<(RGB8, usize)> = map.into_iter().collect(); - sorted.sort_by(|(colour1, freq1), (colour2, freq2)| { - freq2 - .cmp(freq1) - .then(colour2.r.cmp(&colour1.r)) - .then(colour2.g.cmp(&colour1.g)) - .then(colour2.b.cmp(&colour1.b)) - }); - - sorted.into_iter().map(|(color, _count)| color).collect() - } - - /// Pick the colors in the palette from a Vec of colors sorted by number - /// of times they occur, high to low. - fn select_colors(&self, sorted: Vec<RGB8>) -> Vec<RGB8> { - // I made these numbers up - #[allow(non_snake_case)] - //let RGB_TOLERANCE: f32 = 0.01 * 765.0; - //let RGB_TOLERANCE: f32 = 36.0; - let tolerance = (self.tolerance_percent / 100.0) * 765.0; - let max_colours = self.max_colours_min1.as_usize() + 1; - let mut selected_colors: Vec<RGB8> = Vec::with_capacity(max_colours); - - for sorted_color in sorted { - if max_colours > selected_colors.len() { - break; - } else if selected_colors - .iter() - .all(|color| (self.difference_fn)(&sorted_color, color) > tolerance) - { - selected_colors.push(sorted_color); - } - } - - selected_colors - } - - /// Pick the closest colour in the palette for each unique color in the image - fn map_selected(&mut self, sorted: &[RGB8]) { - for colour in sorted { - let mut min_diff = f32::MAX; - let mut min_index = usize::MAX; - - for (index, selected) in self.palette.iter().enumerate() { - let diff = (self.difference_fn)(colour, selected); - - if diff.max(0.0) < min_diff { - min_diff = diff; - min_index = index; - } - } - - self.map[color_index(colour)] = T::from_usize(min_index); - } - } + /// Creates a new squasher and allocates a new color map. A color map + /// contains every 24-bit color and ends up with an amount of memory + /// equal to `16MB * std::mem::size_of(T)`. + pub fn new(max_colors_minus_one: T, buffer: &[u8]) -> Self { + let mut this = Self::from_parts( + max_colors_minus_one, + Box::new(difference::rgb_difference), + 2.0, + ); + this.recolor(buffer); + + this + } + + pub fn builder() -> SquasherBuilder<T> { + SquasherBuilder::new() + } + + pub fn set_tolerance(&mut self, percent: f32) { + self.tolerance_percent = percent; + } + + /// Create a new palette from the colours in the given image. + pub fn recolor(&mut self, image: &[u8]) { + let sorted = Self::unique_and_sort(image); + let selected = self.select_colors(sorted); + self.palette = selected; + } + + /// Create a Squasher from parts. Noteably, this leave your palette empty + fn from_parts(max_colours_min1: T, difference_fn: Box<DiffFn>, tolerance: f32) -> Self { + Self { + max_colours_min1, + palette: vec![], + map: vec![T::zero(); 256 * 256 * 256], + difference_fn, + tolerance_percent: tolerance, + } + } + + /// Take an RGB image buffer and an output buffer. The function will fill + /// the output buffer with indexes into the Palette. The output buffer should + /// be a third of the size of the image buffer. + pub fn map(&mut self, image: &[u8], buffer: &mut [T]) { + if buffer.len() * 3 < image.len() { + panic!("outout buffer too small to fit indexed image"); + } + + // We have to map the colours of this image now because it might contain + // colours not present in the first image. + let sorted = Self::unique_and_sort(image); + self.map_selected(&sorted); + + for (idx, color) in image.chunks(3).enumerate() { + let index = self.map[color_index(&RGB8::new(color[0], color[1], color[2]))]; + + buffer[idx] = index; + } + } + + /// Like [Squasher::map] but it doesn't recount the input image. This will + /// cause colors the Squasher hasn't seen before to come out as index 0 which + /// may be incorrect. + //TODO: gen- Better name? + pub fn map_unsafe(&self, image: &[u8], buffer: &mut [T]) { + if buffer.len() * 3 < image.len() { + panic!("outout buffer too small to fit indexed image"); + } + + for (idx, color) in image.chunks(3).enumerate() { + let index = self.map[color_index(&RGB8::new(color[0], color[1], color[2]))]; + + buffer[idx] = index; + } + } + + /// Retrieve the palette this squasher is working from + pub fn palette(&self) -> &[RGB8] { + &self.palette + } + + /// Retrieve the palette as bytes + pub fn palette_bytes(&self) -> Vec<u8> { + self.palette + .clone() + .into_iter() + .flat_map(|rgb| [rgb.r, rgb.g, rgb.b].into_iter()) + .collect() + } + + /// Takes an image buffer of RGB data and fill the color map + fn unique_and_sort(buffer: &[u8]) -> Vec<RGB8> { + let mut colors: HashMap<RGB8, usize> = HashMap::default(); + + //count pixels + for pixel in buffer.chunks(3) { + let rgb = RGB8::new(pixel[0], pixel[1], pixel[2]); + + match colors.get_mut(&rgb) { + None => { + colors.insert(rgb, 1); + } + Some(n) => *n += 1, + } + } + + Self::sort(colors) + } + + fn sort(map: HashMap<RGB8, usize>) -> Vec<RGB8> { + let mut sorted: Vec<(RGB8, usize)> = map.into_iter().collect(); + sorted.sort_by(|(colour1, freq1), (colour2, freq2)| { + freq2 + .cmp(freq1) + .then(colour2.r.cmp(&colour1.r)) + .then(colour2.g.cmp(&colour1.g)) + .then(colour2.b.cmp(&colour1.b)) + }); + + sorted.into_iter().map(|(color, _count)| color).collect() + } + + /// Pick the colors in the palette from a Vec of colors sorted by number + /// of times they occur, high to low. + fn select_colors(&self, sorted: Vec<RGB8>) -> Vec<RGB8> { + // I made these numbers up + #[allow(non_snake_case)] + //let RGB_TOLERANCE: f32 = 0.01 * 765.0; + //let RGB_TOLERANCE: f32 = 36.0; + let tolerance = (self.tolerance_percent / 100.0) * 765.0; + let max_colours = self.max_colours_min1.as_usize() + 1; + let mut selected_colors: Vec<RGB8> = Vec::with_capacity(max_colours); + + for sorted_color in sorted { + if max_colours <= selected_colors.len() { + break; + } else if selected_colors + .iter() + .all(|color| (self.difference_fn)(&sorted_color, color) > tolerance) + { + selected_colors.push(sorted_color); + } + } + + selected_colors + } + + /// Pick the closest colour in the palette for each unique color in the image + fn map_selected(&mut self, sorted: &[RGB8]) { + for colour in sorted { + let mut min_diff = f32::MAX; + let mut min_index = usize::MAX; + + for (index, selected) in self.palette.iter().enumerate() { + let diff = (self.difference_fn)(colour, selected); + + if diff.max(0.0) < min_diff { + min_diff = diff; + min_index = index; + } + } + + self.map[color_index(colour)] = T::from_usize(min_index); + } + } } impl Squasher<u8> { - /// Takes an RGB image buffer and writes the indicies to the first third of - /// that buffer. The buffer is not resized. - /// - /// # Returns - /// The new size of the image - pub fn map_over(&self, image: &mut [u8]) -> usize { - for idx in 0..(image.len() / 3) { - let rgb_idx = idx * 3; - let color = RGB8::new(image[rgb_idx], image[rgb_idx + 1], image[rgb_idx + 2]); - let color_index = self.map[color_index(&color)]; - - image[idx] = color_index; - } - - image.len() / 3 - } + /// Takes an RGB image buffer and writes the indicies to the first third of + /// that buffer. The buffer is not resized. + /// + /// # Returns + /// The new size of the image + pub fn map_over(&mut self, image: &mut [u8]) -> usize { + // We have to map the colours of this image now because it might contain + // colours not present in the first image. + let sorted = Self::unique_and_sort(image); + self.map_selected(&sorted); + + for idx in 0..(image.len() / 3) { + let rgb_idx = idx * 3; + let color = RGB8::new(image[rgb_idx], image[rgb_idx + 1], image[rgb_idx + 2]); + let color_index = self.map[color_index(&color)]; + + image[idx] = color_index; + } + + image.len() / 3 + } } pub trait Count: Copy + Clone { - fn zero() -> Self; - fn as_usize(&self) -> usize; - fn from_usize(from: usize) -> Self; - fn le(&self, rhs: &usize) -> bool; + fn zero() -> Self; + fn as_usize(&self) -> usize; + fn from_usize(from: usize) -> Self; + fn le(&self, rhs: &usize) -> bool; } macro_rules! count_impl { - ($kind:ty) => { - impl Count for $kind { - fn zero() -> Self { - 0 - } - - fn as_usize(&self) -> usize { - *self as usize - } - - #[inline(always)] - fn from_usize(from: usize) -> Self { - from as Self - } - - #[inline(always)] - fn le(&self, rhs: &usize) -> bool { - *self as usize <= *rhs - } - } - }; + ($kind:ty) => { + impl Count for $kind { + fn zero() -> Self { + 0 + } + + fn as_usize(&self) -> usize { + *self as usize + } + + #[inline(always)] + fn from_usize(from: usize) -> Self { + from as Self + } + + #[inline(always)] + fn le(&self, rhs: &usize) -> bool { + *self as usize <= *rhs + } + } + }; } count_impl!(u8); @@ -293,5 +298,5 @@ count_impl!(usize); #[inline(always)] fn color_index(c: &RGB8) -> usize { - c.r as usize * (256 * 256) + c.g as usize * 256 + c.b as usize + c.r as usize * (256 * 256) + c.g as usize * 256 + c.b as usize } |