tinted_scheme_extractor/
lib.rs

1mod color;
2mod utils;
3
4use palette::{rgb::Rgb, FromColor, Hsl, Srgb};
5use std::{collections::HashMap, path::PathBuf};
6use tinted_builder::{Base16Scheme, Color as SchemeColor};
7
8use crate::{
9    color::Color,
10    utils::{
11        create_palette_with_color_thief_colors, create_palette_with_inverse_colors, dark_color,
12        find_closest_palette, fix_colors, generate_gradient, light_color, load_image,
13    },
14};
15
16pub use tinted_builder::{SchemeSystem, SchemeVariant};
17
18#[non_exhaustive]
19#[derive(thiserror::Error, Debug)]
20pub enum Error {
21    #[error("no colors")]
22    NoColors(String),
23    #[error("generate colors")]
24    GenerateColors(String),
25    #[error("unsupported scheme variant")]
26    UnsupportedSchemeVariant(String),
27    #[error("other")]
28    Other(String),
29}
30
31#[derive(Debug)]
32pub struct SchemeParams {
33    pub image_path: PathBuf,
34    pub author: String,
35    pub description: Option<String>,
36    pub name: String,
37    pub slug: String,
38    pub system: SchemeSystem,
39    pub variant: SchemeVariant,
40    pub verbose: bool,
41}
42
43pub fn create_scheme_from_image(params: SchemeParams) -> Result<Base16Scheme, Error> {
44    let SchemeParams {
45        image_path,
46        author,
47        description,
48        name,
49        slug,
50        system,
51        variant,
52        verbose,
53    } = params;
54    let image = load_image(&image_path);
55    let initial_palette: Vec<Color> = find_closest_palette(&image);
56    let inital_inverse_palette: Vec<Color> = find_closest_palette(&image)
57        .iter()
58        .map(|color| color.get_inverse())
59        .collect();
60    let curated_palette =
61        create_palette_with_inverse_colors(&initial_palette, &inital_inverse_palette);
62    let color_thief_palette: Vec<Srgb<u8>> = color_thief::get_palette(
63        image.to_rgba8().into_raw().as_slice(),
64        color_thief::ColorFormat::Rgba,
65        1,
66        15,
67    )
68    .map_err(|err| Error::GenerateColors(err.to_string()))?
69    .iter()
70    .map(|c| Srgb::new(c.r, c.g, c.b))
71    .collect();
72    let combined_palette =
73        create_palette_with_color_thief_colors(&curated_palette, &color_thief_palette)?;
74    let color_thief_pallette_as_rgb_vec: Vec<Rgb> = color_thief_palette
75        .clone()
76        .iter()
77        .map(|c| {
78            Rgb::new(
79                c.red as f32 / 255.0,
80                c.green as f32 / 255.0,
81                c.blue as f32 / 255.0,
82            )
83        })
84        .collect();
85    let light = light_color(&color_thief_pallette_as_rgb_vec, verbose)?;
86    let dark = dark_color(&color_thief_pallette_as_rgb_vec, verbose)?;
87    let (background, foreground) = match &variant {
88        SchemeVariant::Dark | SchemeVariant::Light => Ok(fix_colors(dark, light, &variant)),
89        variant => Err(Error::UnsupportedSchemeVariant(variant.to_string())),
90    }?;
91    let gradient = generate_gradient(Srgb::from(background), Srgb::from(foreground), 8);
92
93    let mut scheme_palette: HashMap<String, SchemeColor> = HashMap::new();
94
95    for (index, rgb) in gradient.iter().enumerate() {
96        scheme_palette.entry(format!("base0{}", index)).or_insert(
97            SchemeColor::new(format!("{:02X}{:02X}{:02X}", rgb.red, rgb.green, rgb.blue))
98                .map_err(|err| Error::GenerateColors(err.to_string()))?,
99        );
100    }
101
102    for color in &combined_palette {
103        let diff = get_lightness_weight_difference(color, 0.7);
104        let color = color.add_lightness(diff);
105
106        match color.associated_pure_color.as_str() {
107            "red" => {
108                scheme_palette.entry("base08".to_string()).or_insert(
109                    SchemeColor::new(color.to_hex())
110                        .map_err(|err| Error::GenerateColors(err.to_string()))?,
111                );
112            }
113            "orange" => {
114                scheme_palette.entry("base09".to_string()).or_insert(
115                    SchemeColor::new(color.to_hex())
116                        .map_err(|err| Error::GenerateColors(err.to_string()))?,
117                );
118            }
119            "yellow" => {
120                scheme_palette.entry("base0A".to_string()).or_insert(
121                    SchemeColor::new(color.to_hex())
122                        .map_err(|err| Error::GenerateColors(err.to_string()))?,
123                );
124            }
125            "green" => {
126                scheme_palette.entry("base0B".to_string()).or_insert(
127                    SchemeColor::new(color.to_hex())
128                        .map_err(|err| Error::GenerateColors(err.to_string()))?,
129                );
130            }
131            "cyan" => {
132                scheme_palette.entry("base0C".to_string()).or_insert(
133                    SchemeColor::new(color.to_hex())
134                        .map_err(|err| Error::GenerateColors(err.to_string()))?,
135                );
136            }
137            "blue" => {
138                scheme_palette.entry("base0D".to_string()).or_insert(
139                    SchemeColor::new(color.to_hex())
140                        .map_err(|err| Error::GenerateColors(err.to_string()))?,
141                );
142            }
143            "purple" => {
144                scheme_palette.entry("base0E".to_string()).or_insert(
145                    SchemeColor::new(color.to_hex())
146                        .map_err(|err| Error::GenerateColors(err.to_string()))?,
147                );
148            }
149            "brown" => {
150                scheme_palette.entry("base0F".to_string()).or_insert(
151                    SchemeColor::new(color.to_hex())
152                        .map_err(|err| Error::GenerateColors(err.to_string()))?,
153                );
154            }
155            _ => {}
156        }
157
158        if let SchemeSystem::Base24 = system {
159            let updated_color = color.to_saturated(0.7);
160
161            match updated_color.associated_pure_color.as_str() {
162                "red" => {
163                    scheme_palette.entry("base10".to_string()).or_insert(
164                        SchemeColor::new(updated_color.to_hex())
165                            .map_err(|err| Error::GenerateColors(err.to_string()))?,
166                    );
167                }
168                "orange" => {
169                    scheme_palette.entry("base11".to_string()).or_insert(
170                        SchemeColor::new(updated_color.to_hex())
171                            .map_err(|err| Error::GenerateColors(err.to_string()))?,
172                    );
173                }
174                "yellow" => {
175                    scheme_palette.entry("base12".to_string()).or_insert(
176                        SchemeColor::new(updated_color.to_hex())
177                            .map_err(|err| Error::GenerateColors(err.to_string()))?,
178                    );
179                }
180                "green" => {
181                    scheme_palette.entry("base13".to_string()).or_insert(
182                        SchemeColor::new(updated_color.to_hex())
183                            .map_err(|err| Error::GenerateColors(err.to_string()))?,
184                    );
185                }
186                "cyan" => {
187                    scheme_palette.entry("base14".to_string()).or_insert(
188                        SchemeColor::new(updated_color.to_hex())
189                            .map_err(|err| Error::GenerateColors(err.to_string()))?,
190                    );
191                }
192                "blue" => {
193                    scheme_palette.entry("base15".to_string()).or_insert(
194                        SchemeColor::new(updated_color.to_hex())
195                            .map_err(|err| Error::GenerateColors(err.to_string()))?,
196                    );
197                }
198                "purple" => {
199                    scheme_palette.entry("base16".to_string()).or_insert(
200                        SchemeColor::new(updated_color.to_hex())
201                            .map_err(|err| Error::GenerateColors(err.to_string()))?,
202                    );
203                }
204                "brown" => {
205                    scheme_palette.entry("base17".to_string()).or_insert(
206                        SchemeColor::new(updated_color.to_hex())
207                            .map_err(|err| Error::GenerateColors(err.to_string()))?,
208                    );
209                }
210                _ => {}
211            }
212        }
213    }
214
215    let scheme = Base16Scheme {
216        author,
217        description,
218        name,
219        slug,
220        system,
221        variant,
222        palette: scheme_palette,
223    };
224
225    Ok(scheme)
226}
227
228fn get_lightness_weight_difference(color: &Color, threshold: f32) -> f32 {
229    let color: Hsl = Hsl::from_color(color.value.into_format::<f32>());
230    let alpha = 0.5; // Weight for saturation
231    let beta = 1.0; // Weight for lightness
232
233    let visibility_metric = alpha * color.saturation + beta * color.lightness;
234
235    let value = ((threshold - visibility_metric) / beta).clamp(0.0, 1.0);
236
237    value / 2.0
238}