use anyhow::{Context, Result};
use image::{DynamicImage, Rgba};
use palette::{IntoColor, Lab, Oklch, Srgb};
use std::path::Path;
pub mod block_palettes;
pub mod extraction;
pub mod palettes;
pub mod similarity;
pub mod spaces;
pub mod texture_mapping;
#[derive(Debug, Clone, Copy)]
pub struct ExtendedColorData {
pub rgb: [u8; 3],
pub oklab: [f32; 3],
pub hsl: [f32; 3],
pub lab: [f32; 3],
pub oklch: [f32; 3],
pub hex: u32,
}
impl ExtendedColorData {
pub fn from_rgb(r: u8, g: u8, b: u8) -> Self {
let rgb = [r, g, b];
let srgb = Srgb::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0);
let lab: Lab = srgb.into_color();
let oklch: Oklch = srgb.into_color();
let hsl = rgb_to_hsl(r, g, b);
let oklab = rgb_to_oklab_simple(rgb);
let lab_values = [lab.l, lab.a, lab.b];
let oklch_values = [oklch.l, oklch.chroma, oklch.hue.into_positive_degrees()];
let hex = ((r as u32) << 16) | ((g as u32) << 8) | (b as u32);
ExtendedColorData {
rgb,
oklab,
hsl,
lab: lab_values,
oklch: oklch_values,
hex,
}
}
pub fn hex_string(&self) -> String {
format!("#{:06X}", self.hex)
}
pub fn distance_oklab(&self, other: &ExtendedColorData) -> f32 {
let dl = self.oklab[0] - other.oklab[0];
let da = self.oklab[1] - other.oklab[1];
let db = self.oklab[2] - other.oklab[2];
(dl * dl + da * da + db * db).sqrt()
}
pub fn distance_rgb(&self, other: &ExtendedColorData) -> f32 {
let dr = (self.rgb[0] as f32) - (other.rgb[0] as f32);
let dg = (self.rgb[1] as f32) - (other.rgb[1] as f32);
let db = (self.rgb[2] as f32) - (other.rgb[2] as f32);
(dr * dr + dg * dg + db * db).sqrt()
}
}
pub fn extract_dominant_color(image_path: &Path) -> Result<ExtendedColorData> {
let img = image::open(image_path)
.with_context(|| format!("Failed to open image: {:?}", image_path))?;
extract_dominant_color_from_image(&img)
}
pub fn extract_dominant_color_from_image(img: &DynamicImage) -> Result<ExtendedColorData> {
let rgba_img = img.to_rgba8();
let (width, height) = rgba_img.dimensions();
let mut r_sum = 0u64;
let mut g_sum = 0u64;
let mut b_sum = 0u64;
let mut pixel_count = 0u64;
for y in 0..height {
for x in 0..width {
let pixel = rgba_img.get_pixel(x, y);
let Rgba([r, g, b, a]) = *pixel;
if a > 128 {
r_sum += r as u64;
g_sum += g as u64;
b_sum += b as u64;
pixel_count += 1;
}
}
}
if pixel_count == 0 {
anyhow::bail!("No opaque pixels found in image");
}
let avg_r = (r_sum / pixel_count) as u8;
let avg_g = (g_sum / pixel_count) as u8;
let avg_b = (b_sum / pixel_count) as u8;
Ok(ExtendedColorData::from_rgb(avg_r, avg_g, avg_b))
}
fn rgb_to_hsl(r: u8, g: u8, b: u8) -> [f32; 3] {
let r = r as f32 / 255.0;
let g = g as f32 / 255.0;
let b = b as f32 / 255.0;
let max = r.max(g.max(b));
let min = r.min(g.min(b));
let delta = max - min;
let l = (max + min) / 2.0;
if delta == 0.0 {
return [0.0, 0.0, l];
}
let s = if l < 0.5 {
delta / (max + min)
} else {
delta / (2.0 - max - min)
};
let h = if max == r {
((g - b) / delta + if g < b { 6.0 } else { 0.0 }) / 6.0
} else if max == g {
((b - r) / delta + 2.0) / 6.0
} else {
((r - g) / delta + 4.0) / 6.0
};
[h * 360.0, s, l]
}
fn rgb_to_oklab_simple(rgb: [u8; 3]) -> [f32; 3] {
let r = rgb[0] as f32 / 255.0;
let g = rgb[1] as f32 / 255.0;
let b = rgb[2] as f32 / 255.0;
let l = 0.2126 * r + 0.7152 * g + 0.0722 * b;
let a = (r - g) * 0.5;
let b_val = (r + g - 2.0 * b) * 0.25;
[l, a, b_val]
}