Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/color.py: 76%
188 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-04 09:52 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-04 09:52 +0000
1# https://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6'''
7Color utilities and built-in color palettes for a consistent look.
9.. _color:
11Color formats
12.............
14.. list-table::
15 :widths: 15 85
17 * - ``RGB``, ``RGBA``
18 - Integer values for red, green blue, alpha, in the range ``[0, 255]``.::
20 Color('RGBA(255, 255, 255, 255)') # from string
21 Color('RGB(255, 255, 255)')
22 Color(255, 255, 255, 255) # from values
23 Color(255, 255, 255)
25 * - ``rgb``, ``rgba``
26 - Floating point values in the range ``[0.0, 1.0]``.::
28 Color('rgba(1, 1, 1, 1)') # from string
29 Color('rgb(1, 1, 1)')
30 Color(1.0, 1.0, 1.0, 1.0) # from values
31 Color(1.0, 1.0, 1.0)
33 * - ``hex``
34 - Hexadecimal value with 3, 4, 6 or 8 digits.::
36 Color('#fff')
37 Color('#ffff')
38 Color('#ffffff')
39 Color('#ffffffff')
41 * - ``name``
42 - See :py:data:`g_named_colors` for a complete list of
43 available colors.::
45 Color('white')
46 Color('butter1')
47 Color('skyblue2')
50.. py:data:: g_named_colors
52 Dict mapping color names to :py:class:`Color` objects.
53'''
56import re
58from pyrocko.guts import Object, Dict, SObject, Float, String, TBase, \
59 ValidationError, load_string
62guts_prefix = 'pf'
65class ColorError(ValueError):
66 '''
67 Raised for invalid color specifications and conversions.
68 '''
69 pass
72class InvalidColorString(ColorError):
73 '''
74 Raised for invalid color string definitions.
75 '''
76 def __init__(self, s):
77 ColorError.__init__(self, 'Invalid color string: %s' % s)
80g_pattern_hex = re.compile(
81 r'^#([0-9a-fA-F]{1,2}){3,4}$')
84g_pattern_rgb = re.compile(
85 r'^(RGBA?|rgba?)\(([^,]+),([^,]+),([^,]+)(,([^)]+))?\)$')
88g_tango_colors = {
89 'butter1': (252, 233, 79),
90 'butter2': (237, 212, 0),
91 'butter3': (196, 160, 0),
92 'chameleon1': (138, 226, 52),
93 'chameleon2': (115, 210, 22),
94 'chameleon3': (78, 154, 6),
95 'orange1': (252, 175, 62),
96 'orange2': (245, 121, 0),
97 'orange3': (206, 92, 0),
98 'skyblue1': (114, 159, 207),
99 'skyblue2': (52, 101, 164),
100 'skyblue3': (32, 74, 135),
101 'plum1': (173, 127, 168),
102 'plum2': (117, 80, 123),
103 'plum3': (92, 53, 102),
104 'chocolate1': (233, 185, 110),
105 'chocolate2': (193, 125, 17),
106 'chocolate3': (143, 89, 2),
107 'scarletred1': (239, 41, 41),
108 'scarletred2': (204, 0, 0),
109 'scarletred3': (164, 0, 0),
110 'aluminium1': (238, 238, 236),
111 'aluminium2': (211, 215, 207),
112 'aluminium3': (186, 189, 182),
113 'aluminium4': (136, 138, 133),
114 'aluminium5': (85, 87, 83),
115 'aluminium6': (46, 52, 54)}
118g_standard_colors = {
119 'white': (255, 255, 255),
120 'black': (0, 0, 0),
121 'red': (255, 0, 0),
122 'green': (0, 255, 0),
123 'blue': (0, 0, 255)}
126g_named_colors = {}
128g_named_colors.update(g_tango_colors)
129g_named_colors.update(g_standard_colors)
132def parse_color(s):
133 '''
134 Translate color string into :ref:`rgba tuple <color>`.
136 :param s: :ref:`Color definition <color>`.
137 :type s: str
139 :return: Color value in ``rgba`` form.
140 :rtype: :py:class:`tuple` of 4 :py:class:`float`
141 '''
143 orig_s = s
145 rgba = None
147 if s in g_named_colors:
148 rgba = tuple(to_float_1(x) for x in g_named_colors[s]) + (1.0,)
150 else:
151 m = g_pattern_hex.match(s)
152 if m:
153 s = s[1:]
154 if len(s) == 3:
155 s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2]
157 if len(s) == 4:
158 s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2] + s[3] + s[3]
160 if len(s) == 6:
161 s = s + 'FF'
163 if len(s) == 8:
164 try:
165 rgba = tuple(
166 int(s[i*2:i*2+2], base=16) / 255.
167 for i in range(4))
168 except ValueError:
169 raise InvalidColorString(orig_s)
171 else:
172 raise InvalidColorString(orig_s)
174 m = g_pattern_rgb.match(s)
175 if m:
176 rgb_mode = m.group(1)
177 if rgb_mode.startswith('rgb'):
178 typ = float
179 else:
180 def typ(x):
181 return int(x) / 255.
183 try:
184 rgba = (
185 typ(m.group(2)),
186 typ(m.group(3)),
187 typ(m.group(4)),
188 typ(m.group(6)) if m.group(6) else 1.0)
190 except ValueError:
191 raise InvalidColorString(orig_s)
193 if rgba is None:
194 raise InvalidColorString(orig_s)
196 if any(x < 0.0 or x > 1.0 for x in rgba):
197 raise InvalidColorString(orig_s)
199 return rgba
202def to_int_255(f):
203 '''
204 Convert floating point to integer color component
206 Convert a floating point color component (range [0.0, 1.0]) to an integer
207 color component (range [0, 255])
209 :param f: rgba floating point color component
210 :type f: float
212 :return: RGBA integer color component
213 :rtype: int
214 '''
216 if not (0.0 <= f <= 1.0):
217 raise ColorError(
218 'Floating point color component must be in the range [0.0, 1.0]')
220 return int(round(f * 255.))
223def to_float_1(i):
224 '''
225 Convert integer to floating point color component
227 Convert an integer color component (range [0, 255]) to a floating point
228 color component (range [0.0, 1.0])
230 :param i: RGBA integer color component
231 :type i: int
233 :return: rgba floating point color component
234 :rtype: float
235 '''
237 if not (0 <= i <= 255):
238 raise ColorError(
239 'Integer color component must be in the range [0, 255]')
241 return i / 255.
244def simplify_hex(s):
245 '''
246 Simplifiy a hex color code if possible
248 E.g.:
249 - #ffffffff -> #fff
250 - #11aabbff -> #1ab
252 :param s: hex color string
253 :type s: str
255 :return: simplified hex color string
256 :rtype: str
257 '''
259 if s[1] == s[2] and s[3] == s[4] and s[5] == s[6] \
260 and (len(s) == 9 and s[7] == s[8]):
262 s = s[0] + s[1] + s[3] + s[5] + (s[7] if len(s) == 9 else '')
264 if len(s) == 9 and s[-2:].lower() == 'ff':
265 s = s[:7]
267 elif len(s) == 5 and s[-1:].lower() == 'f':
268 s = s[:4]
270 return s
273class Component(Float):
274 class __T(TBase):
275 def validate_extra(self, x):
276 if not (0.0 <= x <= 1.0):
277 raise ValidationError(
278 'Color component must be in the range [0.0, 1.0]')
281class Color(SObject):
282 '''
283 Color with red, green, blue and alpha values.
285 The color can be specified as a :ref:`color string <color>` or by giving
286 the component values in :py:class:`int` or :py:class:`float` format.
288 Examples::
290 Color('black')
291 Color('RGBA(255, 255, 255, 255)')
292 Color('RGB(255, 255, 255)')
293 Color('rgba(1.0, 1.0, 1.0, 1.0')')
294 Color('rgb(1.0, 1.0, 1.0')')
295 Color(255, 255, 255, 255)
296 Color(255, 255, 255)
297 Color(1.0, 1.0, 1.0, 1.0)
298 Color(1.0, 1.0, 1.0)
299 Color(r=1.0, g=1.0, b=1.0, a=1.0)
301 The internal representation is ``rgba``.
302 '''
304 name__ = String.T(optional=True)
305 r__ = Component.T(
306 default=0.0,
307 help='Red component ``[0., 1.]``.')
308 g__ = Component.T(
309 default=0.0,
310 help='Green component ``[0., 1.]``.')
311 b__ = Component.T(
312 default=0.0,
313 help='Blue component ``[0., 1.]``.')
314 a__ = Component.T(
315 default=1.0,
316 help='Alpha (opacity) component ``[0., 1.]``.')
318 def __init__(self, *args, **kwargs):
319 if len(args) == 1:
320 SObject.__init__(self, init_props=False)
321 self.name = args[0]
323 elif len(args) in (3, 4):
324 SObject.__init__(self, init_props=False)
326 if all(isinstance(x, int) for x in args):
327 if len(args) == 3:
328 args = args + (255,)
330 self.RGBA = args
332 elif all(isinstance(x, float) for x in args):
333 if len(args) == 3:
334 args = args + (1.0,)
336 self.rgba = args
338 else:
339 SObject.__init__(self, init_props=False)
340 self.name__ = kwargs.get('name', None)
341 self.r__ = kwargs.get('r', 0.0)
342 self.g__ = kwargs.get('g', 0.0)
343 self.b__ = kwargs.get('b', 0.0)
344 self.a__ = kwargs.get('a', 1.0)
346 def __eq__(self, other):
347 return self.name__ == other.name__ \
348 and self.r__ == other.r__ \
349 and self.g__ == other.g__ \
350 and self.b__ == other.b__ \
351 and self.a__ == other.a__
353 @property
354 def name(self):
355 return self.name__ or ''
357 @name.setter
358 def name(self, name):
359 self.r__, self.g__, self.b__, self.a__ = parse_color(name)
360 self.name__ = name
362 @property
363 def r(self):
364 return self.r__
366 @r.setter
367 def r(self, r):
368 self.name__ = None
369 self.r__ = r
371 @property
372 def g(self):
373 return self.g__
375 @g.setter
376 def g(self, g):
377 self.name__ = None
378 self.g__ = g
380 @property
381 def b(self):
382 return self.b__
384 @b.setter
385 def b(self, b):
386 self.name__ = None
387 self.b__ = b
389 @property
390 def a(self):
391 return self.a__
393 @a.setter
394 def a(self, a):
395 self.name__ = None
396 self.a__ = a
398 @property
399 def rgb(self):
400 '''
401 Red, green and blue floating point color components.
402 '''
404 return self.r__, self.g__, self.b__
406 @rgb.setter
407 def rgb(self, rgb):
408 self.r__, self.g__, self.b__ = rgb
409 self.name__ = None
411 @property
412 def rgba(self):
413 '''
414 Red, green, blue and alpha floating point color components.
415 '''
417 return self.r__, self.g__, self.b__, self.a__
419 @rgba.setter
420 def rgba(self, rgba):
421 self.r__, self.g__, self.b__, self.a__ = rgba
422 self.name__ = None
424 @property
425 def RGB(self):
426 '''
427 Red, green and blue integer color components.
428 '''
430 return tuple(to_int_255(x) for x in self.rgb)
432 @RGB.setter
433 def RGB(self, RGB):
434 self.r__, self.g__, self.b__ = (to_float_1(x) for x in RGB)
435 self.name__ = None
437 @property
438 def RGBA(self):
439 '''
440 Red, green, blue and alpha integer color components.
441 '''
443 return tuple(to_int_255(x) for x in self.rgba)
445 @RGBA.setter
446 def RGBA(self, RGBA):
447 self.r__, self.g__, self.b__, self.a__ = (to_float_1(x) for x in RGBA)
448 self.name__ = None
450 @property
451 def str_hex(self):
452 '''
453 Hex color string.
454 '''
456 return simplify_hex('#%02x%02x%02x%02x' % self.RGBA)
458 def use_hex_name(self):
459 self.name__ = simplify_hex('#%02x%02x%02x%02x' % self.RGBA)
461 @property
462 def str_rgb(self):
463 '''
464 Red, green and blue floating point color components as string.
466 Output will be like ``'rgb(<red>, <green>, <blue>)'.``
467 '''
469 return 'rgb(%5.3f, %5.3f, %5.3f)' % self.rgb
471 @property
472 def str_RGB(self):
473 '''
474 Red, green and blue integer color components as string.
476 Output will be like ``'RGB(<red>, <green>, <blue>)'``
477 '''
479 return 'RGB(%i, %i, %i)' % self.RGB
481 @property
482 def str_rgba(self):
483 '''
484 Red, green, blue and alpha floating point color components as string.
486 Output will be like ``'rgba(<red>, <green>, <blue>, <alpha>)'``.
487 '''
489 return 'rgba(%5.3f, %5.3f, %5.3f, %5.3f)' % self.rgba
491 @property
492 def str_RGBA(self):
493 '''
494 Red, green, blue and alpha integer color components as string'
496 Output will be like ``'RGBA(<red>, <green>, <blue>, <alpha>)'``.
497 '''
499 return 'RGBA(%i, %i, %i, %i)' % self.RGBA
501 def describe(self):
502 '''
503 Returns all possible definitions of the color.
504 '''
506 return '''
507 name: %s
508 hex: %s
509 RGBA: %s
510 rgba: %s
511 str: %s
512''' % (
513 self.name,
514 self.str_hex,
515 self.str_RGBA,
516 self.str_rgba,
517 str(self))
519 def __str__(self):
520 return self.name__ if self.name__ is not None else self.str_rgba
523class ColorGroup(Object):
524 '''
525 Group of predefined colors.
527 Each ColorGroup has a name and a set of colornames and referring Color
528 Objects
529 '''
531 name = String.T(optional=True)
532 mapping = Dict.T(String.T(), Color.T())
535g_groups = []
537for name, color_dict in [
538 ('tango', g_tango_colors),
539 ('standard', g_standard_colors)]:
541 g_groups.append(ColorGroup(
542 name=name,
543 mapping=dict((k, Color(*v)) for (k, v) in color_dict.items())))
545 for color in g_groups[-1].mapping.values():
546 color.use_hex_name()
549def interpolate(a, b, blend, method='rgb'):
550 assert method == 'rgb'
551 assert 0.0 <= blend <= 1.0
553 c_rgba = tuple(
554 a * (1.0-blend) + b * blend
555 for (a, b) in zip(a.rgba, b.rgba))
557 return Color(*c_rgba)
560if __name__ == '__main__':
562 import sys
564 for g in g_groups:
565 print(load_string(str(g)))
567 for s in sys.argv[1:]:
569 try:
570 color = Color(s)
571 except ColorError as e:
572 sys.exit(str(e))
574 print(color.describe())