Source code for pyrocko.color

# https://pyrocko.org - GPLv3
#
# The Pyrocko Developers, 21st Century
# ---|P------/S----------~Lg----------

'''
Color utilities and built-in color palettes for a consistent look.

.. _color:

Color formats
.............

.. list-table::
    :widths: 15 85

    * - ``RGB``, ``RGBA``
      - Integer values for red, green blue, alpha, in the range ``[0, 255]``.::

            Color('RGBA(255, 255, 255, 255)')  # from string
            Color('RGB(255, 255, 255)')
            Color(255, 255, 255, 255)          # from values
            Color(255, 255, 255)

    * - ``rgb``, ``rgba``
      - Floating point values in the range ``[0.0, 1.0]``.::

            Color('rgba(1, 1, 1, 1)')   # from string
            Color('rgb(1, 1, 1)')
            Color(1.0, 1.0, 1.0, 1.0)   # from values
            Color(1.0, 1.0, 1.0)

    * - ``hex``
      - Hexadecimal value with 3, 4, 6 or 8 digits.::

            Color('#fff')
            Color('#ffff')
            Color('#ffffff')
            Color('#ffffffff')

    * - ``name``
      - See :py:data:`g_named_colors` for a complete list of
        available colors.::

            Color('white')
            Color('butter1')
            Color('skyblue2')


.. py:data:: g_named_colors

    Dict mapping color names to :py:class:`Color` objects.
'''


import re

from pyrocko.guts import Object, Dict, SObject, Float, String, TBase, \
    ValidationError, load_string


guts_prefix = 'pf'


