Source code for treevalue.utils.color

import colorsys
import math
import re
from typing import Optional, Union, Tuple

from hbutils.reflection import post_process, raising, freduce, dynamic_call, warning_


def _round_mapper(min_: float, max_: float):
    min_, max_ = min(min_, max_), max(min_, max_)
    round_ = max_ - min_

    def _func(v):
        if v < min_:
            v += math.ceil((min_ - v) / round_) * round_
        if v > max_:
            v -= math.ceil((v - max_) / round_) * round_

        return v

    return _func


def _range_mapper(min_: Optional[float], max_: Optional[float], warning=None):
    if min_ is not None and max_ is not None:
        min_, max_ = min(min_, max_), max(min_, max_)
    warning = dynamic_call(warning_(warning if warning is not None else lambda: None))

    def _func(v):
        if max_ is not None and v > max_:
            warning(v, min_, max_)
            return max_
        elif min_ is not None and v < min_:
            warning(v, min_, max_)
            return min_
        else:
            return v

    return _func


class GetSetProxy:
    def __init__(self, getter, setter=None):
        self.__getter = getter
        self.__setter = setter or raising(lambda x: NotImplementedError)

    def set(self, value):
        return self.__setter(value)

    def get(self):
        return self.__getter()


_r_mapper = _range_mapper(0.0, 1.0, lambda v, min_, max_: Warning(
    'Red value should be no less than %.3d and no more than %.3d, but %.3d found.' % (min_, max_, v)))
_g_mapper = _range_mapper(0.0, 1.0, lambda v, min_, max_: Warning(
    'Green value should be no less than %.3d and no more than %.3d, but %.3d found.' % (min_, max_, v)))
_b_mapper = _range_mapper(0.0, 1.0, lambda v, min_, max_: Warning(
    'Blue value should be no less than %.3d and no more than %.3d, but %.3d found.' % (min_, max_, v)))
_a_mapper = _range_mapper(0.0, 1.0, lambda v, min_, max_: Warning(
    'Alpha value should be no less than %.3d and no more than %.3d, but %.3d found.' % (min_, max_, v)))


class RGBColorProxy:
    def __init__(self, this: 'Color', r: GetSetProxy, g: GetSetProxy, b: GetSetProxy):
        self.__this = this
        self.__rp = r
        self.__gp = g
        self.__bp = b

    @property
    def red(self) -> float:
        return self.__rp.get()

    @red.setter
    def red(self, new: float):
        self.__rp.set(new)

    @property
    def green(self) -> float:
        return self.__gp.get()

    @green.setter
    def green(self, new: float):
        self.__gp.set(new)

    @property
    def blue(self) -> float:
        return self.__bp.get()

    @blue.setter
    def blue(self, new: float):
        self.__bp.set(new)

    def __iter__(self):
        yield self.red
        yield self.green
        yield self.blue

    def __repr__(self):
        return '<{cls} red: {red}, green: {green}, blue: {blue}>'.format(
            cls=self.__class__.__name__,
            red='%.3f' % (self.red,),
            green='%.3f' % (self.green,),
            blue='%.3f' % (self.blue,),
        )


_hsv_h_mapper = _round_mapper(0.0, 1.0)
_hsv_s_mapper = _range_mapper(0.0, 1.0, lambda v, min_, max_: Warning(
    'Saturation value should be no less than %.3d and no more than %.3d, but %.3d found.' % (min_, max_, v)))
_hsv_v_mapper = _range_mapper(0.0, 1.0, lambda v, min_, max_: Warning(
    'Brightness(value) value should be no less than %.3d and no more than %.3d, but %.3d found.' % (min_, max_, v)))


class HSVColorProxy:
    def __init__(self, this: 'Color', h: GetSetProxy, s: GetSetProxy, v: GetSetProxy):
        this.__this = this
        self.__hp = h
        self.__sp = s
        self.__vp = v

    @property
    def hue(self) -> float:
        return self.__hp.get()

    @hue.setter
    def hue(self, new: float):
        self.__hp.set(new)

    @property
    def saturation(self) -> float:
        return self.__sp.get()

    @saturation.setter
    def saturation(self, new: float):
        self.__sp.set(new)

    @property
    def value(self) -> float:
        return self.__vp.get()

    @value.setter
    def value(self, new: float):
        self.__vp.set(new)

    def __iter__(self):
        yield self.hue
        yield self.saturation
        yield self.value

    def __repr__(self):
        return '<{cls} hue: {hue}, saturation: {saturation}, value: {value}>'.format(
            cls=self.__class__.__name__,
            hue='%.3f' % (self.hue,),
            saturation='%.3f' % (self.saturation,),
            value='%.3f' % (self.value,),
        )


_hls_h_mapper = _round_mapper(0.0, 1.0)
_hls_l_mapper = _range_mapper(0.0, 1.0, lambda v, min_, max_: Warning(
    'Lightness value should be no less than %.3d and no more than %.3d, but %.3d found.' % (min_, max_, v)))
