1# https://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6from __future__ import absolute_import, print_function, division
8import re
10from pyrocko.guts import Object, Dict, SObject, Float, String, TBase, \
11 ValidationError, load_string
14guts_prefix = 'pf'
17class ColorError(ValueError):
18 pass
21class InvalidColorString(ColorError):
22 def __init__(self, s):
23 ColorError.__init__(self, 'Invalid color string: %s' % s)
26g_pattern_hex = re.compile(
27 r'^#([0-9a-fA-F]{1,2}){3,4}$')
30g_pattern_rgb = re.compile(
31 r'^(RGBA?|rgba?)\(([^,]+),([^,]+),([^,]+)(,([^)]+))?\)$')
34g_tango_colors = {
35 'butter1': (252, 233, 79),
36 'butter2': (237, 212, 0),
37 'butter3': (196, 160, 0),
38 'chameleon1': (138, 226, 52),
39 'chameleon2': (115, 210, 22),
40 'chameleon3': (78, 154, 6),
41 'orange1': (252, 175, 62),
42 'orange2': (245, 121, 0),
43 'orange3': (206, 92, 0),
44 'skyblue1': (114, 159, 207),
45 'skyblue2': (52, 101, 164),
46 'skyblue3': (32, 74, 135),
47 'plum1': (173, 127, 168),
48 'plum2': (117, 80, 123),
49 'plum3': (92, 53, 102),
50 'chocolate1': (233, 185, 110),
51 'chocolate2': (193, 125, 17),
52 'chocolate3': (143, 89, 2),
53 'scarletred1': (239, 41, 41),
54 'scarletred2': (204, 0, 0),
55 'scarletred3': (164, 0, 0),
56 'aluminium1': (238, 238, 236),
57 'aluminium2': (211, 215, 207),
58 'aluminium3': (186, 189, 182),
59 'aluminium4': (136, 138, 133),
60 'aluminium5': (85, 87, 83),
61 'aluminium6': (46, 52, 54)}
64g_standard_colors = {
65 'white': (255, 255, 255),
66 'black': (0, 0, 0),
67 'red': (255, 0, 0),
68 'green': (0, 255, 0),
69 'blue': (0, 0, 255)}
72g_named_colors = {}
74g_named_colors.update(g_tango_colors)
75g_named_colors.update(g_standard_colors)
78def parse_color(s):
79 '''
80 Translate color string into rgba values (range [0.0, 1.0])
82 The color string can be defined as
83 - **integer RGB(A) values** (range [0, 255])
84 e.g. 'RGBA(255, 255, 255, 255)'
85 or 'RGB(255, 255, 255)' (alpha set to 255),
86 - **floating point rgb(a) values** (range [0.0, 1.0])
87 e.g. 'rgba(1.0, 1.0, 1.0, 1.0)'
88 or 'rgba(1.0, 1.0, 1.0)' (alpha set to 255),
89 - **hex color** with 3, 4, 6 or 8 digits after #
90 e.g. #fff, #ffff, #ffffff or #ffffffff
91 - **name of predefined colors**,
92 e.g. 'butter1' or 'white'.
93 See pyrocko.plot.color.g_groups for complete list.
95 :param s: Color string as name, RGB(A), rgb(a) or hex
96 :type s: string
98 :return: floating point rgba values between 0.0 and 1.0.
99 :rtype: tuple of float, `len(rgba) = 4`
100 '''
102 orig_s = s
104 rgba = None
106 if s in g_named_colors:
107 rgba = tuple(to_float_1(x) for x in g_named_colors[s]) + (1.0,)
109 else:
110 m = g_pattern_hex.match(s)
111 if m:
112 s = s[1:]
113 if len(s) == 3:
114 s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2]
116 if len(s) == 4:
117 s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2] + s[3] + s[3]
119 if len(s) == 6:
120 s = s + 'FF'
122 if len(s) == 8:
123 try:
124 rgba = tuple(
125 int(s[i*2:i*2+2], base=16) / 255.
126 for i in range(4))
127 except ValueError:
128 raise InvalidColorString(orig_s)
130 else:
131 raise InvalidColorString(orig_s)
133 m = g_pattern_rgb.match(s)
134 if m:
135 rgb_mode = m.group(1)
136 if rgb_mode.startswith('rgb'):
137 typ = float
138 else:
139 def typ(x):
140 return int(x) / 255.
142 try:
143 rgba = (
144 typ(m.group(2)),
145 typ(m.group(3)),
146 typ(m.group(4)),
147 typ(m.group(6)) if m.group(6) else 1.0)
149 except ValueError:
150 raise InvalidColorString(orig_s)
152 if rgba is None:
153 raise InvalidColorString(orig_s)
155 if any(x < 0.0 or x > 1.0 for x in rgba):
156 raise InvalidColorString(orig_s)
158 return rgba
161def to_int_255(f):
162 '''
163 Convert floating point to integer color component
165 Convert a floating point color component (range [0.0, 1.0]) to an integer
166 color component (range [0, 255])
168 :param f: rgba floating point color component
169 :type f: float
171 :return: RGBA integer color component
172 :rtype: int
173 '''
175 if not (0.0 <= f <= 1.0):
176 raise ColorError(
177 'Floating point color component must be in the range [0.0, 1.0]')
179 return int(round(f * 255.))
182def to_float_1(i):
183 '''
184 Convert floating point to integer color component
186 Convert an integer color component (range [0, 255]) to a floating point
187 color component (range [0.0, 1.0])
189 :param i: RGBA integer color component
190 :type i: int
192 :return: rgba floating point color component
193 :rtype: float
194 '''
196 if not (0 <= i <= 255):
197 raise ColorError(
198 'Integer color component must be in the range [0, 255]')
200 return i / 255.
203def simplify_hex(s):
204 '''
205 Simplifiy a hex color code if possible
207 E.g.:
208 - #ffffffff -> #fff
209 - #11aabbff -> #1ab
211 :param s: hex color string
212 :type s: str
214 :return: simplified hex color string
215 :rtype: str
216 '''
218 if s[1] == s[2] and s[3] == s[4] and s[5] == s[6] \
219 and (len(s) == 9 and s[7] == s[8]):
221 s = s[0] + s[1] + s[3] + s[5] + (s[7] if len(s) == 9 else '')
223 if len(s) == 9 and s[-2:].lower() == 'ff':
224 s = s[:7]
226 elif len(s) == 5 and s[-1:].lower() == 'f':
227 s = s[:4]
229 return s
232class Component(Float):
233 class __T(TBase):
234 def validate_extra(self, x):
235 if not (0.0 <= x <= 1.0):
236 raise ValidationError(
237 'Color component must be in the range [0.0, 1.0]')
240class Color(SObject):
241 '''
242 Color class with red, green, blue and alpha values ranging [0.0, 1.0].
244 A name of color can be given instead of the RGBA/rgba/hex color.
245 '''
247 name__ = String.T(optional=True)
248 r__ = Component.T(default=0.0)
249 g__ = Component.T(default=0.0)
250 b__ = Component.T(default=0.0)
251 a__ = Component.T(default=1.0)
253 def __init__(self, *args, **kwargs):
254 if len(args) == 1:
255 SObject.__init__(self, init_props=False)
256 self.name = args[0]
258 elif len(args) in (3, 4):
259 SObject.__init__(self, init_props=False)
261 if all(isinstance(x, int) for x in args):
262 if len(args) == 3:
263 args = args + (255,)
265 self.RGBA = args
267 elif all(isinstance(x, float) for x in args):
268 if len(args) == 3:
269 args = args + (1.0,)
271 self.rgba = args
273 else:
274 SObject.__init__(self, init_props=False)
275 self.name__ = kwargs.get('name', None)
276 self.r__ = kwargs.get('r', 0.0)
277 self.g__ = kwargs.get('g', 0.0)
278 self.b__ = kwargs.get('b', 0.0)
279 self.a__ = kwargs.get('a', 1.0)
281 def __eq__(self, other):
282 return self.name__ == other.name__ \
283 and self.r__ == other.r__ \
284 and self.g__ == other.g__ \
285 and self.b__ == other.b__ \
286 and self.a__ == other.a__
288 @property
289 def name(self):
290 return self.name__ or ''
292 @name.setter
293 def name(self, name):
294 self.r__, self.g__, self.b__, self.a__ = parse_color(name)
295 self.name__ = name
297 @property
298 def r(self):
299 '''
300 Red floating point color component
301 '''
303 return self.r__
305 @r.setter
306 def r(self, r):
307 self.name__ = None
308 self.r__ = r
310 @property
311 def g(self):
312 '''
313 Green floating point color component
314 '''
316 return self.g__
318 @g.setter
319 def g(self, g):
320 self.name__ = None
321 self.g__ = g
323 @property
324 def b(self):
325 '''
326 Blue floating point color component
327 '''
329 return self.b__
331 @b.setter
332 def b(self, b):
333 self.name__ = None
334 self.b__ = b
336 @property
337 def a(self):
338 '''
339 Transparency (alpha) floating point color component
340 '''
342 return self.a__
344 @a.setter
345 def a(self, a):
346 self.name__ = None
347 self.a__ = a
349 @property
350 def rgb(self):
351 '''
352 Red, green and blue floating point color components
353 '''
355 return self.r__, self.g__, self.b__
357 @rgb.setter
358 def rgb(self, rgb):
359 self.r__, self.g__, self.b__ = rgb
360 self.name__ = None
362 @property
363 def rgba(self):
364 '''
365 Red, green, blue and alpha floating point color components
366 '''
368 return self.r__, self.g__, self.b__, self.a__
370 @rgba.setter
371 def rgba(self, rgba):
372 self.r__, self.g__, self.b__, self.a__ = rgba
373 self.name__ = None
375 @property
376 def RGB(self):
377 '''
378 Red, green and blue integer color components
379 '''
381 return tuple(to_int_255(x) for x in self.rgb)
383 @RGB.setter
384 def RGB(self, RGB):
385 self.r__, self.g__, self.b__ = (to_float_1(x) for x in RGB)
386 self.name__ = None
388 @property
389 def RGBA(self):
390 '''
391 Red, green, blue and alpha integer color components
392 '''
394 return tuple(to_int_255(x) for x in self.rgba)
396 @RGBA.setter
397 def RGBA(self, RGBA):
398 self.r__, self.g__, self.b__, self.a__ = (to_float_1(x) for x in RGBA)
399 self.name__ = None
401 @property
402 def str_hex(self):
403 '''
404 Hex color string
405 '''
407 return simplify_hex('#%02x%02x%02x%02x' % self.RGBA)
409 def use_hex_name(self):
410 self.name__ = simplify_hex('#%02x%02x%02x%02x' % self.RGBA)
412 @property
413 def str_rgb(self):
414 '''
415 red, green and blue floating point color components as string
417 Output will be of type 'rgb(<red>, <green>, <blue>)'
418 '''
420 return 'rgb(%5.3f, %5.3f, %5.3f)' % self.rgb
422 @property
423 def str_RGB(self):
424 '''
425 Red, green and blue integer color components as string
427 Output will be of type 'RGB(<red>, <green>, <blue>)'
428 '''
430 return 'RGB(%i, %i, %i)' % self.RGB
432 @property
433 def str_rgba(self):
434 '''
435 Red, green, blue and alpha floating point color components as string
437 Output will be of type 'rgba(<red>, <green>, <blue>, <alpha>)'
438 '''
440 return 'rgba(%5.3f, %5.3f, %5.3f, %5.3f)' % self.rgba
442 @property
443 def str_RGBA(self):
444 '''
445 Red, green, blue and alpha integer color components as string
447 Output will be of type 'RGBA(<red>, <green>, <blue>, <alpha>)'
448 '''
450 return 'RGBA(%i, %i, %i, %i)' % self.RGBA
452 def describe(self):
453 '''
454 Returns all possible definitions of the color
455 '''
457 return '''
458 name: %s
459 hex: %s
460 RGBA: %s
461 rgba: %s
462 str: %s
463''' % (
464 self.name,
465 self.str_hex,
466 self.str_RGBA,
467 self.str_rgba,
468 str(self))
470 def __str__(self):
471 return self.name__ if self.name__ is not None else self.str_rgba
474class ColorGroup(Object):
475 '''
476 Group of predefined colors.
478 Each ColorGroup has a name and a set of colornames and referring Color
479 Objects
480 '''
482 name = String.T(optional=True)
483 mapping = Dict.T(String.T(), Color.T())
486g_groups = []
488for name, color_dict in [
489 ('tango', g_tango_colors),
490 ('standard', g_standard_colors)]:
492 g_groups.append(ColorGroup(
493 name=name,
494 mapping=dict((k, Color(*v)) for (k, v) in color_dict.items())))
496 for color in g_groups[-1].mapping.values():
497 color.use_hex_name()
500def interpolate(a, b, blend, method='rgb'):
501 assert method == 'rgb'
502 assert 0.0 <= blend <= 1.0
504 c_rgba = tuple(
505 a * (1.0-blend) + b * blend
506 for (a, b) in zip(a.rgba, b.rgba))
508 return Color(*c_rgba)
511if __name__ == '__main__':
513 import sys
515 for g in g_groups:
516 print(load_string(str(g)))
518 for s in sys.argv[1:]:
520 try:
521 color = Color(s)
522 except ColorError as e:
523 sys.exit(str(e))
525 print(color.describe())