feat: replace HSLuv colors with OKLCh

This commit is contained in:
link2xt
2025-08-25 18:20:52 +00:00
committed by l
parent 776408c564
commit aaa83a8f52
5 changed files with 52 additions and 33 deletions

32
Cargo.lock generated
View File

@@ -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"

View File

@@ -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.

View File

@@ -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)

View File

@@ -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<u8>) -> 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
// <https://xmpp.org/extensions/xep-0392.html#testvectors-fullrange-no-cvd>
@@ -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);
}
}

View File

@@ -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?;