diff --git a/Cargo.lock b/Cargo.lock index 224a20b832..9272a44eb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1112,10 +1112,12 @@ dependencies = [ "rand 0.7.3", "regex", "rusqlite", + "rust-hsluv", "rustyline", "sanitize-filename", "serde", "serde_json", + "sha-1", "sha2", "smallvec", "stop-token", @@ -3019,6 +3021,12 @@ dependencies = [ "crossbeam-utils 0.8.1", ] +[[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.18" @@ -3224,9 +3232,9 @@ dependencies = [ [[package]] name = "sha-1" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce3cdf1b5e620a498ee6f2a171885ac7e22f0e12089ec4b3d22b84921792507c" +checksum = "f4b312c3731e3fe78a185e6b9b911a7aa715b8e31cce117975219aab2acf285d" dependencies = [ "block-buffer", "cfg-if 1.0.0", diff --git a/Cargo.toml b/Cargo.toml index 0e0a95d488..ef7234c62e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ deltachat_derive = { path = "./deltachat_derive" } libc = "0.2.51" pgp = { version = "0.7.0", default-features = false } hex = "0.4.0" +sha-1 = "0.9.3" sha2 = "0.9.0" rand = "0.7.0" smallvec = "1.0.0" @@ -64,6 +65,7 @@ url = "2.1.1" async-std-resolver = "0.19.5" async-tar = "0.3.0" uuid = { version = "0.8", features = ["serde", "v4"] } +rust-hsluv = "0.1.4" pretty_env_logger = { version = "0.4.0", optional = true } log = {version = "0.4.8", optional = true } diff --git a/src/chat.rs b/src/chat.rs index 0dc729180f..83963aa83a 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -15,6 +15,7 @@ use serde::{Deserialize, Serialize}; use crate::aheader::EncryptPreference; use crate::blob::{BlobError, BlobObject}; use crate::chatlist::dc_get_archived_cnt; +use crate::color::str_to_color; use crate::config::Config; use crate::constants::{ Blocked, Chattype, ShowEmails, Viewtype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, @@ -26,8 +27,8 @@ use crate::contact::{addr_cmp, Contact, Origin, VerifiedStatus}; use crate::context::Context; use crate::dc_tools::{ dc_create_id, dc_create_outgoing_rfc724_mid, dc_create_smeared_timestamp, - dc_create_smeared_timestamps, dc_get_abs_path, dc_gm2local_offset, dc_str_to_color, - improve_single_line_input, time, IsNoneOrEmpty, + dc_create_smeared_timestamps, dc_get_abs_path, dc_gm2local_offset, improve_single_line_input, + time, IsNoneOrEmpty, }; use crate::ephemeral::{delete_expired_messages, schedule_ephemeral_task, Timer as EphemeralTimer}; use crate::events::EventType; @@ -891,7 +892,7 @@ impl Chat { } } } else { - color = dc_str_to_color(&self.name); + color = str_to_color(&self.name); } color @@ -3081,7 +3082,7 @@ mod tests { "param": "", "gossiped_timestamp": 0, "is_sending_locations": false, - "color": 15895624, + "color": 35391, "profile_image": "", "draft": "", "is_muted": false, diff --git a/src/color.rs b/src/color.rs new file mode 100644 index 0000000000..457b4c25d5 --- /dev/null +++ b/src/color.rs @@ -0,0 +1,46 @@ +//! Implementation of Consistent Color Generation +//! +//! Consistent Color Generation is defined in XEP-0392. +//! +//! Color Vision Deficiency correction is not implemented as Delta Chat does not offer +//! corresponding settings. +use hsluv::hsluv_to_rgb; +use sha1::{Digest, Sha1}; + +/// Converts an identifier to Hue angle. +fn str_to_angle(s: impl AsRef) -> f64 { + let bytes = s.as_ref().as_bytes(); + let result = Sha1::digest(bytes); + let checksum: u16 = result.get(0).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 +} + +/// Converts an identifier to RGB color. +/// +/// 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. +/// +/// 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. +pub(crate) fn str_to_color(s: impl AsRef) -> u32 { + let (r, g, b) = hsluv_to_rgb((str_to_angle(s), 100.0, 50.0)); + 65536 * (r * 256.0) as u32 + 256 * (g * 256.0) as u32 + (b * 256.0) as u32 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::float_cmp)] + #[test] + fn test_str_to_angle() { + // Test against test vectors from + // https://xmpp.org/extensions/xep-0392.html#testvectors-fullrange-no-cvd + assert!((str_to_angle("Romeo") - 327.255249).abs() < 1e-6); + assert!((str_to_angle("juliet@capulet.lit") - 209.410400).abs() < 1e-6); + assert!((str_to_angle("😺") - 331.199341).abs() < 1e-6); + assert!((str_to_angle("council") - 359.994507).abs() < 1e-6); + assert!((str_to_angle("Board") - 171.430664).abs() < 1e-6); + } +} diff --git a/src/contact.rs b/src/contact.rs index 7646848983..91520fb8a2 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -9,15 +9,14 @@ use regex::Regex; use crate::aheader::EncryptPreference; use crate::chat::ChatId; +use crate::color::str_to_color; use crate::config::Config; use crate::constants::{ Chattype, DC_CHAT_ID_DEADDROP, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_DEVICE_ADDR, DC_CONTACT_ID_LAST_SPECIAL, DC_CONTACT_ID_SELF, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY, }; use crate::context::Context; -use crate::dc_tools::{ - dc_get_abs_path, dc_str_to_color, improve_single_line_input, listflags_has, EmailAddress, -}; +use crate::dc_tools::{dc_get_abs_path, improve_single_line_input, listflags_has, EmailAddress}; use crate::events::EventType; use crate::key::{DcKey, SignedPublicKey}; use crate::login_param::LoginParam; @@ -947,7 +946,7 @@ impl Contact { /// and can be used for an fallback avatar with white initials /// as well as for headlines in bubbles of group chats. pub fn get_color(&self) -> u32 { - dc_str_to_color(&self.addr) + str_to_color(&self.addr) } /// Gets the contact's status. diff --git a/src/dc_tools.rs b/src/dc_tools.rs index ce182935ae..d1fb386709 100644 --- a/src/dc_tools.rs +++ b/src/dc_tools.rs @@ -48,31 +48,6 @@ pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow { } } -/// the colors must fulfill some criterions as: -/// - contrast to black and to white -/// - work as a text-color -/// - being noticeable on a typical map -/// - harmonize together while being different enough -/// (therefore, we cannot just use random rgb colors :) -const COLORS: [u32; 16] = [ - 0xe5_65_55, 0xf2_8c_48, 0x8e_85_ee, 0x76_c8_4d, 0x5b_b6_cc, 0x54_9c_dd, 0xd2_5c_99, 0xb3_78_00, - 0xf2_30_30, 0x39_b2_49, 0xbb_24_3b, 0x96_40_78, 0x66_87_4f, 0x30_8a_b9, 0x12_7e_d0, 0xbe_45_0c, -]; - -#[allow(clippy::indexing_slicing)] -pub(crate) fn dc_str_to_color(s: impl AsRef) -> u32 { - let str_lower = s.as_ref().to_lowercase(); - let mut checksum = 0; - let bytes = str_lower.as_bytes(); - for (i, byte) in bytes.iter().enumerate() { - checksum += (i + 1) * *byte as usize; - checksum %= 0x00ff_ffff; - } - let color_index = checksum % COLORS.len(); - - COLORS[color_index] -} - /* ****************************************************************************** * date/time tools ******************************************************************************/ diff --git a/src/lib.rs b/src/lib.rs index 132873637b..9e9793b9fe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,6 +77,7 @@ pub mod stock_str; mod token; #[macro_use] mod dehtml; +mod color; pub mod html; pub mod plaintext;