1# https://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6import re
8from pyrocko.guts import Object, Dict, SObject, Float, String, TBase, \
9 ValidationError, load_string
12guts_prefix = 'pf'
15class ColorError(ValueError):
16 pass
19class InvalidColorString(ColorError):
20 def __init__(self, s):
21 ColorError.__init__(self, 'Invalid color string: %s' % s)
24g_pattern_hex = re.compile(
25 r'^#([0-9a-fA-F]{1,2}){3,4}$')
28g_pattern_rgb = re.compile(
29 r'^(RGBA?|rgba?)\(([^,]+),([^,]+),([^,]+)(,([^)]+))?\)$')
32g_tango_colors = {
33 'butter1': (252, 233, 79),
34 'butter2': (237, 212, 0),
35 'butter3': (196, 160, 0),
36 'chameleon1': (138, 226, 52),
37 'chameleon2': (115, 210, 22),
38 'chameleon3': (78, 154, 6),
39 'orange1': (252, 175, 62),
40 'orange2': (245, 121, 0),
41 'orange3': (206, 92, 0),
42 'skyblue1': (114, 159, 207),
43 'skyblue2': (52, 101, 164),
44 'skyblue3': (32, 74, 135),
45 'plum1': (173, 127, 168),
46 'plum2': (117, 80, 123),
47 'plum3': (92, 53, 102),
48 'chocolate1': (233, 185, 110),
49 'chocolate2': (193, 125, 17),
50 'chocolate3': (143, 89, 2),
51 'scarletred1': (239, 41, 41),
52 'scarletred2': (204, 0, 0),
53 'scarletred3': (164, 0, 0),
54 'aluminium1': (238, 238, 236),
55 'aluminium2': (211, 215, 207),
56 'aluminium3': (186, 189, 182),
57 'aluminium4': (136, 138, 133),
58 'aluminium5': (85, 87, 83),
59 'aluminium6': (46, 52, 54)}
62g_standard_colors = {
63 'white': (255, 255, 255),
64 'black': (0, 0, 0),
65 'red': (255, 0, 0),
66 'green': (0, 255, 0),
67 'blue': (0, 0, 255)}
70g_named_colors = {}
72g_named_colors.update(g_tango_colors)
73g_named_colors.update(g_standard_colors)
76def parse_color(s):
77 '''
78 Translate color string into rgba values (range [0.0, 1.0])
80 The color string can be defined as
81 - **integer RGB(A) values** (range [0, 255])
82 e.g. 'RGBA(255, 255, 255, 255)'
83 or 'RGB(255, 255, 255)' (alpha set to 255),
84 - **floating point rgb(a) values** (range [0.0, 1.0])
85 e.g. 'rgba(1.0, 1.0, 1.0, 1.0)'
86 or 'rgba(1.0, 1.0, 1.0)' (alpha set to 255),
87 - **hex color** with 3, 4, 6 or 8 digits after #
88 e.g. #fff, #ffff, #ffffff or #ffffffff
89 - **name of predefined colors**,
90 e.g. 'butter1' or 'white'.
91 See pyrocko.plot.color.g_groups for complete list.
93 :param s: Color string as name, RGB(A), rgb(a) or hex
94 :type s: string
96 :return: floating point rgba values between 0.0 and 1.0.
97 :rtype: tuple of float, `len(rgba) = 4`
98 '''
100 orig_s = s
102 rgba = None
104 if s in g_named_colors:
105 rgba = tuple(to_float_1(x) for x in g_named_colors[s]) + (1.0,)
107 else:
108 m = g_pattern_hex.match(s)
109 if m:
110 s = s[1:]
111 if len(s) == 3:
112 s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2]
114 if len(s) == 4:
115 s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2] + s[3] + s[3]
117 if len(s) == 6:
118 s = s + 'FF'
120 if len(s) == 8:
121 try:
122 rgba = tuple(
123 int(s[i*2:i*2+2], base=16) / 255.
124 for i in range(4))
125 except ValueError:
126 raise InvalidColorString(orig_s)
128 else:
129 raise InvalidColorString(orig_s)
131 m = g_pattern_rgb.match(s)
132 if m:
133 rgb_mode = m.group(1)
134 if rgb_mode.startswith('rgb'):
135 typ = float
136 else:
137 def typ(x):
138 return int(x) / 255.
140 try:
141 rgba = (
142 typ(m.group(2)),
143 typ(m.group(3)),
144 typ(m.group(4)),
145 typ(m.group(6)) if m.group(6) else 1.0)
147 except ValueError:
148 raise InvalidColorString(orig_s)
150 if rgba is None:
151 raise InvalidColorString(orig_s)
153 if any(x < 0.0 or x > 1.0 for x in rgba):
154 raise InvalidColorString(orig_s)
156 return rgba
159def to_int_255(f):
160 '''
161 Convert floating point to integer color component
163 Convert a floating point color component (range [0.0, 1.0]) to an integer
164 color component (range [0, 255])
166 :param f: rgba floating point color component
167 :type f: float
169 :return: RGBA integer color component
170 :rtype: int
171 '''
173 if not (0.0 <= f <= 1.0):
174 raise ColorError(
175 'Floating point color component must be in the range [0.0, 1.0]')
177 return int(round(f * 255.))
180def to_float_1(i):
181 '''
182 Convert floating point to integer color component
184 Convert an integer color component (range [0, 255]) to a floating point
185 color component (range [0.0, 1.0])
187 :param i: RGBA integer color component
188 :type i: int
190 :return: rgba floating point color component
191 :rtype: float
192 '''
194 if not (0 <= i <= 255):
195 raise ColorError(
196 'Integer color component must be in the range [0, 255]')
198 return i / 255.
201def simplify_hex(s):
202 '''
203 Simplifiy a hex color code if possible
205 E.g.:
206 - #ffffffff -> #fff
207 - #11aabbff -> #1ab
209 :param s: hex color string
210 :type s: str
212 :return: simplified hex color string
213 :rtype: str
214 '''
216 if s[1] == s[2] and s[3] == s[4] and s[5] == s[6] \
217 and (len(s) == 9 and s[7] == s[8]):
219 s = s[0] + s[1] + s[3] + s[5] + (s[7] if len(s) == 9 else '')
221 if len(s) == 9 and s[-2:].lower() == 'ff':
222 s = s[:7]
224 elif len(s) == 5 and s[-1:].lower() == 'f':
225 s = s[:4]
227 return s
230class Component(Float):
231 class __T(TBase):
232 def validate_extra(self, x):
233 if not (0.0 <= x <= 1.0):
234 raise ValidationError(
235 'Color component must be in the range [0.0, 1.0]')
238class Color(SObject):
239 '''
240 Color class with red, green, blue and alpha values ranging [0.0, 1.0].
242 A name of color can be given instead of the RGBA/rgba/hex color.
243 '''
245 name__ = String.T(optional=True)
246 r__ = Component.T(default=0.0)
247 g__ = Component.T(default=0.0)
248 b__ = Component.T(default=0.0)
249 a__ = Component.T(default=1.0)
251 def __init__(self, *args, **kwargs):
252 if len(args) == 1:
253 SObject.__init__(self, init_props=False)
254 self.name = args[0]
256 elif len(args) in (3, 4):
257 SObject.__init__(self, init_props=False)
259 if all(isinstance(x, int) for x in args):
260 if len(args) == 3:
261 args = args + (255,)
263 self.RGBA = args
265 elif all(isinstance(x, float) for x in args):
266 if len(args) == 3:
267 args = args + (1.0,)
269 self.rgba = args
271 else:
272 SObject.__init__(self, init_props=False)
273 self.name__ = kwargs.get('name', None)
274 self.r__ = kwargs.get('r', 0.0)
275 self.g__ = kwargs.get('g', 0.0)
276 self.b__ = kwargs.get('b', 0.0)
277 self.a__ = kwargs.get('a', 1.0)
279 def __eq__(self, other):
280 return self.name__ == other.name__ \
281 and self.r__ == other.r__ \
282 and self.g__ == other.g__ \
283 and self.b__ == other.b__ \
284 and self.a__ == other.a__
286 @property
287 def name(self):
288 return self.name__ or ''
290 @name.setter
291 def name(self, name):
292 self.r__, self.g__, self.b__, self.a__ = parse_color(name)
293 self.name__ = name
295 @property
296 def r(self):
297 '''
298 Red floating point color component
299 '''
301 return self.r__
303 @r.setter
304 def r(self, r):
305 self.name__ = None
306 self.r__ = r
308 @property
309 def g(self):
310 '''
311 Green floating point color component
312 '''
314 return self.g__
316 @g.setter
317 def g(self, g):
318 self.name__ = None
319 self.g__ = g
321 @property
322 def b(self):
323 '''
324 Blue floating point color component
325 '''
327 return self.b__
329 @b.setter
330 def b(self, b):
331 self.name__ = None
332 self.b__ = b
334 @property
335 def a(self):
336 '''
337 Transparency (alpha) floating point color component
338 '''
340 return self.a__
342 @a.setter
343 def a(self, a):
344 self.name__ = None
345 self.a__ = a
347 @property
348 def rgb(self):
349 '''
350 Red, green and blue floating point color components
351 '''
353 return self.r__, self.g__, self.b__
355 @rgb.setter
356 def rgb(self, rgb):
357 self.r__, self.g__, self.b__ = rgb
358 self.name__ = None
360 @property
361 def rgba(self):
362 '''
363 Red, green, blue and alpha floating point color components
364 '''
366 return self.r__, self.g__, self.b__, self.a__
368 @rgba.setter
369 def rgba(self, rgba):
370 self.r__, self.g__, self.b__, self.a__ = rgba
371 self.name__ = None
373 @property
374 def RGB(self):
375 '''
376 Red, green and blue integer color components
377 '''
379 return tuple(to_int_255(x) for x in self.rgb)
381 @RGB.setter
382 def RGB(self, RGB):
383 self.r__, self.g__, self.b__ = (to_float_1(x) for x in RGB)
384 self.name__ = None
386 @property
387 def RGBA(self):
388 '''
389 Red, green, blue and alpha integer color components
390 '''
392 return tuple(to_int_255(x) for x in self.rgba)
394 @RGBA.setter
395 def RGBA(self, RGBA):
396 self.r__, self.g__, self.b__, self.a__ = (to_float_1(x) for x in RGBA)
397 self.name__ = None
399 @property
400 def str_hex(self):
401 '''
402 Hex color string
403 '''
405 return simplify_hex('#%02x%02x%02x%02x' % self.RGBA)
407 def use_hex_name(self):
408 self.name__ = simplify_hex('#%02x%02x%02x%02x' % self.RGBA)
410 @property
411 def str_rgb(self):
412 '''
413 red, green and blue floating point color components as string
415 Output will be of type 'rgb(<red>, <green>, <blue>)'
416 '''
418 return 'rgb(%5.3f, %5.3f, %5.3f)' % self.rgb
420 @property
421 def str_RGB(self):
422 '''
423 Red, green and blue integer color components as string
425 Output will be of type 'RGB(<red>, <green>, <blue>)'
426 '''
428 return 'RGB(%i, %i, %i)' % self.RGB
430 @property
431 def str_rgba(self):
432 '''
433 Red, green, blue and alpha floating point color components as string
435 Output will be of type 'rgba(<red>, <green>, <blue>, <alpha>)'
436 '''
438 return 'rgba(%5.3f, %5.3f, %5.3f, %5.3f)' % self.rgba
440 @property
441 def str_RGBA(self):
442 '''
443 Red, green, blue and alpha integer color components as string
445 Output will be of type 'RGBA(<red>, <green>, <blue>, <alpha>)'
446 '''
448 return 'RGBA(%i, %i, %i, %i)' % self.RGBA
450 def describe(self):
451 '''
452 Returns all possible definitions of the color
453 '''
455 return '''
456 name: %s
457 hex: %s
458 RGBA: %s
459 rgba: %s
460 str: %s
461''' % (
462 self.name,
463 self.str_hex,
464 self.str_RGBA,
465 self.str_rgba,
466 str(self))
468 def __str__(self):
469 return self.name__ if self.name__ is not None else self.str_rgba
472class ColorGroup(Object):
473 '''
474 Group of predefined colors.
476 Each ColorGroup has a name and a set of colornames and referring Color
477 Objects
478 '''
480 name = String.T(optional=True)
481 mapping = Dict.T(String.T(), Color.T())
484g_groups = []
486for name, color_dict in [
487 ('tango', g_tango_colors),
488 ('standard', g_standard_colors)]:
490 g_groups.append(ColorGroup(
491 name=name,
492 mapping=dict((k, Color(*v)) for (k, v) in color_dict.items())))
494 for color in g_groups[-1].mapping.values():
495 color.use_hex_name()
498def interpolate(a, b, blend, method='rgb'):
499 assert method == 'rgb'
500 assert 0.0 <= blend <= 1.0
502 c_rgba = tuple(
503 a * (1.0-blend) + b * blend
504 for (a, b) in zip(a.rgba, b.rgba))
506 return Color(*c_rgba)
509if __name__ == '__main__':
511 import sys
513 for g in g_groups:
514 print(load_string(str(g)))
516 for s in sys.argv[1:]:
518 try:
519 color = Color(s)
520 except ColorError as e:
521 sys.exit(str(e))
523 print(color.describe())