_hls_s_mapper = _range_mapper(0.0, 1.0, lambda v, min_, max_: Warning(
    'Saturation value should be no less than %.3d and no more than %.3d, but %.3d found.' % (min_, max_, v)))


class HLSColorProxy:
    def __init__(self, this: 'Color', h: GetSetProxy, l: GetSetProxy, s: GetSetProxy):
        this.__this = this
        self.__hp = h
        self.__lp = l
        self.__sp = s

    @property
    def hue(self) -> float:
        return self.__hp.get()

    @hue.setter
    def hue(self, new: float):
        self.__hp.set(new)

    @property
    def lightness(self) -> float:
        return self.__lp.get()

    @lightness.setter
    def lightness(self, new: float):
        self.__lp.set(new)

    @property
    def saturation(self) -> float:
        return self.__sp.get()

    @saturation.setter
    def saturation(self, new: float):
        self.__sp.set(new)

    def __iter__(self):
        yield self.hue
        yield self.lightness
        yield self.saturation

    def __repr__(self):
        return '<{cls} hue: {hue}, lightness: {lightness}, saturation: {saturation}>'.format(
            cls=self.__class__.__name__,
            hue='%.3f' % (self.hue,),
            lightness='%.3f' % (self.lightness,),
            saturation='%.3f' % (self.saturation,),
        )


_ratio_to_255 = lambda x: int(round(x * 255))
_ratio_to_hex = post_process(lambda x: '%02x' % (x,))(_ratio_to_255)
_hex_to_255 = lambda x: int(x, base=16) if x is not None else None
_hex_to_ratio = post_process(lambda x: x / 255.0 if x is not None else None)(_hex_to_255)

_RGB_COLOR_PATTERN = re.compile(r'^#?([a-zA-Z\d]{2})([a-zA-Z\d]{2})([a-zA-Z\d]{2})([a-zA-Z\d]{2}|)$')


@freduce(init=None)
def _ratio_or(a, b):
    return b if a is None else a


