Skip to main content

excelize_rs/
hsl.rs

1//! HSL color utilities.
2//!
3//! This module corresponds to `hsl.go` in the Go implementation.
4
5/// HSL color representation.
6#[derive(Debug, Default, Clone, Copy, PartialEq)]
7pub struct HSL {
8    /// Hue.
9    pub h: f64,
10    /// Saturation.
11    pub s: f64,
12    /// Lightness.
13    pub l: f64,
14}
15
16/// Convert an RGB triple to HSL.
17pub fn rgb_to_hsl(r: u8, g: u8, b: u8) -> (f64, f64, f64) {
18    let f_r = f64::from(r) / 255.0;
19    let f_g = f64::from(g) / 255.0;
20    let f_b = f64::from(b) / 255.0;
21    let max_val = f64::max(f64::max(f_r, f_g), f_b);
22    let min_val = f64::min(f64::min(f_r, f_g), f_b);
23    let l = (max_val + min_val) / 2.0;
24    let mut h;
25    let s;
26    if max_val == min_val {
27        // Achromatic.
28        h = 0.0;
29        s = 0.0;
30    } else {
31        // Chromatic.
32        let d = max_val - min_val;
33        s = if l > 0.5 {
34            d / (2.0 - max_val - min_val)
35        } else {
36            d / (max_val + min_val)
37        };
38        match max_val {
39            x if x == f_r => {
40                h = (f_g - f_b) / d;
41                if f_g < f_b {
42                    h += 6.0;
43                }
44            }
45            x if x == f_g => {
46                h = (f_b - f_r) / d + 2.0;
47            }
48            _ => {
49                h = (f_r - f_g) / d + 4.0;
50            }
51        }
52        h /= 6.0;
53    }
54    (h, s, l)
55}
56
57/// Convert an HSL triple to RGB.
58pub fn hsl_to_rgb(h: f64, s: f64, l: f64) -> (u8, u8, u8) {
59    let (f_r, f_g, f_b) = if s == 0.0 {
60        (l, l, l)
61    } else {
62        let q = if l < 0.5 {
63            l * (1.0 + s)
64        } else {
65            l + s - s * l
66        };
67        let p = 2.0 * l - q;
68        (
69            hue_to_rgb(p, q, h + 1.0 / 3.0),
70            hue_to_rgb(p, q, h),
71            hue_to_rgb(p, q, h - 1.0 / 3.0),
72        )
73    };
74    let r = ((f_r * 255.0) + 0.5) as u8;
75    let g = ((f_g * 255.0) + 0.5) as u8;
76    let b = ((f_b * 255.0) + 0.5) as u8;
77    (r, g, b)
78}
79
80fn hue_to_rgb(p: f64, q: f64, mut t: f64) -> f64 {
81    if t < 0.0 {
82        t += 1.0;
83    }
84    if t > 1.0 {
85        t -= 1.0;
86    }
87    if t < 1.0 / 6.0 {
88        return p + (q - p) * 6.0 * t;
89    }
90    if t < 0.5 {
91        return q;
92    }
93    if t < 2.0 / 3.0 {
94        return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
95    }
96    p
97}
98
99/// Apply a tint to a base RGB color and return the result as `"FFRRGGBB"`.
100///
101/// `base_color` should be a 6-digit (`RRGGBB`) or 8-digit (`FFRRGGBB`) hex
102/// string. For other inputs the original color is returned unchanged when the
103/// tint is zero, or with an `FF` prefix otherwise.
104pub fn theme_color(base_color: &str, tint: f64) -> String {
105    if tint == 0.0 {
106        return format!("FF{}", base_color);
107    }
108
109    let color = if base_color.len() == 8 {
110        &base_color[2..]
111    } else {
112        base_color
113    };
114
115    if color.len() != 6 {
116        return format!("FF{}", base_color);
117    }
118
119    let parse = |s: &str| u8::from_str_radix(s, 16).unwrap_or(0);
120    let r = parse(&color[0..2]);
121    let g = parse(&color[2..4]);
122    let b = parse(&color[4..6]);
123
124    let (h, s, mut l) = rgb_to_hsl(r, g, b);
125    if tint < 0.0 {
126        l *= 1.0 + tint;
127    } else {
128        l = l * (1.0 - tint) + (1.0 - (1.0 - tint));
129    }
130
131    let (br, bg, bb) = hsl_to_rgb(h, s, l);
132    format!("FF{:02X}{:02X}{:02X}", br, bg, bb)
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_rgb_to_hsl_red() {
141        let (h, s, l) = rgb_to_hsl(255, 0, 0);
142        assert!((h - 0.0).abs() < 1e-10 || (h - 1.0).abs() < 1e-10);
143        assert!((s - 1.0).abs() < 1e-10);
144        assert!((l - 0.5).abs() < 1e-10);
145    }
146
147    #[test]
148    fn test_rgb_to_hsl_green() {
149        let (h, s, l) = rgb_to_hsl(0, 255, 0);
150        assert!((h - 1.0 / 3.0).abs() < 1e-10);
151        assert!((s - 1.0).abs() < 1e-10);
152        assert!((l - 0.5).abs() < 1e-10);
153    }
154
155    #[test]
156    fn test_rgb_to_hsl_blue() {
157        let (h, s, l) = rgb_to_hsl(0, 0, 255);
158        assert!((h - 2.0 / 3.0).abs() < 1e-10);
159        assert!((s - 1.0).abs() < 1e-10);
160        assert!((l - 0.5).abs() < 1e-10);
161    }
162
163    #[test]
164    fn test_rgb_to_hsl_white() {
165        let (h, s, l) = rgb_to_hsl(255, 255, 255);
166        assert_eq!(h, 0.0);
167        assert_eq!(s, 0.0);
168        assert!((l - 1.0).abs() < 1e-10);
169    }
170
171    #[test]
172    fn test_rgb_to_hsl_black() {
173        let (h, s, l) = rgb_to_hsl(0, 0, 0);
174        assert_eq!(h, 0.0);
175        assert_eq!(s, 0.0);
176        assert_eq!(l, 0.0);
177    }
178
179    #[test]
180    fn test_hsl_to_rgb_red() {
181        assert_eq!(hsl_to_rgb(0.0, 1.0, 0.5), (255, 0, 0));
182    }
183
184    #[test]
185    fn test_hsl_to_rgb_green() {
186        assert_eq!(hsl_to_rgb(1.0 / 3.0, 1.0, 0.5), (0, 255, 0));
187    }
188
189    #[test]
190    fn test_hsl_to_rgb_blue() {
191        assert_eq!(hsl_to_rgb(2.0 / 3.0, 1.0, 0.5), (0, 0, 255));
192    }
193
194    #[test]
195    fn test_hsl_to_rgb_white() {
196        assert_eq!(hsl_to_rgb(0.0, 0.0, 1.0), (255, 255, 255));
197    }
198
199    #[test]
200    fn test_hsl_to_rgb_black() {
201        assert_eq!(hsl_to_rgb(0.0, 0.0, 0.0), (0, 0, 0));
202    }
203
204    #[test]
205    fn test_hsl_round_trip() {
206        for r in [0, 64, 128, 192, 255] {
207            for g in [0, 64, 128, 192, 255] {
208                for b in [0, 64, 128, 192, 255] {
209                    let (h, s, l) = rgb_to_hsl(r, g, b);
210                    let (rr, rg, rb) = hsl_to_rgb(h, s, l);
211                    assert_eq!((r, g, b), (rr, rg, rb));
212                }
213            }
214        }
215    }
216
217    #[test]
218    fn test_theme_color() {
219        assert_eq!(theme_color("000000", -0.1), "FF000000");
220        assert_eq!(theme_color("000000", 0.0), "FF000000");
221        assert_eq!(theme_color("00FF00", 0.2), "FF33FF33");
222        assert_eq!(theme_color("000000", 1.0), "FFFFFFFF");
223        assert_eq!(theme_color("FF0000", 1.0), "FFFFFFFF");
224        assert_eq!(theme_color("FFFFFF", -1.0), "FF000000");
225    }
226
227    #[test]
228    fn test_theme_color_eight_digit_input() {
229        assert_eq!(theme_color("FF00FF00", 0.2), "FF33FF33");
230    }
231
232    #[test]
233    fn test_theme_color_invalid_input() {
234        assert_eq!(theme_color("red", 0.5), "FFred");
235        assert_eq!(theme_color("red", 0.0), "FFred");
236    }
237}