Skip to main content

swatchthis/
lib.rs

1//! Colour swatch extraction from images using k-means clustering.
2//!
3//! Supports clustering in both RGB and CIELAB colour spaces, with random or
4//! k-means++ initialisation. Designed to work in native Rust and WebAssembly.
5//!
6//! # Example
7//!
8//! ```
9//! use swatchthis::{generate_swatches_kmeans, pixels_from_rgba};
10//! use swatchthis::algorithms::kmeans::{KmeansColorSpace, InitMethod};
11//!
12//! // Simulate a small image: 4 red pixels and 4 blue pixels (RGBA)
13//! let rgba: Vec<u8> = [255, 0, 0, 255].repeat(4)
14//!     .into_iter()
15//!     .chain([0, 0, 255, 255].repeat(4))
16//!     .collect();
17//!
18//! let pixels = pixels_from_rgba(&rgba);
19//! let swatches = generate_swatches_kmeans(&pixels, 2, KmeansColorSpace::Rgb, InitMethod::KMeansPlusPlus, 42);
20//!
21//! assert_eq!(swatches.len(), 2);
22//! for s in &swatches {
23//!     println!("{} ({}px)", s.hex(), s.population);
24//! }
25//! ```
26
27pub mod algorithms;
28pub mod color;
29pub mod preprocessors;
30pub mod swatch;
31
32use algorithms::kmeans;
33use algorithms::median_cut;
34use algorithms::octree;
35use color::Rgb;
36use kmeans::{InitMethod, KmeansColorSpace};
37use octree::{OctreeColorSpace, OctreeDepth};
38use swatch::Swatch;
39
40/// Extracts dominant colour swatches from a slice of pixels.
41///
42/// Returns swatches sorted by population (most common first). Large images are
43/// automatically subsampled for performance; population counts reflect the
44/// sampled distribution.
45///
46/// # Example
47///
48/// ```
49/// use swatchthis::generate_swatches_kmeans;
50/// use swatchthis::color::Rgb;
51/// use swatchthis::algorithms::kmeans::{KmeansColorSpace, InitMethod};
52///
53/// let pixels = vec![Rgb::new(255, 0, 0); 100];
54/// let swatches = generate_swatches_kmeans(&pixels, 1, KmeansColorSpace::Rgb, InitMethod::Random, 1);
55///
56/// assert_eq!(swatches[0].color, Rgb::new(255, 0, 0));
57/// ```
58pub fn generate_swatches_kmeans(
59    pixels: &[Rgb],
60    count: usize,
61    color_space: KmeansColorSpace,
62    init: InitMethod,
63    seed: u64,
64) -> Vec<Swatch> {
65    collect_sorted_swatches(kmeans::extract_colors_kmeans(
66        pixels,
67        count,
68        color_space,
69        init,
70        seed,
71    ))
72}
73
74pub fn generate_swatches_octree(
75    pixels: &[Rgb],
76    count: usize,
77    color_space: OctreeColorSpace,
78    max_depth: OctreeDepth,
79) -> Vec<Swatch> {
80    collect_sorted_swatches(octree::extract_colors_octree(
81        pixels,
82        count,
83        color_space,
84        max_depth,
85    ))
86}
87
88pub fn generate_swatches_median_cut(pixels: &[Rgb], count: usize) -> Vec<Swatch> {
89    collect_sorted_swatches(median_cut::extract_colors_median_cut(pixels, count))
90}
91
92fn collect_sorted_swatches(raw: Vec<(Rgb, u32)>) -> Vec<Swatch> {
93    let mut swatches: Vec<Swatch> = raw.into_iter().map(|(c, p)| Swatch::new(c, p)).collect();
94    swatches.sort_by_key(|b| std::cmp::Reverse(b.population));
95    swatches
96}
97
98/// Parses raw RGBA bytes into [`Rgb`] pixels. Alpha is discarded.
99///
100/// This is useful for converting `ImageData` from an HTML canvas or any other
101/// source that provides pixels as contiguous RGBA bytes.
102///
103/// # Example
104///
105/// ```
106/// use swatchthis::pixels_from_rgba;
107/// use swatchthis::color::Rgb;
108///
109/// let rgba = [255, 128, 0, 255, 0, 0, 0, 255];
110/// let pixels = pixels_from_rgba(&rgba);
111///
112/// assert_eq!(pixels, vec![Rgb::new(255, 128, 0), Rgb::new(0, 0, 0)]);
113/// ```
114pub fn pixels_from_rgba(data: &[u8]) -> Vec<Rgb> {
115    data.chunks_exact(4)
116        .map(|chunk| Rgb::new(chunk[0], chunk[1], chunk[2]))
117        .collect()
118}
119
120const MAX_SAMPLE: usize = 20_000;
121
122fn sample_step(len: usize) -> usize {
123    if len > MAX_SAMPLE {
124        len / MAX_SAMPLE
125    } else {
126        1
127    }
128}
129
130#[cfg(feature = "wasm")]
131use swatch::swatches_to_json;
132#[cfg(feature = "wasm")]
133use wasm_bindgen::prelude::*;
134
135#[cfg(feature = "wasm")]
136#[wasm_bindgen(js_name = generateSwatches)]
137pub fn generate_swatches_kmeans_wasm(
138    rgba_data: &[u8],
139    count: usize,
140    color_space: &str,
141    init_method: &str,
142    seed: u64,
143) -> String {
144    let pixels = pixels_from_rgba(rgba_data);
145
146    let cs = match color_space {
147        "lab" => KmeansColorSpace::Lab,
148        "lab-ciede2000" => KmeansColorSpace::LabCIEDE2000,
149        _ => KmeansColorSpace::Rgb,
150    };
151    let init = match init_method {
152        "random" => InitMethod::Random,
153        _ => InitMethod::KMeansPlusPlus,
154    };
155
156    let swatches = generate_swatches_kmeans(&pixels, count, cs, init, seed);
157    swatches_to_json(&swatches)
158}
159
160#[cfg(feature = "wasm")]
161#[wasm_bindgen(js_name = generateSwatchesOctree)]
162pub fn generate_swatches_octree_wasm(
163    rgba_data: &[u8],
164    count: usize,
165    color_space: &str,
166    max_depth: u32,
167) -> String {
168    let pixels = pixels_from_rgba(rgba_data);
169
170    let cs = match color_space {
171        "lab" => OctreeColorSpace::Lab,
172        _ => OctreeColorSpace::Rgb,
173    };
174
175    let swatches = generate_swatches_octree(&pixels, count, cs, OctreeDepth::from_u32(max_depth));
176    swatches_to_json(&swatches)
177}
178
179#[cfg(feature = "wasm")]
180#[wasm_bindgen(js_name = generateSwatchesMedianCut)]
181pub fn generate_swatches_median_cut_wasm(rgba_data: &[u8], count: usize) -> String {
182    let pixels = pixels_from_rgba(rgba_data);
183    let swatches = generate_swatches_median_cut(&pixels, count);
184    swatches_to_json(&swatches)
185}
186
187#[cfg(feature = "wasm")]
188#[wasm_bindgen(js_name = complementaryColor)]
189pub fn complementary_color_wasm(r: u8, g: u8, b: u8) -> Vec<u8> {
190    let comp = Rgb::new(r, g, b).to_hsl().complement().to_rgb();
191    vec![comp.r, comp.g, comp.b]
192}
193
194#[cfg(feature = "wasm")]
195#[wasm_bindgen(js_name = slicPreprocess)]
196pub fn slic_preprocess_wasm(
197    rgba_data: &[u8],
198    width: usize,
199    height: usize,
200    num_superpixels: usize,
201    compactness: f32,
202) -> Vec<u8> {
203    let pixels = pixels_from_rgba(rgba_data);
204    let result =
205        preprocessors::slic::slic_preprocess(&pixels, width, height, num_superpixels, compactness);
206    preprocessors::rgb_vec_to_rgba(&result)
207}
208
209#[cfg(feature = "wasm")]
210#[wasm_bindgen(js_name = seedsPreprocess)]
211pub fn seeds_preprocess_wasm(
212    rgba_data: &[u8],
213    width: usize,
214    height: usize,
215    num_superpixels: usize,
216    num_levels: usize,
217    histogram_bins: usize,
218) -> Vec<u8> {
219    let pixels = pixels_from_rgba(rgba_data);
220    let result = preprocessors::seeds::seeds_preprocess(
221        &pixels,
222        width,
223        height,
224        num_superpixels,
225        num_levels,
226        histogram_bins,
227    );
228    preprocessors::rgb_vec_to_rgba(&result)
229}