From a2c3233c19019242745cea4f931f3435bf0724f7 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 12 Feb 2021 21:36:04 +0300 Subject: [PATCH] Implement Consistent Color Generation (XEP-0392) This should make colors used by Delta Chat for emails similar to colors used by XMPP clients implementing the same specification[1], such as Conversations and Snikket. Previously Delta Chat used only hardcoded 16 colors, so this change should also increase the number of colors and make it easier to distinguish different contacts. [1] https://xmpp.org/extensions/xep-0392.html --- Cargo.lock | 12 ++++++++++-- Cargo.toml | 2 ++ src/chat.rs | 9 +++++---- src/color.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++++ src/contact.rs | 7 +++---- src/dc_tools.rs | 25 ------------------------- src/lib.rs | 1 + 7 files changed, 67 insertions(+), 35 deletions(-) create mode 100644 src/color.rs diff --git a/Cargo.lock b/Cargo.lock index 224a20b83..9272a44eb 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 0e0a95d48..ef7234c62 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 0dc729180..83963aa83 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 000000000..457b4c25d --- /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 764684898..91520fb8a 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 ce182935a..d1fb38670 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 132873637..9e9793b9f 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;