[docs]class Color: """ Overview: Color utility object. """
[docs] def __init__(self, c: Union[str, Tuple[float, float, float]], alpha: Optional[float] = None): """ Overview: Constructor of ``Color``. Arguments: - c (:obj:`Union[str, Tuple[float, float, float]]`): Color value, can be hex string value \ or tuple rgb value. - alpha: (:obj:`Optional[float]`): Alpha value of color, \ default is `None` which means no alpha value. """ if isinstance(c, tuple): self.__r, self.__g, self.__b = _r_mapper(c[0]), _g_mapper(c[1]), _b_mapper(c[2]) self.__alpha = _a_mapper(alpha) if alpha is not None else None elif isinstance(c, str): _finding = _RGB_COLOR_PATTERN.findall(c) if _finding: _first = _finding[0] rs, gs, bs, as_ = _first as_ = None if not as_ else as_ r, g, b, a = map(_hex_to_ratio, (rs, gs, bs, as_)) if alpha is not None: a = a * alpha if a is not None else alpha self.__init__((r, g, b), a) else: raise ValueError("Invalid string color, matching of pattern {pattern} " "expected but {actual} found.".format(pattern=repr(_RGB_COLOR_PATTERN.pattern), actual=repr(c), )) else: raise TypeError('Unknown color value - {c}.'.format(c=repr(c)))
@property def alpha(self) -> Optional[float]: """ Overview: Get value of alpha. Returns: - alpha (:obj:`Optional[float]`): Alpha value. """ return self.__alpha @alpha.setter def alpha(self, new: Optional[float]): """ Overview: Set value of alpha. Arguments: - new (:obj:`Optional[float]`): New value of alpha. """ if new is not None: new = _a_mapper(new) self.__alpha = new def __get_rgb(self): return self.__r, self.__g, self.__b def __set_rgb(self, r=None, g=None, b=None): self.__r, self.__g, self.__b = map(lambda args: _ratio_or(*args), zip((r, g, b), self.__get_rgb())) @property def rgb(self) -> RGBColorProxy: """ Overview: Get rgb color system based color proxy. Returns: - proxy (:obj:`RGBColorProxy`): Rgb color proxy. """ return RGBColorProxy( self, GetSetProxy( lambda: self.__r, lambda x: self.__set_rgb(r=_r_mapper(x)), ), GetSetProxy( lambda: self.__g, lambda x: self.__set_rgb(g=_g_mapper(x)), ), GetSetProxy( lambda: self.__b, lambda x: self.__set_rgb(b=_b_mapper(x)), ), ) def __get_hsv(self): return colorsys.rgb_to_hsv(self.__r, self.__g, self.__b) def __set_hsv(self, h=None, s=None, v=None): h, s, v = map(lambda args: _ratio_or(*args), zip((h, s, v), self.__get_hsv())) self.__r, self.__g, self.__b = colorsys.hsv_to_rgb(h, s, v) @property def hsv(self) -> HSVColorProxy: """ Overview: Get hsv color system based color proxy. Returns: - proxy (:obj:`HSVColorProxy`): Hsv color proxy. """ return HSVColorProxy( self, GetSetProxy( lambda: self.__get_hsv()[0], lambda x: self.__set_hsv(h=_hsv_h_mapper(x)), ), GetSetProxy( lambda: self.__get_hsv()[1], lambda x: self.__set_hsv(s=_hsv_s_mapper(x)), ), GetSetProxy( lambda: self.__get_hsv()[2], lambda x: self.__set_hsv(v=_hsv_v_mapper(x)), ), ) def __get_hls(self): return colorsys.rgb_to_hls(self.__r, self.__g, self.__b) def __set_hls(self, h=None, l_=None, s=None): h, l, s = map(lambda args: _ratio_or(*args), zip((h, l_, s), self.__get_hls())) self.__r, self.__g, self.__b = colorsys.hls_to_rgb(h, l, s) @property def hls(self) -> HLSColorProxy: """ Overview: Get hls color system based color proxy. Returns: - proxy (:obj:`HLSColorProxy`): Hls color proxy. """ return HLSColorProxy( self, GetSetProxy( lambda: self.__get_hls()[0], lambda x: self.__set_hls(h=_hls_h_mapper(x)), ), GetSetProxy( lambda: self.__get_hls()[1], lambda x: self.__set_hls(l_=_hls_l_mapper(x)), ), GetSetProxy( lambda: self.__get_hls()[2], lambda x: self.__set_hls(s=_hls_s_mapper(x)), ), ) def __get_hex(self, include_alpha: bool): rs, gs, bs = _ratio_to_hex(self.__r), _ratio_to_hex(self.__g), _ratio_to_hex(self.__b) as_ = _ratio_to_hex(self.__alpha) if self.__alpha is not None and include_alpha else '' return '#' + rs + gs + bs + as_
[docs] def __repr__(self): if self.__alpha is not None: return '<{cls} {hex}, alpha: {alpha}>'.format( cls=self.__class__.__name__, hex=self.__get_hex(False), alpha='%.3f' % (self.__alpha,), ) else: return '<{cls} {hex}>'.format( cls=self.__class__.__name__, hex=self.__get_hex(False), )
[docs] def __str__(self): return self.__get_hex(True)
[docs] def __getstate__(self) -> Tuple[float, float, float, Optional[float]]: """ Overview: Dump color as pickle object. Returns: - info (:obj:`Tuple[float, float, float, Optional[float]]`): Dumped data object. """ return self.__r, self.__g, self.__b, self.__alpha
[docs] def __setstate__(self, v: Tuple[float, float, float, Optional[float]]): """ Overview: Load color from pickle object. Args: - v (:obj:`Tuple[float, float, float, Optional[float]]`): Dumped data object. """ self.__r, self.__g, self.__b, self.__alpha = v
[docs] def __hash__(self): """ Overview: Get hash value of current object. Returns: - hash (:obj:`int`): Hash value of current color. """ return hash(self.__getstate__())
[docs] def __eq__(self, other): """ Overview: Get equality between colors. Arguments: - other: Another object. Returns: - equal (:obj:`bool`): Equal or not. """ if other is self: return True elif type(other) == type(self): return other.__getstate__() == self.__getstate__() else: return False
[docs] @classmethod def from_hsv(cls, h, s, v, alpha=None) -> 'Color': """ Overview: Load color from hsv system. Arguments: - h (:obj:`float`): Hue value, should be a float value in :math:`\\left[0, 1\\right)`. - s (:obj:`float`): Saturation value, should be a float value in :math:`\\left[0, 1\\right]`. - v (:obj:`float`): Brightness (value) value, should be a float value \ in :math:`\\left[0, 1\\right]`. - alpha (:obj:`Optional[float]`): Alpha value, should be a float value \ in :math:`\\left[0, 1\\right]`, default is None which means no alpha value is used. Returns: - color (:obj:`Color`): Color object. """ return cls(colorsys.hsv_to_rgb(h, s, v), alpha)
[docs] @classmethod def from_hls(cls, h: float, l: float, s: float, alpha: Optional[float] = None) -> 'Color': """ Overview: Load color from hls system. Arguments: - h (:obj:`float`): Hue value, should be a float value in :math:`\\left[0, 1\\right)`. - l (:obj:`float`): Lightness value, should be a float value in :math:`\\left[0, 1\\right]`. - s (:obj:`float`): Saturation value, should be a float value in :math:`\\left[0, 1\\right]`. - alpha (:obj:`Optional[float]`): Alpha value, should be a float value \ in :math:`\\left[0, 1\\right]`, default is None which means no alpha value is used. Returns: - color (:obj:`Color`): Color object. """ return cls(colorsys.hls_to_rgb(h, l, s), alpha)