deltachat/
color.rs

1//! Color generation.
2//!
3//! This is similar to Consistent Color Generation defined in XEP-0392,
4//! but uses OKLCh colorspace instead of HSLuv
5//! to ensure that colors have the same lightness.
6use colorutils_rs::{Oklch, Rgb, TransferFunction};
7use sha1::{Digest, Sha1};
8
9/// Converts an identifier to Hue angle.
10fn str_to_angle(s: &str) -> f32 {
11    let bytes = s.as_bytes();
12    let result = Sha1::digest(bytes);
13    let checksum: u16 = result.first().map_or(0, |&x| u16::from(x))
14        + 256 * result.get(1).map_or(0, |&x| u16::from(x));
15    f32::from(checksum) / 65536.0 * 360.0
16}
17
18/// Converts RGB tuple to a 24-bit number.
19///
20/// Returns a 24-bit number with 8 least significant bits corresponding to the blue color and 8
21/// most significant bits corresponding to the red color.
22fn rgb_to_u32(rgb: Rgb<u8>) -> u32 {
23    65536 * u32::from(rgb.r) + 256 * u32::from(rgb.g) + u32::from(rgb.b)
24}
25
26/// Converts an identifier to RGB color.
27///
28/// Lightness is set to half (0.5) to make colors suitable both for light and dark theme.
29pub fn str_to_color(s: &str) -> u32 {
30    let lightness = 0.5;
31    let chroma = 0.22;
32    let angle = str_to_angle(s);
33    let oklch = Oklch::new(lightness, chroma, angle);
34    let rgb = oklch.to_rgb(TransferFunction::Srgb);
35
36    rgb_to_u32(rgb)
37}
38
39/// Returns color as a "#RRGGBB" `String` where R, G, B are hex digits.
40pub fn color_int_to_hex_string(color: u32) -> String {
41    format!("{color:#08x}").replace("0x", "#")
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47
48    #[test]
49    #[allow(clippy::excessive_precision)]
50    fn test_str_to_angle() {
51        // Test against test vectors from
52        // <https://xmpp.org/extensions/xep-0392.html#testvectors-fullrange-no-cvd>
53        assert!((str_to_angle("Romeo") - 327.255249).abs() < 1e-6);
54        assert!((str_to_angle("juliet@capulet.lit") - 209.410400).abs() < 1e-6);
55        assert!((str_to_angle("😺") - 331.199341).abs() < 1e-6);
56        assert!((str_to_angle("council") - 359.994507).abs() < 1e-6);
57        assert!((str_to_angle("Board") - 171.430664).abs() < 1e-6);
58    }
59
60    #[test]
61    fn test_rgb_to_u32() {
62        assert_eq!(rgb_to_u32(Rgb::new(0, 0, 0)), 0);
63        assert_eq!(rgb_to_u32(Rgb::new(0xff, 0xff, 0xff)), 0xffffff);
64        assert_eq!(rgb_to_u32(Rgb::new(0, 0, 0xff)), 0x0000ff);
65        assert_eq!(rgb_to_u32(Rgb::new(0, 0xff, 0)), 0x00ff00);
66        assert_eq!(rgb_to_u32(Rgb::new(0xff, 0, 0)), 0xff0000);
67        assert_eq!(rgb_to_u32(Rgb::new(0xff, 0x80, 0)), 0xff8000);
68    }
69}