diff --git a/Cargo.lock b/Cargo.lock index 1884d3174..42562364a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -929,6 +929,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorutils-rs" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "103c2458789cd7b46e6ed7c7ba1bf969b6569c902e3732843c55962c53eac686" +dependencies = [ + "erydanos", + "half", + "num-traits", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1299,6 +1310,7 @@ dependencies = [ "brotli", "bytes", "chrono", + "colorutils-rs", "criterion", "data-encoding", "deltachat-contact-tools", @@ -1342,7 +1354,6 @@ dependencies = [ "ratelimit", "regex", "rusqlite", - "rust-hsluv", "rustls", "rustls-pki-types", "sanitize-filename", @@ -1895,6 +1906,15 @@ version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" +[[package]] +name = "erydanos" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cbdc4987ed8e9ece64845393c2d53596b3a4ccbfb3948d799d58f6450e89fb1" +dependencies = [ + "num-traits", +] + [[package]] name = "escaper" version = "0.1.1" @@ -2332,9 +2352,9 @@ dependencies = [ [[package]] name = "half" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if", "crunchy", @@ -5059,12 +5079,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "rust-hsluv" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efe2374f2385cdd8755a446f80b2a646de603c9d8539ca38734879b5c71e378b" - [[package]] name = "rustc-demangle" version = "0.1.24" diff --git a/Cargo.toml b/Cargo.toml index b2ff8a88e..d3bc6ed3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,9 +49,11 @@ async-native-tls = { version = "0.5", default-features = false, features = ["run async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] } async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] } base64 = { workspace = true } +blake3 = "1.8.2" brotli = { version = "8", default-features=false, features = ["std"] } bytes = "1" chrono = { workspace = true, features = ["alloc", "clock", "std"] } +colorutils-rs = { version = "0.7.5", default-features = false } data-encoding = "2.9.0" escaper = "0.1" fast-socks5 = "0.10" @@ -85,7 +87,6 @@ quoted_printable = "0.5" rand = { workspace = true } regex = { workspace = true } rusqlite = { workspace = true, features = ["sqlcipher"] } -rust-hsluv = "0.1" rustls-pki-types = "1.12.0" rustls = { version = "0.23.22", default-features = false } sanitize-filename = { workspace = true } @@ -112,7 +113,6 @@ tracing = "0.1.41" url = "2" uuid = { version = "1", features = ["serde", "v4"] } webpki-roots = "0.26.8" -blake3 = "1.8.2" [dev-dependencies] anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests. diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index f83542766..64651f583 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -32,7 +32,7 @@ async fn test_chat_info() { "archived": false, "param": "", "is_sending_locations": false, - "color": 35391, + "color": 29377, "profile_image": {}, "draft": "", "is_muted": false, @@ -1931,7 +1931,7 @@ async fn test_chat_get_color() -> Result<()> { let t = TestContext::new().await; let chat_id = create_group_ex(&t, None, "a chat").await?; let color1 = Chat::load_from_db(&t, chat_id).await?.get_color(&t).await?; - assert_eq!(color1, 0x008772); + assert_eq!(color1, 0x613dd7); // upper-/lowercase makes a difference for the colors, these are different groups // (in contrast to email addresses, where upper-/lowercase is ignored in practise) diff --git a/src/color.rs b/src/color.rs index 146970b0f..f52c529ea 100644 --- a/src/color.rs +++ b/src/color.rs @@ -1,38 +1,42 @@ -//! Implementation of Consistent Color Generation. +//! Color generation. //! -//! Consistent Color Generation is defined in XEP-0392. +//! This is similar to Consistent Color Generation defined in XEP-0392, +//! but uses OKLCh colorspace instead of HSLuv +//! to ensure that colors have the same lightness. //! //! Color Vision Deficiency correction is not implemented as Delta Chat does not offer //! corresponding settings. -use hsluv::hsluv_to_rgb; +use colorutils_rs::{Oklch, Rgb, TransferFunction}; use sha1::{Digest, Sha1}; /// Converts an identifier to Hue angle. -fn str_to_angle(s: &str) -> f64 { +fn str_to_angle(s: &str) -> f32 { let bytes = s.as_bytes(); let result = Sha1::digest(bytes); let checksum: u16 = result.first().map_or(0, |&x| u16::from(x)) + 256 * result.get(1).map_or(0, |&x| u16::from(x)); - f64::from(checksum) / 65536.0 * 360.0 + f32::from(checksum) / 65536.0 * 360.0 } /// Converts RGB tuple to a 24-bit number. /// /// Returns a 24-bit number with 8 least significant bits corresponding to the blue color and 8 /// most significant bits corresponding to the red color. -fn rgb_to_u32((r, g, b): (f64, f64, f64)) -> u32 { - let r = ((r * 256.0) as u32).min(255); - let g = ((g * 256.0) as u32).min(255); - let b = ((b * 256.0) as u32).min(255); - 65536 * r + 256 * g + b +fn rgb_to_u32(rgb: Rgb) -> u32 { + 65536 * u32::from(rgb.r) + 256 * u32::from(rgb.g) + u32::from(rgb.b) } /// Converts an identifier to RGB color. /// -/// Saturation is set to maximum (100.0) to make colors distinguishable, and lightness is set to -/// half (50.0) to make colors suitable both for light and dark theme. +/// Lightness is set to half (0.5) to make colors suitable both for light and dark theme. pub fn str_to_color(s: &str) -> u32 { - rgb_to_u32(hsluv_to_rgb((str_to_angle(s), 100.0, 50.0))) + let lightness = 0.5; + let chroma = 0.22; + let angle = str_to_angle(s); + let oklch = Oklch::new(lightness, chroma, angle); + let rgb = oklch.to_rgb(TransferFunction::Srgb); + + rgb_to_u32(rgb) } /// Returns color as a "#RRGGBB" `String` where R, G, B are hex digits. @@ -45,6 +49,7 @@ mod tests { use super::*; #[test] + #[allow(clippy::excessive_precision)] fn test_str_to_angle() { // Test against test vectors from // @@ -57,11 +62,11 @@ mod tests { #[test] fn test_rgb_to_u32() { - assert_eq!(rgb_to_u32((0.0, 0.0, 0.0)), 0); - assert_eq!(rgb_to_u32((1.0, 1.0, 1.0)), 0xffffff); - assert_eq!(rgb_to_u32((0.0, 0.0, 1.0)), 0x0000ff); - assert_eq!(rgb_to_u32((0.0, 1.0, 0.0)), 0x00ff00); - assert_eq!(rgb_to_u32((1.0, 0.0, 0.0)), 0xff0000); - assert_eq!(rgb_to_u32((1.0, 0.5, 0.0)), 0xff8000); + assert_eq!(rgb_to_u32(Rgb::new(0, 0, 0)), 0); + assert_eq!(rgb_to_u32(Rgb::new(0xff, 0xff, 0xff)), 0xffffff); + assert_eq!(rgb_to_u32(Rgb::new(0, 0, 0xff)), 0x0000ff); + assert_eq!(rgb_to_u32(Rgb::new(0, 0xff, 0)), 0x00ff00); + assert_eq!(rgb_to_u32(Rgb::new(0xff, 0, 0)), 0xff0000); + assert_eq!(rgb_to_u32(Rgb::new(0xff, 0x80, 0)), 0xff8000); } } diff --git a/src/contact/contact_tests.rs b/src/contact/contact_tests.rs index 14ccc4d93..48f2f2af4 100644 --- a/src/contact/contact_tests.rs +++ b/src/contact/contact_tests.rs @@ -759,7 +759,7 @@ async fn test_contact_get_color() -> Result<()> { let t = TestContext::new().await; let contact_id = Contact::create(&t, "name", "name@example.net").await?; let color1 = Contact::get_by_id(&t, contact_id).await?.get_color(); - assert_eq!(color1, 0xA739FF); + assert_eq!(color1, 0x4947dc); let t = TestContext::new().await; let contact_id = Contact::create(&t, "prename name", "name@example.net").await?;