about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--squash/src/main.rs6
-rw-r--r--src/difference.rs54
-rw-r--r--src/lib.rs144
3 files changed, 134 insertions, 70 deletions
diff --git a/squash/src/main.rs b/squash/src/main.rs
index 5437ea1..8e3a79b 100644
--- a/squash/src/main.rs
+++ b/squash/src/main.rs
@@ -1,5 +1,5 @@
 use cli::DifferenceFn;
-use colorsquash::{Squasher, SquasherBuilder};
+use colorsquash::SquasherBuilder;
 
 use crate::cli::{InType, OutType};
 
@@ -23,8 +23,8 @@ fn main() -> Result<(), anyhow::Error> {
 	}
 
 	builder = match cli.difference {
-		DifferenceFn::Rgb => builder.difference(&colorsquash::difference::rgb_difference),
-		DifferenceFn::Redmean => builder.difference(&colorsquash::difference::redmean_difference),
+		DifferenceFn::Rgb => builder.difference(&colorsquash::difference::rgb),
+		DifferenceFn::Redmean => builder.difference(&colorsquash::difference::redmean),
 	};
 
 	let mut squasher = builder.build(&image.data);
diff --git a/src/difference.rs b/src/difference.rs
index 42c4c4f..b52b34f 100644
--- a/src/difference.rs
+++ b/src/difference.rs
@@ -1,30 +1,56 @@
-//! A set of difference functions you can use with [SquasherBuilder::difference]
+//! A set of difference functions you can use with [SquasherBuilder::difference()]
+//!
+//! # Writing your own difference function
+//! The type you want is `dyn Fn(&RGB8, &RGB8) -> f32`  
+//! (defined as [`DiffFn`])
+//!
+//! The first argument is the color already in the palette and the second is
+//! the color we're checking. These are [RGB8] which is a rexport from the `rgb`
+//! crate.
+//!
+//! The value returned is between 0 and 768, but that's not a hard-rule. If you
+//! return a value out of that range you'll have to adjust the tolerance with
+//! [Squasher::set_tolerance()] or [SquasherBuilder::tolerance].
+//!
+//! The difference functions have the possibility of being called hundreds of
+//! thousands of times; you might want to `#[inline(always)]`
+
+// This is used in the module level documentation just above. Without it we'd
+// have to fully qualify the interlink which is also how it'd be displayed.
+#[allow(unused_imports)]
+use crate::{Squasher, SquasherBuilder};
 
 // rexport this so people don't need to add the rgb crate to their project. this
 // also helps avoid crate version mismatch
+/// rexport from the [`rgb`](https://docs.rs/rgb/0.8.37/rgb/) crate.
 pub use rgb::RGB8;
 
+/// Type definition for difference functions.
+pub type DiffFn = dyn Fn(&RGB8, &RGB8) -> f32;
+
 /// A naïve comparison just summing the channel differences
 /// I.E. `|a.red - b.red| + |a.green - b.green| + |a.blue - b.blue|`
 #[allow(clippy::many_single_char_names)]
 #[inline(always)]
