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
This commit is contained in:
link2xt
2021-02-12 21:36:04 +03:00
committed by link2xt
parent 982dc53dc1
commit a2c3233c19
7 changed files with 67 additions and 35 deletions

12
Cargo.lock generated
View File

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

View File

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

View File

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

46
src/color.rs Normal file
View File

@@ -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<str>) -> 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<str>) -> 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);
}
}

View File

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

View File

@@ -48,31 +48,6 @@ pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
}
}
/// 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<str>) -> 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
******************************************************************************/

View File

@@ -77,6 +77,7 @@ pub mod stock_str;
mod token;
#[macro_use]
mod dehtml;
mod color;
pub mod html;
pub mod plaintext;