From d85ad57f1e7a2338e20176a4450732a3d52f2747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Mon, 1 Oct 2018 17:30:25 +0200 Subject: from __future__ import xep_0392.v0_5 Ahem. Thes updates poezio to use the HSLuv colour space to generate colours according to XEP-0392. This will become the standard in XEP-0392 v0.5. --- poezio/colors.py | 39 ++++++++------------------------------- setup.py | 2 +- 2 files changed, 9 insertions(+), 32 deletions(-) diff --git a/poezio/colors.py b/poezio/colors.py index 6bbbb12e..e8bbfa36 100644 --- a/poezio/colors.py +++ b/poezio/colors.py @@ -3,6 +3,8 @@ import curses import hashlib import math +import hsluv + Palette = Dict[float, int] # BT.601 (YCbCr) constants, see XEP-0392 @@ -33,13 +35,6 @@ def ncurses_color_to_rgb(color: int) -> Tuple[float, float, float]: return r / 5, g / 5, b / 5 -def rgb_to_ycbcr(r: float, g: float, b: float) -> Tuple[float, float, float]: - y = K_R * r + K_G * g + K_B * b - cr = (r - y) / (1 - K_R) / 2 - cb = (b - y) / (1 - K_B) / 2 - return y, cb, cr - - def generate_ccg_palette(curses_palette: List[int], reference_y: float) -> Palette: cbcr_palette = {} # type: Dict[float, Tuple[float, int]] @@ -48,8 +43,10 @@ def generate_ccg_palette(curses_palette: List[int], # drop grayscale if r == g == b: continue - y, cb, cr = rgb_to_ycbcr(r, g, b) - key = round(cbcr_to_angle(cb, cr), 2) + h, _, y = hsluv.rgb_to_hsluv((r, g, b)) + # this is to keep the code compatible with earlier versions of XEP-0392 + y = y / 100 + key = round(h) try: existing_y, *_ = cbcr_palette[key] except KeyError: @@ -68,35 +65,15 @@ def text_to_angle(text: str) -> float: hf = hashlib.sha1() hf.update(text.encode("utf-8")) hue = int.from_bytes(hf.digest()[:2], "little") - return hue / 65535 * math.pi * 2 - - -def angle_to_cbcr_edge(angle: float) -> Tuple[float, float]: - cr = math.sin(angle) - cb = math.cos(angle) - if abs(cr) > abs(cb): - factor = 0.5 / abs(cr) - else: - factor = 0.5 / abs(cb) - return cb * factor, cr * factor - - -def cbcr_to_angle(cb: float, cr: float) -> float: - magn = math.sqrt(cb**2 + cr**2) - if magn > 0: - cr /= magn - cb /= magn - return math.atan2(cr, cb) % (2 * math.pi) + return hue / 65535 * 360 def ccg_palette_lookup(palette: Palette, angle: float) -> int: # try quick lookup first try: - color = palette[round(angle, 2)] + return palette[round(angle)] except KeyError: pass - else: - return color best_metric = float("inf") best = None diff --git a/setup.py b/setup.py index 24eb26f8..574879d1 100755 --- a/setup.py +++ b/setup.py @@ -131,7 +131,7 @@ setup(name="poezio", ('share/metainfo/', ['data/io.poez.Poezio.appdata.xml'])] + find_doc('share/doc/poezio/source', 'source') + find_doc('share/doc/poezio/html', 'build/html')), - install_requires=['slixmpp>=1.3.0', 'aiodns', 'pyasn1_modules', 'pyasn1'], + install_requires=['slixmpp>=1.3.0', 'aiodns', 'pyasn1_modules', 'pyasn1', 'hsluv~=0.0.2'], extras_require={'OTR plugin': 'python-potr>=1.0', 'Screen autoaway plugin': 'pyinotify==0.9.4', 'Avoiding cython': 'cffi'}) -- cgit v1.2.3 From 348aabc290850ef6c1b7f5a921b49272cbee85a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Mon, 1 Oct 2018 21:08:48 +0200 Subject: XEP-0392/CCG: prefer JID over nickname --- poezio/user.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/poezio/user.py b/poezio/user.py index 655eb0de..5d5d43c1 100644 --- a/poezio/user.py +++ b/poezio/user.py @@ -59,7 +59,11 @@ class User: theme = get_theme() if theme.ccg_palette: # use XEP-0392 CCG - fg_color = colors.ccg_text_to_color(theme.ccg_palette, self.nick) + if self.jid and self.jid.domain: + input_ = str(self.jid.bare) + else: + input_ = self.nick + fg_color = colors.ccg_text_to_color(theme.ccg_palette, input_) self.color = fg_color, -1 else: mod = len(theme.LIST_COLOR_NICKNAMES) -- cgit v1.2.3 From 2b70b57f32d9ac83ed2ec29145f4422779b010ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Mon, 15 Oct 2018 21:56:34 +0200 Subject: JID.bare is always a str --- poezio/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poezio/user.py b/poezio/user.py index 5d5d43c1..43832917 100644 --- a/poezio/user.py +++ b/poezio/user.py @@ -60,7 +60,7 @@ class User: if theme.ccg_palette: # use XEP-0392 CCG if self.jid and self.jid.domain: - input_ = str(self.jid.bare) + input_ = self.jid.bare else: input_ = self.nick fg_color = colors.ccg_text_to_color(theme.ccg_palette, input_) -- cgit v1.2.3 From 07606b0cd252fe2836120ed43abd169c9470ad0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Thu, 14 Feb 2019 16:35:28 +0100 Subject: Embed hsluv See: https://lab.louiz.org/poezio/poezio/merge_requests/13#note_7453 --- poezio/colors.py | 2 +- poezio/hsluv.py | 360 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 poezio/hsluv.py diff --git a/poezio/colors.py b/poezio/colors.py index e8bbfa36..c1019145 100644 --- a/poezio/colors.py +++ b/poezio/colors.py @@ -3,7 +3,7 @@ import curses import hashlib import math -import hsluv +from . import hsluv Palette = Dict[float, int] diff --git a/poezio/hsluv.py b/poezio/hsluv.py new file mode 100644 index 00000000..7dce5061 --- /dev/null +++ b/poezio/hsluv.py @@ -0,0 +1,360 @@ +# This file was taken from https://github.com/hsluv/hsluv-python +# +# Copyright (c) 2015 Alexei Boronine +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +""" This module is generated by transpiling Haxe into Python and cleaning +the resulting code by hand, e.g. removing unused Haxe classes. To try it +yourself, clone https://github.com/hsluv/hsluv and run: + + haxe -cp haxe/src hsluv.Hsluv -python hsluv.py +""" + +import math + + + +__version__ = '0.0.2' + +m = [[3.240969941904521, -1.537383177570093, -0.498610760293], + [-0.96924363628087, 1.87596750150772, 0.041555057407175], + [0.055630079696993, -0.20397695888897, 1.056971514242878]] +minv = [[0.41239079926595, 0.35758433938387, 0.18048078840183], + [0.21263900587151, 0.71516867876775, 0.072192315360733], + [0.019330818715591, 0.11919477979462, 0.95053215224966]] +refY = 1.0 +refU = 0.19783000664283 +refV = 0.46831999493879 +kappa = 903.2962962 +epsilon = 0.0088564516 +hex_chars = "0123456789abcdef" + + +def _distance_line_from_origin(line): + v = math.pow(line['slope'], 2) + 1 + return math.fabs(line['intercept']) / math.sqrt(v) + + +def _length_of_ray_until_intersect(theta, line): + return line['intercept'] / (math.sin(theta) - line['slope'] * math.cos(theta)) + + +def _get_bounds(l): + result = [] + sub1 = math.pow(l + 16, 3) / 1560896 + if sub1 > epsilon: + sub2 = sub1 + else: + sub2 = l / kappa + _g = 0 + while _g < 3: + c = _g + _g = _g + 1 + m1 = m[c][0] + m2 = m[c][1] + m3 = m[c][2] + _g1 = 0 + while _g1 < 2: + t = _g1 + _g1 = _g1 + 1 + top1 = (284517 * m1 - 94839 * m3) * sub2 + top2 = (838422 * m3 + 769860 * m2 + 731718 * m1) * l * sub2 - (769860 * t) * l + bottom = (632260 * m3 - 126452 * m2) * sub2 + 126452 * t + result.append({'slope': top1 / bottom, 'intercept': top2 / bottom}) + return result + + +def _max_safe_chroma_for_l(l): + bounds = _get_bounds(l) + _hx_min = 1.7976931348623157e+308 + _g = 0 + while _g < 2: + i = _g + _g = _g + 1 + length = _distance_line_from_origin(bounds[i]) + if math.isnan(_hx_min): + _hx_min = _hx_min + elif math.isnan(length): + _hx_min = length + else: + _hx_min = min(_hx_min, length) + return _hx_min + + +def _max_chroma_for_lh(l, h): + hrad = h / 360 * math.pi * 2 + bounds = _get_bounds(l) + _hx_min = 1.7976931348623157e+308 + _g = 0 + while _g < len(bounds): + bound = bounds[_g] + _g = (_g + 1) + length = _length_of_ray_until_intersect(hrad, bound) + if length >= 0: + if math.isnan(_hx_min): + _hx_min = _hx_min + elif math.isnan(length): + _hx_min = length + else: + _hx_min = min(_hx_min, length) + return _hx_min + + +def _dot_product(a, b): + sum = 0 + _g1 = 0 + _g = len(a) + while _g1 < _g: + i = _g1 + _g1 = _g1 + 1 + sum += a[i] * b[i] + return sum + + +def _from_linear(c): + if c <= 0.0031308: + return 12.92 * c + else: + return 1.055 * math.pow(c, 0.416666666666666685) - 0.055 + + +def _to_linear(c): + if c > 0.04045: + return math.pow((c + 0.055) / 1.055, 2.4) + else: + return c / 12.92 + + +def xyz_to_rgb(_hx_tuple): + return [ + _from_linear(_dot_product(m[0], _hx_tuple)), + _from_linear(_dot_product(m[1], _hx_tuple)), + _from_linear(_dot_product(m[2], _hx_tuple))] + + +def rgb_to_xyz(_hx_tuple): + rgbl = [_to_linear(_hx_tuple[0]), + _to_linear(_hx_tuple[1]), + _to_linear(_hx_tuple[2])] + return [_dot_product(minv[0], rgbl), + _dot_product(minv[1], rgbl), + _dot_product(minv[2], rgbl)] + + +def _y_to_l(y): + if y <= epsilon: + return y / refY * kappa + else: + return 116 * math.pow(y / refY, 0.333333333333333315) - 16 + + +def _l_to_y(l): + if l <= 8: + return refY * l / kappa + else: + return refY * math.pow((l + 16) / 116, 3) + + +def xyz_to_luv(_hx_tuple): + x = float(_hx_tuple[0]) + y = float(_hx_tuple[1]) + z = float(_hx_tuple[2]) + divider = x + 15 * y + 3 * z + var_u = 4 * x + var_v = 9 * y + if divider != 0: + var_u = var_u / divider + var_v = var_v / divider + else: + var_u = float("nan") + var_v = float("nan") + l = _y_to_l(y) + if l == 0: + return [0, 0, 0] + u = 13 * l * (var_u - refU) + v = 13 * l * (var_v - refV) + return [l, u, v] + + +def luv_to_xyz(_hx_tuple): + l = float(_hx_tuple[0]) + u = float(_hx_tuple[1]) + v = float(_hx_tuple[2]) + if l == 0: + return [0, 0, 0] + var_u = u / (13 * l) + refU + var_v = v / (13 * l) + refV + y = _l_to_y(l) + x = 0 - ((9 * y * var_u) / (((var_u - 4) * var_v) - var_u * var_v)) + z = (((9 * y) - (15 * var_v * y)) - (var_v * x)) / (3 * var_v) + return [x, y, z] + + +def luv_to_lch(_hx_tuple): + l = float(_hx_tuple[0]) + u = float(_hx_tuple[1]) + v = float(_hx_tuple[2]) + _v = (u * u) + (v * v) + if _v < 0: + c = float("nan") + else: + c = math.sqrt(_v) + if c < 0.00000001: + h = 0 + else: + hrad = math.atan2(v, u) + h = hrad * 180.0 / 3.1415926535897932 + if h < 0: + h = 360 + h + return [l, c, h] + + +def lch_to_luv(_hx_tuple): + l = float(_hx_tuple[0]) + c = float(_hx_tuple[1]) + h = float(_hx_tuple[2]) + hrad = h / 360.0 * 2 * math.pi + u = math.cos(hrad) * c + v = math.sin(hrad) * c + return [l, u, v] + + +def hsluv_to_lch(_hx_tuple): + h = float(_hx_tuple[0]) + s = float(_hx_tuple[1]) + l = float(_hx_tuple[2]) + if l > 99.9999999: + return [100, 0, h] + if l < 0.00000001: + return [0, 0, h] + _hx_max = _max_chroma_for_lh(l, h) + c = _hx_max / 100 * s + return [l, c, h] + + +def lch_to_hsluv(_hx_tuple): + l = float(_hx_tuple[0]) + c = float(_hx_tuple[1]) + h = float(_hx_tuple[2]) + if l > 99.9999999: + return [h, 0, 100] + if l < 0.00000001: + return [h, 0, 0] + _hx_max = _max_chroma_for_lh(l, h) + s = c / _hx_max * 100 + return [h, s, l] + + +def hpluv_to_lch(_hx_tuple): + h = float(_hx_tuple[0]) + s = float(_hx_tuple[1]) + l = float(_hx_tuple[2]) + if l > 99.9999999: + return [100, 0, h] + if l < 0.00000001: + return [0, 0, h] + _hx_max = _max_safe_chroma_for_l(l) + c = _hx_max / 100 * s + return [l, c, h] + + +def lch_to_hpluv(_hx_tuple): + l = float(_hx_tuple[0]) + c = float(_hx_tuple[1]) + h = float(_hx_tuple[2]) + if l > 99.9999999: + return [h, 0, 100] + if l < 0.00000001: + return [h, 0, 0] + _hx_max = _max_safe_chroma_for_l(l) + s = c / _hx_max * 100 + return [h, s, l] + + +def rgb_to_hex(_hx_tuple): + h = "#" + _g = 0 + while _g < 3: + i = _g + _g = _g + 1 + chan = float(_hx_tuple[i]) + c = math.floor(chan * 255 + 0.5) + digit2 = int(c % 16) + digit1 = int((c - digit2) / 16) + + h += hex_chars[digit1] + hex_chars[digit2] + return h + + +def hex_to_rgb(hex): + hex = hex.lower() + ret = [] + _g = 0 + while _g < 3: + i = _g + _g = _g + 1 + index = i * 2 + 1 + _hx_str = hex[index] + digit1 = hex_chars.find(_hx_str) + index1 = i * 2 + 2 + str1 = hex[index1] + digit2 = hex_chars.find(str1) + n = digit1 * 16 + digit2 + ret.append(n / 255.0) + return ret + + +def lch_to_rgb(_hx_tuple): + return xyz_to_rgb(luv_to_xyz(lch_to_luv(_hx_tuple))) + + +def rgb_to_lch(_hx_tuple): + return luv_to_lch(xyz_to_luv(rgb_to_xyz(_hx_tuple))) + + +def hsluv_to_rgb(_hx_tuple): + return lch_to_rgb(hsluv_to_lch(_hx_tuple)) + + +def rgb_to_hsluv(_hx_tuple): + return lch_to_hsluv(rgb_to_lch(_hx_tuple)) + + +def hpluv_to_rgb(_hx_tuple): + return lch_to_rgb(hpluv_to_lch(_hx_tuple)) + + +def rgb_to_hpluv(_hx_tuple): + return lch_to_hpluv(rgb_to_lch(_hx_tuple)) + + +def hsluv_to_hex(_hx_tuple): + return rgb_to_hex(hsluv_to_rgb(_hx_tuple)) + + +def hpluv_to_hex(_hx_tuple): + return rgb_to_hex(hpluv_to_rgb(_hx_tuple)) + + +def hex_to_hsluv(s): + return rgb_to_hsluv(hex_to_rgb(s)) + + +def hex_to_hpluv(s): + return rgb_to_hpluv(hex_to_rgb(s)) -- cgit v1.2.3 From 322f02f0f4ff7b2fc622e0a08f0363d806dc0bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Thu, 14 Feb 2019 17:24:23 +0100 Subject: Remove now-obsolete hsluv dependency --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 574879d1..24eb26f8 100755 --- a/setup.py +++ b/setup.py @@ -131,7 +131,7 @@ setup(name="poezio", ('share/metainfo/', ['data/io.poez.Poezio.appdata.xml'])] + find_doc('share/doc/poezio/source', 'source') + find_doc('share/doc/poezio/html', 'build/html')), - install_requires=['slixmpp>=1.3.0', 'aiodns', 'pyasn1_modules', 'pyasn1', 'hsluv~=0.0.2'], + install_requires=['slixmpp>=1.3.0', 'aiodns', 'pyasn1_modules', 'pyasn1'], extras_require={'OTR plugin': 'python-potr>=1.0', 'Screen autoaway plugin': 'pyinotify==0.9.4', 'Avoiding cython': 'cffi'}) -- cgit v1.2.3