mirror of
https://github.com/chatmail/core.git
synced 2026-05-19 23:06:32 +03:00
feat: replace HSLuv colors with OKLCh
This commit is contained in:
32
Cargo.lock
generated
32
Cargo.lock
generated
@@ -929,6 +929,17 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
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]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@@ -1299,6 +1310,7 @@ dependencies = [
|
|||||||
"brotli",
|
"brotli",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"colorutils-rs",
|
||||||
"criterion",
|
"criterion",
|
||||||
"data-encoding",
|
"data-encoding",
|
||||||
"deltachat-contact-tools",
|
"deltachat-contact-tools",
|
||||||
@@ -1342,7 +1354,6 @@ dependencies = [
|
|||||||
"ratelimit",
|
"ratelimit",
|
||||||
"regex",
|
"regex",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"rust-hsluv",
|
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"sanitize-filename",
|
"sanitize-filename",
|
||||||
@@ -1895,6 +1906,15 @@ version = "3.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f"
|
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]]
|
[[package]]
|
||||||
name = "escaper"
|
name = "escaper"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -2332,9 +2352,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "half"
|
name = "half"
|
||||||
version = "2.4.0"
|
version = "2.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e"
|
checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"crunchy",
|
"crunchy",
|
||||||
@@ -5059,12 +5079,6 @@ dependencies = [
|
|||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rust-hsluv"
|
|
||||||
version = "0.1.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "efe2374f2385cdd8755a446f80b2a646de603c9d8539ca38734879b5c71e378b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.24"
|
version = "0.1.24"
|
||||||
|
|||||||
@@ -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-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
|
||||||
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
|
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
|
blake3 = "1.8.2"
|
||||||
brotli = { version = "8", default-features=false, features = ["std"] }
|
brotli = { version = "8", default-features=false, features = ["std"] }
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
||||||
|
colorutils-rs = { version = "0.7.5", default-features = false }
|
||||||
data-encoding = "2.9.0"
|
data-encoding = "2.9.0"
|
||||||
escaper = "0.1"
|
escaper = "0.1"
|
||||||
fast-socks5 = "0.10"
|
fast-socks5 = "0.10"
|
||||||
@@ -85,7 +87,6 @@ quoted_printable = "0.5"
|
|||||||
rand = { workspace = true }
|
rand = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||||
rust-hsluv = "0.1"
|
|
||||||
rustls-pki-types = "1.12.0"
|
rustls-pki-types = "1.12.0"
|
||||||
rustls = { version = "0.23.22", default-features = false }
|
rustls = { version = "0.23.22", default-features = false }
|
||||||
sanitize-filename = { workspace = true }
|
sanitize-filename = { workspace = true }
|
||||||
@@ -112,7 +113,6 @@ tracing = "0.1.41"
|
|||||||
url = "2"
|
url = "2"
|
||||||
uuid = { version = "1", features = ["serde", "v4"] }
|
uuid = { version = "1", features = ["serde", "v4"] }
|
||||||
webpki-roots = "0.26.8"
|
webpki-roots = "0.26.8"
|
||||||
blake3 = "1.8.2"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ async fn test_chat_info() {
|
|||||||
"archived": false,
|
"archived": false,
|
||||||
"param": "",
|
"param": "",
|
||||||
"is_sending_locations": false,
|
"is_sending_locations": false,
|
||||||
"color": 35391,
|
"color": 29377,
|
||||||
"profile_image": {},
|
"profile_image": {},
|
||||||
"draft": "",
|
"draft": "",
|
||||||
"is_muted": false,
|
"is_muted": false,
|
||||||
@@ -1931,7 +1931,7 @@ async fn test_chat_get_color() -> Result<()> {
|
|||||||
let t = TestContext::new().await;
|
let t = TestContext::new().await;
|
||||||
let chat_id = create_group_ex(&t, None, "a chat").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?;
|
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
|
// upper-/lowercase makes a difference for the colors, these are different groups
|
||||||
// (in contrast to email addresses, where upper-/lowercase is ignored in practise)
|
// (in contrast to email addresses, where upper-/lowercase is ignored in practise)
|
||||||
|
|||||||
43
src/color.rs
43
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
|
//! Color Vision Deficiency correction is not implemented as Delta Chat does not offer
|
||||||
//! corresponding settings.
|
//! corresponding settings.
|
||||||
use hsluv::hsluv_to_rgb;
|
use colorutils_rs::{Oklch, Rgb, TransferFunction};
|
||||||
use sha1::{Digest, Sha1};
|
use sha1::{Digest, Sha1};
|
||||||
|
|
||||||
/// Converts an identifier to Hue angle.
|
/// 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 bytes = s.as_bytes();
|
||||||
let result = Sha1::digest(bytes);
|
let result = Sha1::digest(bytes);
|
||||||
let checksum: u16 = result.first().map_or(0, |&x| u16::from(x))
|
let checksum: u16 = result.first().map_or(0, |&x| u16::from(x))
|
||||||
+ 256 * result.get(1).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.
|
/// 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
|
/// 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.
|
/// most significant bits corresponding to the red color.
|
||||||
fn rgb_to_u32((r, g, b): (f64, f64, f64)) -> u32 {
|
fn rgb_to_u32(rgb: Rgb<u8>) -> u32 {
|
||||||
let r = ((r * 256.0) as u32).min(255);
|
65536 * u32::from(rgb.r) + 256 * u32::from(rgb.g) + u32::from(rgb.b)
|
||||||
let g = ((g * 256.0) as u32).min(255);
|
|
||||||
let b = ((b * 256.0) as u32).min(255);
|
|
||||||
65536 * r + 256 * g + b
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts an identifier to RGB color.
|
/// Converts an identifier to RGB color.
|
||||||
///
|
///
|
||||||
/// Saturation is set to maximum (100.0) to make colors distinguishable, and lightness is set to
|
/// Lightness is set to half (0.5) to make colors suitable both for light and dark theme.
|
||||||
/// half (50.0) to make colors suitable both for light and dark theme.
|
|
||||||
pub fn str_to_color(s: &str) -> u32 {
|
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.
|
/// Returns color as a "#RRGGBB" `String` where R, G, B are hex digits.
|
||||||
@@ -45,6 +49,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[allow(clippy::excessive_precision)]
|
||||||
fn test_str_to_angle() {
|
fn test_str_to_angle() {
|
||||||
// Test against test vectors from
|
// Test against test vectors from
|
||||||
// <https://xmpp.org/extensions/xep-0392.html#testvectors-fullrange-no-cvd>
|
// <https://xmpp.org/extensions/xep-0392.html#testvectors-fullrange-no-cvd>
|
||||||
@@ -57,11 +62,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_rgb_to_u32() {
|
fn test_rgb_to_u32() {
|
||||||
assert_eq!(rgb_to_u32((0.0, 0.0, 0.0)), 0);
|
assert_eq!(rgb_to_u32(Rgb::new(0, 0, 0)), 0);
|
||||||
assert_eq!(rgb_to_u32((1.0, 1.0, 1.0)), 0xffffff);
|
assert_eq!(rgb_to_u32(Rgb::new(0xff, 0xff, 0xff)), 0xffffff);
|
||||||
assert_eq!(rgb_to_u32((0.0, 0.0, 1.0)), 0x0000ff);
|
assert_eq!(rgb_to_u32(Rgb::new(0, 0, 0xff)), 0x0000ff);
|
||||||
assert_eq!(rgb_to_u32((0.0, 1.0, 0.0)), 0x00ff00);
|
assert_eq!(rgb_to_u32(Rgb::new(0, 0xff, 0)), 0x00ff00);
|
||||||
assert_eq!(rgb_to_u32((1.0, 0.0, 0.0)), 0xff0000);
|
assert_eq!(rgb_to_u32(Rgb::new(0xff, 0, 0)), 0xff0000);
|
||||||
assert_eq!(rgb_to_u32((1.0, 0.5, 0.0)), 0xff8000);
|
assert_eq!(rgb_to_u32(Rgb::new(0xff, 0x80, 0)), 0xff8000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -759,7 +759,7 @@ async fn test_contact_get_color() -> Result<()> {
|
|||||||
let t = TestContext::new().await;
|
let t = TestContext::new().await;
|
||||||
let contact_id = Contact::create(&t, "name", "name@example.net").await?;
|
let contact_id = Contact::create(&t, "name", "name@example.net").await?;
|
||||||
let color1 = Contact::get_by_id(&t, contact_id).await?.get_color();
|
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 t = TestContext::new().await;
|
||||||
let contact_id = Contact::create(&t, "prename name", "name@example.net").await?;
|
let contact_id = Contact::create(&t, "prename name", "name@example.net").await?;
|
||||||
|
|||||||
Reference in New Issue
Block a user