[docs]class ColorError(ValueError): ''' Raised for invalid color specifications and conversions. ''' pass
[docs]class InvalidColorString(ColorError): ''' Raised for invalid color string definitions. ''' def __init__(self, s): ColorError.__init__(self, 'Invalid color string: %s' % s)
g_pattern_hex = re.compile( r'^#([0-9a-fA-F]{1,2}){3,4}$') g_pattern_rgb = re.compile( r'^(RGBA?|rgba?)\(([^,]+),([^,]+),([^,]+)(,([^)]+))?\)$') g_tango_colors = { 'butter1': (252, 233, 79), 'butter2': (237, 212, 0), 'butter3': (196, 160, 0), 'chameleon1': (138, 226, 52), 'chameleon2': (115, 210, 22), 'chameleon3': (78, 154, 6), 'orange1': (252, 175, 62), 'orange2': (245, 121, 0), 'orange3': (206, 92, 0), 'skyblue1': (114, 159, 207), 'skyblue2': (52, 101, 164), 'skyblue3': (32, 74, 135), 'plum1': (173, 127, 168), 'plum2': (117, 80, 123), 'plum3': (92, 53, 102), 'chocolate1': (233, 185, 110), 'chocolate2': (193, 125, 17), 'chocolate3': (143, 89, 2), 'scarletred1': (239, 41, 41), 'scarletred2': (204, 0, 0), 'scarletred3': (164, 0, 0), 'aluminium1': (238, 238, 236), 'aluminium2': (211, 215, 207), 'aluminium3': (186, 189, 182), 'aluminium4': (136, 138, 133), 'aluminium5': (85, 87, 83), 'aluminium6': (46, 52, 54)} g_standard_colors = { 'white': (255, 255, 255), 'black': (0, 0, 0), 'red': (255, 0, 0), 'green': (0, 255, 0), 'blue': (0, 0, 255)} g_named_colors = {} g_named_colors.update(g_tango_colors) g_named_colors.update(g_standard_colors)
[docs]def parse_color(s): ''' Translate color string into :ref:`rgba tuple <color>`. :param s: :ref:`Color definition <color>`. :type s: str :return: Color value in ``rgba`` form. :rtype: :py:class:`tuple` of 4 :py:class:`float` ''' orig_s = s rgba = None if s in g_named_colors: rgba = tuple(to_float_1(x) for x in g_named_colors[s]) + (1.0,) else: m = g_pattern_hex.match(s) if m: s = s[1:] if len(s) == 3: s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2] if len(s) == 4: s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2] + s[3] + s[3] if len(s) == 6: s = s + 'FF' if len(s) == 8: try: rgba = tuple( int(s[i*2:i*2+2], base=16) / 255. for i in range(4)) except ValueError: raise InvalidColorString(orig_s) else: raise InvalidColorString(orig_s) m = g_pattern_rgb.match(s) if m: rgb_mode = m.group(1) if rgb_mode.startswith('rgb'): typ = float else: def typ(x): return int(x) / 255. try: rgba = ( typ(m.group(2)), typ(m.group(3)), typ(m.group(4)), typ(m.group(6)) if m.group(6) else 1.0) except ValueError: raise InvalidColorString(orig_s) if rgba is None: raise InvalidColorString(orig_s) if any(x < 0.0 or x > 1.0 for x in rgba): raise InvalidColorString(orig_s) return rgba
[docs]def to_int_255(f): ''' Convert floating point to integer color component Convert a floating point color component (range [0.0, 1.0]) to an integer color component (range [0, 255]) :param f: rgba floating point color component :type f: float :return: RGBA integer color component :rtype: int ''' if not (0.0 <= f <= 1.0): raise ColorError( 'Floating point color component must be in the range [0.0, 1.0]') return int(round(f * 255.))
[docs]def to_float_1(i): ''' Convert integer to floating point color component Convert an integer color component (range [0, 255]) to a floating point color component (range [0.0, 1.0]) :param i: RGBA integer color component :type i: int :return: rgba floating point color component :rtype: float ''' if not (0 <= i <= 255): raise ColorError( 'Integer color component must be in the range [0, 255]') return i / 255.
[docs]def simplify_hex(s): ''' Simplifiy a hex color code if possible E.g.: - #ffffffff -> #fff - #11aabbff -> #1ab :param s: hex color string :type s: str :return: simplified hex color string :rtype: str ''' if s[1] == s[2] and s[3] == s[4] and s[5] == s[6] \ and (len(s) == 9 and s[7] == s[8]): s = s[0] + s[1] + s[3] + s[5] + (s[7] if len(s) == 9 else '') if len(s) == 9 and s[-2:].lower() == 'ff': s = s[:7] elif len(s) == 5 and s[-1:].lower() == 'f': s = s[:4] return s
[docs]class Component(Float): class __T(TBase): def validate_extra(self, x): if not (0.0 <= x <= 1.0): raise ValidationError( 'Color component must be in the range [0.0, 1.0]')
[docs]class Color(SObject): ''' Color with red, green, blue and alpha values. The color can be specified as a :ref:`color string <color>` or by giving the component values in :py:class:`int` or :py:class:`float` format. Examples:: Color('black') Color('RGBA(255, 255, 255, 255)') Color('RGB(255, 255, 255)') Color('rgba(1.0, 1.0, 1.0, 1.0')') Color('rgb(1.0, 1.0, 1.0')') Color(255, 255, 255, 255) Color(255, 255, 255) Color(1.0, 1.0, 1.0, 1.0) Color(1.0, 1.0, 1.0) Color(r=1.0, g=1.0, b=1.0, a=1.0) The internal representation is ``rgba``. ''' name__ = String.T(optional=True) r__ = Component.T( default=0.0, help='Red component ``[0., 1.]``.') g__ = Component.T( default=0.0, help='Green component ``[0., 1.]``.') b__ = Component.T( default=0.0, help='Blue component ``[0., 1.]``.') a__ = Component.T( default=1.0, help='Alpha (opacity) component ``[0., 1.]``.') def __init__(self, *args, **kwargs): if len(args) == 1: SObject.__init__(self, init_props=False) self.name = args[0] elif len(args) in (3, 4): SObject.__init__(self, init_props=False) if all(isinstance(x, int) for x in args): if len(args) == 3: args = args + (255,) self.RGBA = args elif all(isinstance(x, float) for x in args): if len(args) == 3: args = args + (1.0,) self.rgba = args else: SObject.__init__(self, init_props=False) self.name__ = kwargs.get('name', None) self.r__ = kwargs.get('r', 0.0) self.g__ = kwargs.get('g', 0.0) self.b__ = kwargs.get('b', 0.0) self.a__ = kwargs.get('a', 1.0) def __eq__(self, other): return self.name__ == other.name__ \ and self.r__ == other.r__ \ and self.g__ == other.g__ \ and self.b__ == other.b__ \ and self.a__ == other.a__ @property def name(self): return self.name__ or '' @name.setter def name(self, name): self.r__, self.g__, self.b__, self.a__ = parse_color(name) self.name__ = name @property def r(self): return self.r__ @r.setter def r(self, r): self.name__ = None self.r__ = r @property def g(self): return self.g__ @g.setter def g(self, g): self.name__ = None self.g__ = g @property def b(self): return self.b__ @b.setter def b(self, b): self.name__ = None self.b__ = b @property def a(self): return self.a__ @a.setter def a(self, a): self.name__ = None self.a__ = a @property def rgb(self): ''' Red, green and blue floating point color components. ''' return self.r__, self.g__, self.b__ @rgb.setter def rgb(self, rgb): self.r__, self.g__, self.b__ = rgb self.name__ = None @property def rgba(self): ''' Red, green, blue and alpha floating point color components. ''' return self.r__, self.g__, self.b__, self.a__ @rgba.setter def rgba(self, rgba): self.r__, self.g__, self.b__, self.a__ = rgba self.name__ = None @property def RGB(self): ''' Red, green and blue integer color components. ''' return tuple(to_int_255(x) for x in self.rgb) @RGB.setter def RGB(self, RGB): self.r__, self.g__, self.b__ = (to_float_1(x) for x in RGB) self.name__ = None @property def RGBA(self): ''' Red, green, blue and alpha integer color components. ''' return tuple(to_int_255(x) for x in self.rgba) @RGBA.setter def RGBA(self, RGBA): self.r__, self.g__, self.b__, self.a__ = (to_float_1(x) for x in RGBA) self.name__ = None @property def str_hex(self): ''' Hex color string. ''' return simplify_hex('#%02x%02x%02x%02x' % self.RGBA) def use_hex_name(self): self.name__ = simplify_hex('#%02x%02x%02x%02x' % self.RGBA) @property def str_rgb(self): ''' Red, green and blue floating point color components as string. Output will be like ``'rgb(<red>, <green>, <blue>)'.`` ''' return 'rgb(%5.3f, %5.3f, %5.3f)' % self.rgb @property def str_RGB(self): ''' Red, green and blue integer color components as string. Output will be like ``'RGB(<red>, <green>, <blue>)'`` ''' return 'RGB(%i, %i, %i)' % self.RGB @property def str_rgba(self): ''' Red, green, blue and alpha floating point color components as string. Output will be like ``'rgba(<red>, <green>, <blue>, <alpha>)'``. ''' return 'rgba(%5.3f, %5.3f, %5.3f, %5.3f)' % self.rgba @property def str_RGBA(self): ''' Red, green, blue and alpha integer color components as string' Output will be like ``'RGBA(<red>, <green>, <blue>, <alpha>)'``. ''' return 'RGBA(%i, %i, %i, %i)' % self.RGBA
[docs] def describe(self): ''' Returns all possible definitions of the color. ''' return ''' name: %s hex: %s RGBA: %s rgba: %s str: %s ''' % ( self.name, self.str_hex, self.str_RGBA, self.str_rgba, str(self))
def __str__(self): return self.name__ if self.name__ is not None else self.str_rgba
[docs]class ColorGroup(Object): ''' Group of predefined colors. Each ColorGroup has a name and a set of colornames and referring Color Objects ''' name = String.T(optional=True) mapping = Dict.T(String.T(), Color.T())
g_groups = [] for name, color_dict in [ ('tango', g_tango_colors), ('standard', g_standard_colors)]: g_groups.append(ColorGroup( name=name, mapping=dict((k, Color(*v)) for (k, v) in color_dict.items()))) for color in g_groups[-1].mapping.values(): color.use_hex_name() def interpolate(a, b, blend, method='rgb'): assert method == 'rgb' assert 0.0 <= blend <= 1.0 c_rgba = tuple( a * (1.0-blend) + b * blend for (a, b) in zip(a.rgba, b.rgba)) return Color(*c_rgba) if __name__ == '__main__': import sys for g in g_groups: print(load_string(str(g))) for s in sys.argv[1:]: try: color = Color(s) except ColorError as e: sys.exit(str(e)) print(color.describe())