-pub fn rgb_difference(a: &RGB8, b: &RGB8) -> f32 {
-    let absdiff = |a: u8, b: u8| (a as f32 - b as f32).abs();
-    absdiff(a.r, b.r) + absdiff(a.g, b.g) + absdiff(a.b, b.b)
+pub fn rgb(a: &RGB8, b: &RGB8) -> f32 {
+	let absdiff = |a: u8, b: u8| (a as f32 - b as f32).abs();
+	absdiff(a.r, b.r) + absdiff(a.g, b.g) + absdiff(a.b, b.b)
 }
 
 // https://en.wikipedia.org/wiki/Color_difference#sRGB
+/// a slightly more intelligent algorithm that weighs the channels in an attempt
+/// to better align with human color perception.
 #[inline(always)]
-pub fn redmean_difference(a: &RGB8, b: &RGB8) -> f32 {
-    let delta_r = a.r as f32 - b.r as f32;
-    let delta_g = a.g as f32 - b.g as f32;
-    let delta_b = a.b as f32 - b.b as f32;
-    // reasonably sure calling it prime is wrong, but
-    let r_prime = 0.5 * (a.r as f32 + b.r as f32);
+pub fn redmean(a: &RGB8, b: &RGB8) -> f32 {
+	let delta_r = a.r as f32 - b.r as f32;
+	let delta_g = a.g as f32 - b.g as f32;
+	let delta_b = a.b as f32 - b.b as f32;
+	// reasonably sure calling it prime is wrong, but
+	let r_prime = 0.5 * (a.r as f32 + b.r as f32);
 
-    let red_part = (2.0 + (r_prime / 256.0)) * (delta_r * delta_r);
-    let green_part = 4.0 * (delta_g * delta_g);
-    let blue_part = (2.0 + (255.0 - r_prime) / 256.0) * (delta_b * delta_b);
+	let red_part = (2.0 + (r_prime / 256.0)) * (delta_r * delta_r);
+	let green_part = 4.0 * (delta_g * delta_g);
+	let blue_part = (2.0 + (255.0 - r_prime) / 256.0) * (delta_b * delta_b);
 
-    (red_part + green_part + blue_part).sqrt()
+	(red_part + green_part + blue_part).sqrt()
 }
diff --git a/src/lib.rs b/src/lib.rs
index 40a8f28..38f8e1a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,11 +1,11 @@
 #[cfg(kmeans)]
 use kmeans::{KMeans, KMeansConfig};
-use rgb::RGB8;
+use rgb::{ComponentBytes, FromSlice, RGB8};
 use std::collections::HashMap;
 
 pub mod difference;
 
-type DiffFn = dyn Fn(&RGB8, &RGB8) -> f32;
+use difference::DiffFn;
 
 pub struct SquasherBuilder<T> {
 	max_colours: T,
@@ -28,7 +28,8 @@ impl<T: Count> SquasherBuilder<T> {
 
 	/// The function to use to compare colours.
 	///
-	/// see the [difference] module for functions included with the crate.
+	/// see the [difference] module for functions included with the crate and
+	/// information on implementing your own.
 	pub fn difference(mut self, difference: &'static DiffFn) -> SquasherBuilder<T> {
 		self.difference_fn = Box::new(difference);
 		self
@@ -41,7 +42,10 @@ impl<T: Count> SquasherBuilder<T> {
 		self
 	}
 
-	pub fn build(self, image: &[u8]) -> Squasher<T> {
+	pub fn build<'a, Img>(self, image: Img) -> Squasher<T>
+	where
+		Img: Into<ImageData<'a>>,
+	{
 		let mut squasher =
 			Squasher::from_parts(self.max_colours, self.difference_fn, self.tolerance);
 		squasher.recolor(image);
@@ -54,7 +58,7 @@ impl<T: Count> Default for SquasherBuilder<T> {
 	fn default() -> Self {
 		Self {
 			max_colours: T::from_usize(255),
-			difference_fn: Box::new(difference::rgb_difference),
+			difference_fn: Box::new(difference::rgb),
 			tolerance: 1.0,
 		}
 	}
@@ -73,12 +77,11 @@ 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),
-			1.0,
-		);
+	pub fn new<'a, Img>(max_colors_minus_one: T, buffer: Img) -> Self
+	where
+		Img: Into<ImageData<'a>>,
+	{
+		let mut this = Self::from_parts(max_colors_minus_one, Box::new(difference::rgb), 1.0);
 		this.recolor(buffer);
 
 		this
@@ -88,23 +91,35 @@ impl<T: Count> Squasher<T> {
 		SquasherBuilder::new()
 	}
 
+	/// Set the tolerance
 	pub fn set_tolerance(&mut self, percent: f32) {
 		self.tolerance_percent = percent;
 	}
 
 	/// Create a new palette from the colours in the given image.
 	#[cfg(not(kmeans))]
-	pub fn recolor(&mut self, image: &[u8]) {
+	pub fn recolor<'a, Img>(&mut self, image: Img)
+	where
+		Img: Into<ImageData<'a>>,
+	{
 		let sorted = Self::unique_and_sort(image);
 		let selected = self.select_colors(sorted);
 		self.palette = selected;
 	}
 
 	#[cfg(kmeans)]
-	pub fn recolor(&mut self, image: &[u8]) {
+	pub fn recolor<'a, Img>(&mut self, image: Img)
+	where
+		Img: Into<ImageData<'a>>,
+	{
+		let ImageData(rgb) = image.into();
+
 		let kmean = KMeans::new(
-			image.iter().map(|u| *u as f32).collect::<Vec<f32>>(),
-			image.len() / 3,
+			rgb.as_bytes()
+				.iter()
+				.map(|u| *u as f32)
+				.collect::<Vec<f32>>(),
+			rgb.as_bytes().len() / 3,
 			3,
 		);
 		let k = self.max_colours_min1.as_usize() + 1;
@@ -137,43 +152,47 @@ impl<T: Count> Squasher<T> {
 	/// 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");
+	pub fn map<'a, Img>(&mut self, image: Img, buffer: &mut [T])
+	where
+		Img: Into<ImageData<'a>>,
+	{
+		let ImageData(rgb) = image.into();
+
+		if buffer.len() * 3 < rgb.len() {
+			panic!("output 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);
+		let sorted = Self::unique_and_sort(rgb);
 		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;
+		for (idx, color) in rgb.iter().enumerate() {
+			buffer[idx] = self.map[color_index(color)];
 		}
 	}
 
 	/// 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.
+	/// 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");
+	pub fn map_no_recolor<'a, Img>(&self, image: Img, buffer: &mut [T])
+	where
+		Img: Into<ImageData<'a>>,
+	{
+		let ImageData(rgb) = image.into();
+
+		if buffer.len() * 3 < rgb.len() {
+			panic!("output 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;
+		for (idx, color) in rgb.iter().enumerate() {
+			buffer[idx] = self.map[color_index(color)];
 		}
 	}
 
 	#[cfg(feature = "gifed")]
 	pub fn palette_gifed(&self) -> gifed::block::Palette {
-		use rgb::ComponentBytes;
-
 		self.palette.as_slice().as_bytes().try_into().unwrap()
 	}
 
@@ -184,24 +203,22 @@ impl<T: Count> Squasher<T> {
 
 	/// 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()
+		self.palette.as_bytes().to_owned()
 	}
 
 	/// Takes an image buffer of RGB data and fill the color map
-	fn unique_and_sort(buffer: &[u8]) -> Vec<RGB8> {
+	fn unique_and_sort<'a, Img>(buffer: Img) -> Vec<RGB8>
+	where
+		Img: Into<ImageData<'a>>,
+	{
+		let ImageData(rgb) = buffer.into();
 		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) {
+		for px in rgb {
+			match colors.get_mut(px) {
 				None => {
-					colors.insert(rgb, 1);
+					colors.insert(*px, 1);
 				}
 				Some(n) => *n += 1,
 			}
@@ -227,10 +244,6 @@ impl<T: Count> Squasher<T> {
 	/// of times they occur, high to low.
 	#[cfg(not(kmeans))]
 	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);
@@ -238,10 +251,9 @@ impl<T: Count> Squasher<T> {
 		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)
-			{
+			} else if selected_colors.iter().all(|selected_color| {
+				(self.difference_fn)(selected_color, &sorted_color) > tolerance
+			}) {
 				selected_colors.push(sorted_color);
 			}
 		}
@@ -276,9 +288,14 @@ impl Squasher<u8> {
 	/// # Returns
 	/// The new size of the image
 	pub fn map_over(&mut self, image: &mut [u8]) -> usize {
+		// "redundant slicing" here is to drop the mut on the reference because
+		// ImageData doesn't have a From<&mut [u8]> and I don't particularly want
+		// it to
+		#[allow(clippy::redundant_slicing)]
+		let sorted = Self::unique_and_sort(&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 in 0..(image.len() / 3) {
@@ -330,6 +347,27 @@ count_impl!(u32);
 count_impl!(u64);
 count_impl!(usize);
 
+pub struct ImageData<'a>(&'a [RGB8]);
+
+impl<'a> From<&'a Vec<u8>> for ImageData<'a> {
+	fn from(plain: &'a Vec<u8>) -> Self {
+		ImageData(plain.as_rgb())
+	}
+}
+
+impl<'a> From<&'a [u8]> for ImageData<'a> {
+	fn from(plain: &'a [u8]) -> Self {
+		ImageData(plain.as_rgb())
+	}
+}
+
+impl<'a> From<&'a [RGB8]> for ImageData<'a> {
+	fn from(rgb: &'a [RGB8]) -> Self {
+		ImageData(rgb)
+	}
+}
+
+/// Compute the color index into the big-map-of-all-colours.
 #[inline(always)]
 fn color_index(c: &RGB8) -> usize {
 	c.r as usize * (256 * 256) + c.g as usize * 256 + c.b as usize