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_nat_colors = {
73 'nat_green1': (94, 102, 80),
74 'nat_green2': (143, 150, 125),
75 'nat_brown': (180, 157, 132),
76 'nat_gray': (95, 85, 83),
77 'nat_yellow': (199, 187, 0),
78 'nat_blue': (0, 101, 153),
79 'nat_acc_blue': (0, 179, 236),
80 'nat_acc_purple': (131, 33, 93),
81 'nat_acc_yellow': (199, 187, 0),
82 'nat_acc_orange': (238, 178, 0),
83 'nat_acc_gray': (95, 85, 83)}
85g_named_colors = {}
87g_named_colors.update(g_tango_colors)
88g_named_colors.update(g_standard_colors)
89g_named_colors.update(g_nat_colors)
92def parse_color(s):
93 '''
94 Translate color string into rgba values (range [0.0, 1.0])
96 The color string can be defined as
97 - **integer RGB(A) values** (range [0, 255])
98 e.g. 'RGBA(255, 255, 255, 255)'
99 or 'RGB(255, 255, 255)' (alpha set to 255),
100 - **floating point rgb(a) values** (range [0.0, 1.0])
101 e.g. 'rgba(1.0, 1.0, 1.0, 1.0)'
102 or 'rgba(1.0, 1.0, 1.0)' (alpha set to 255),
103 - **hex color** with 3, 4, 6 or 8 digits after #
104 e.g. #fff, #ffff, #ffffff or #ffffffff
105 - **name of predefined colors**,
106 e.g. 'butter1' or 'white'.
107 See pyrocko.plot.color.g_groups for complete list.
109 :param s: Color string as name, RGB(A), rgb(a) or hex
110 :type s: string
112 :return: floating point rgba values between 0.0 and 1.0.
113 :rtype: tuple of float, `len(rgba) = 4`
114 '''
116 orig_s = s
118 rgba = None
120 if s in g_named_colors:
121 rgba = tuple(to_float_1(x) for x in g_named_colors[s]) + (1.0,)
123 else:
124 m = g_pattern_hex.match(s)
125 if m:
126 s = s[1:]
127 if len(s) == 3:
128 s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2]
130 if len(s) == 4:
131 s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2] + s[3] + s[3]
133 if len(s) == 6:
134 s = s + 'FF'
136 if len(s) == 8:
137 try:
138 rgba = tuple(
139 int(s[i*2:i*2+2], base=16) / 255.
140 for i in range(4))
141 except ValueError:
142 raise InvalidColorString(orig_s)
144 else:
145 raise InvalidColorString(orig_s)
147 m = g_pattern_rgb.match(s)
148 if m:
149 rgb_mode = m.group(1)
150 if rgb_mode.startswith('rgb'):
151 typ = float
152 else:
153 def typ(x):
154 return int(x) / 255.
156 try:
157 rgba = (
158 typ(m.group(2)),
159 typ(m.group(3)),
160 typ(m.group(4)),
161 typ(m.group(6)) if m.group(6) else 1.0)
163 except ValueError:
164 raise InvalidColorString(orig_s)
166 if rgba is None:
167 raise InvalidColorString(orig_s)
169 if any(x < 0.0 or x > 1.0 for x in rgba):
170 raise InvalidColorString(orig_s)
172 return rgba
175def to_int_255(f):
176 '''
177 Convert floating point to integer color component
179 Convert a floating point color component (range [0.0, 1.0]) to an integer
180 color component (range [0, 255])
182 :param f: rgba floating point color component
183 :type f: float
185 :return: RGBA integer color component
186 :rtype: int
187 '''
189 if not (0.0 <= f <= 1.0):
190 raise ColorError(
191 'Floating point color component must be in the range [0.0, 1.0]')
193 return int(round(f * 255.))
196def to_float_1(i):
197 '''
198 Convert floating point to integer color component
200 Convert an integer color component (range [0, 255]) to a floating point
201 color component (range [0.0, 1.0])
203 :param i: RGBA integer color component
204 :type i: int
206 :return: rgba floating point color component
207 :rtype: float
208 '''
210 if not (0 <= i <= 255):
211 raise ColorError(
212 'Integer color component must be in the range [0, 255]')
214 return i / 255.
217def simplify_hex(s):
218 '''
219 Simplifiy a hex color code if possible
221 E.g.:
222 - #ffffffff -> #fff
223 - #11aabbff -> #1ab
225 :param s: hex color string
226 :type s: str
228 :return: simplified hex color string
229 :rtype: str
230 '''
232 if s[1] == s[2] and s[3] == s[4] and s[5] == s[6] \
233 and (len(s) == 9 and s[7] == s[8]):
235 s = s[0] + s[1] + s[3] + s[5] + (s[7] if len(s) == 9 else '')
237 if len(s) == 9 and s[-2:].lower() == 'ff':
238 s = s[:7]
240 elif len(s) == 5 and s[-1:].lower() == 'f':
241 s = s[:4]
243 return s
246class Component(Float):
247 class __T(TBase):
248 def validate_extra(self, x):
249 if not (0.0 <= x <= 1.0):
250 raise ValidationError(
251 'Color component must be in the range [0.0, 1.0]')
254class Color(SObject):
255 '''
256 Color class with red, green, blue and alpha values ranging [0.0, 1.0].
258 A name of color can be given instead of the RGBA/rgba/hex color.
259 '''
261 name__ = String.T(optional=True)
262 r__ = Component.T(default=0.0)
263 g__ = Component.T(default=0.0)
264 b__ = Component.T(default=0.0)
265 a__ = Component.T(default=1.0)
267 def __init__(self, *args, **kwargs):
268 if len(args) == 1:
269 SObject.__init__(self, init_props=False)
270 self.name = args[0]
272 elif len(args) in (3, 4):
273 SObject.__init__(self, init_props=False)
275 if all(isinstance(x, int) for x in args):
276 if len(args) == 3:
277 args = args + (255,)
279 self.RGBA = args
281 elif all(isinstance(x, float) for x in args):
282 if len(args) == 3:
283 args = args + (1.0,)
285 self.rgba = args
287 else:
288 SObject.__init__(self, **kwargs)
290 @property
291 def name(self):
292 return self.name__ or ''
294 @name.setter
295 def name(self, name):
296 self.r__, self.g__, self.b__, self.a__ = parse_color(name)
297 self.name__ = name
299 @property
300 def r(self):
301 '''
302 Red floating point color component
303 '''
305 return self.r__
307 @r.setter
308 def r(self, r):
309 self.name__ = None
310 self.r__ = r
312 @property
313 def g(self):
314 '''
315 Green floating point color component
316 '''
318 return self.g__
320 @g.setter
321 def g(self, g):
322 self.name__ = None
323 self.g__ = g
325 @property
326 def b(self):
327 '''
328 Blue floating point color component
329 '''
331 return self.b__
333 @b.setter
334 def b(self, b):
335 self.name__ = None
336 self.b__ = b
338 @property
339 def a(self):
340 '''
341 Transparency (alpha) floating point color component
342 '''
344 return self.a__
346 @a.setter
347 def a(self, a):
348 self.name__ = None
349 self.a__ = a
351 @property
352 def rgb(self):
353 '''
354 Red, green and blue floating point color components
355 '''
357 return self.r__, self.g__, self.b__
359 @rgb.setter
360 def rgb(self, rgb):
361 self.r__, self.g__, self.b__ = rgb
362 self.name__ = None
364 @property
365 def rgba(self):
366 '''
367 Red, green, blue and alpha floating point color components
368 '''
370 return self.r__, self.g__, self.b__, self.a__
372 @rgba.setter
373 def rgba(self, rgba):
374 self.r__, self.g__, self.b__, self.a__ = rgba
375 self.name__ = None
377 @property
378 def RGB(self):
379 '''
380 Red, green and blue integer color components
381 '''
383 return tuple(to_int_255(x) for x in self.rgb)
385 @RGB.setter
386 def RGB(self, RGB):
387 self.r__, self.g__, self.b__ = (to_float_1(x) for x in RGB)
388 self.name__ = None
390 @property
391 def RGBA(self):
392 '''
393 Red, green, blue and alpha integer color components
394 '''
396 return tuple(to_int_255(x) for x in self.rgba)
398 @RGBA.setter
399 def RGBA(self, RGBA):
400 self.r__, self.g__, self.b__, self.a__ = (to_float_1(x) for x in RGBA)
401 self.name__ = None
403 @property
404 def str_hex(self):
405 '''
406 Hex color string
407 '''
409 return simplify_hex('#%02x%02x%02x%02x' % self.RGBA)
411 def use_hex_name(self):
412 self.name__ = simplify_hex('#%02x%02x%02x%02x' % self.RGBA)
414 @property
415 def str_rgb(self):
416 '''
417 red, green and blue floating point color components as string
419 Output will be of type 'rgb(<red>, <green>, <blue>)'
420 '''
422 return 'rgb(%5.3f, %5.3f, %5.3f)' % self.rgb
424 @property
425 def str_RGB(self):
426 '''
427 Red, green and blue integer color components as string
429 Output will be of type 'RGB(<red>, <green>, <blue>)'
430 '''
432 return 'RGB(%i, %i, %i)' % self.RGB
434 @property
435 def str_rgba(self):
436 '''
437 Red, green, blue and alpha floating point color components as string
439 Output will be of type 'rgba(<red>, <green>, <blue>, <alpha>)'
440 '''
442 return 'rgba(%5.3f, %5.3f, %5.3f, %5.3f)' % self.rgba
444 @property
445 def str_RGBA(self):
446 '''
447 Red, green, blue and alpha integer color components as string
449 Output will be of type 'RGBA(<red>, <green>, <blue>, <alpha>)'
450 '''
452 return 'RGBA(%i, %i, %i, %i)' % self.RGBA
454 def describe(self):
455 '''
456 Returns all possible definitions of the color
457 '''
459 return '''
460 name: %s
461 hex: %s
462 RGBA: %s
463 rgba: %s
464 str: %s
465''' % (
466 self.name,
467 self.str_hex,
468 self.str_RGBA,
469 self.str_rgba,
470 str(self))
472 def __str__(self):
473 return self.name__ if self.name__ is not None else self.str_rgba
476class ColorGroup(Object):
477 '''
478 Group of predefined colors.
480 Each ColorGroup has a name and a set of colornames and referring Color
481 Objects
482 '''
484 name = String.T(optional=True)
485 mapping = Dict.T(String.T(), Color.T())
488g_groups = []
490for name, color_dict in [
491 ('tango', g_tango_colors),
492 ('standard', g_standard_colors),
493 ('nat', g_nat_colors)]:
495 g_groups.append(ColorGroup(
496 name=name,
497 mapping=dict((k, Color(*v)) for (k, v) in color_dict.items())))
499 for color in g_groups[-1].mapping.values():
500 color.use_hex_name()
502if __name__ == '__main__':
504 import sys
506 for g in g_groups:
507 print(load_string(str(g)))
509 for s in sys.argv[1:]:
511 try:
512 color = Color(s)
513 except ColorError as e:
514 sys.exit(str(e))
516 print(color.describe())