1# https://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

4# ---|P------/S----------~Lg---------- 

5 

6from __future__ import absolute_import, print_function, division 

7 

8import re 

9 

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

11 ValidationError, load_string 

12 

13 

14guts_prefix = 'pf' 

15 

16 

17class ColorError(ValueError): 

18 pass 

19 

20 

21class InvalidColorString(ColorError): 

22 def __init__(self, s): 

23 ColorError.__init__(self, 'Invalid color string: %s' % s) 

24 

25 

26g_pattern_hex = re.compile( 

27 r'^#([0-9a-fA-F]{1,2}){3,4}$') 

28 

29 

30g_pattern_rgb = re.compile( 

31 r'^(RGBA?|rgba?)\(([^,]+),([^,]+),([^,]+)(,([^)]+))?\)$') 

32 

33 

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)} 

62 

63 

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)} 

70 

71 

72g_named_colors = {} 

73 

74g_named_colors.update(g_tango_colors) 

75g_named_colors.update(g_standard_colors) 

76 

77 

78def parse_color(s): 

79 ''' 

80 Translate color string into rgba values (range [0.0, 1.0]) 

81 

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. 

94 

95 :param s: Color string as name, RGB(A), rgb(a) or hex 

96 :type s: string 

97 

98 :return: floating point rgba values between 0.0 and 1.0. 

99 :rtype: tuple of float, `len(rgba) = 4` 

100 ''' 

101 

102 orig_s = s 

103 

104 rgba = None 

105 

106 if s in g_named_colors: 

107 rgba = tuple(to_float_1(x) for x in g_named_colors[s]) + (1.0,) 

108 

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] 

115 

116 if len(s) == 4: 

117 s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2] + s[3] + s[3] 

118 

119 if len(s) == 6: 

120 s = s + 'FF' 

121 

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) 

129 

130 else: 

131 raise InvalidColorString(orig_s) 

132 

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. 

141 

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) 

148 

149 except ValueError: 

150 raise InvalidColorString(orig_s) 

151 

152 if rgba is None: 

153 raise InvalidColorString(orig_s) 

154 

155 if any(x < 0.0 or x > 1.0 for x in rgba): 

156 raise InvalidColorString(orig_s) 

157 

158 return rgba 

159 

160 

161def to_int_255(f): 

162 ''' 

163 Convert floating point to integer color component 

164 

165 Convert a floating point color component (range [0.0, 1.0]) to an integer 

166 color component (range [0, 255]) 

167 

168 :param f: rgba floating point color component 

169 :type f: float 

170 

171 :return: RGBA integer color component 

172 :rtype: int 

173 ''' 

174 

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]') 

178 

179 return int(round(f * 255.)) 

180 

181 

182def to_float_1(i): 

183 ''' 

184 Convert floating point to integer color component 

185 

186 Convert an integer color component (range [0, 255]) to a floating point 

187 color component (range [0.0, 1.0]) 

188 

189 :param i: RGBA integer color component 

190 :type i: int 

191 

192 :return: rgba floating point color component 

193 :rtype: float 

194 ''' 

195 

196 if not (0 <= i <= 255): 

197 raise ColorError( 

198 'Integer color component must be in the range [0, 255]') 

199 

200 return i / 255. 

201 

202 

203def simplify_hex(s): 

204 ''' 

205 Simplifiy a hex color code if possible 

206 

207 E.g.: 

208 - #ffffffff -> #fff 

209 - #11aabbff -> #1ab 

210 

211 :param s: hex color string 

212 :type s: str 

213 

214 :return: simplified hex color string 

215 :rtype: str 

216 ''' 

217 

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]): 

220 

221 s = s[0] + s[1] + s[3] + s[5] + (s[7] if len(s) == 9 else '') 

222 

223 if len(s) == 9 and s[-2:].lower() == 'ff': 

224 s = s[:7] 

225 

226 elif len(s) == 5 and s[-1:].lower() == 'f': 

227 s = s[:4] 

228 

229 return s 

230 

231 

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]') 

238 

239 

240class Color(SObject): 

241 ''' 

242 Color class with red, green, blue and alpha values ranging [0.0, 1.0]. 

243 

244 A name of color can be given instead of the RGBA/rgba/hex color. 

245 ''' 

246 

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) 

252 

253 def __init__(self, *args, **kwargs): 

254 if len(args) == 1: 

255 SObject.__init__(self, init_props=False) 

256 self.name = args[0] 

257 

258 elif len(args) in (3, 4): 

259 SObject.__init__(self, init_props=False) 

260 

261 if all(isinstance(x, int) for x in args): 

262 if len(args) == 3: 

263 args = args + (255,) 

264 

265 self.RGBA = args 

266 

267 elif all(isinstance(x, float) for x in args): 

268 if len(args) == 3: 

269 args = args + (1.0,) 

270 

271 self.rgba = args 

272 

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) 

280 

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__ 

287 

288 @property 

289 def name(self): 

290 return self.name__ or '' 

291 

292 @name.setter 

293 def name(self, name): 

294 self.r__, self.g__, self.b__, self.a__ = parse_color(name) 

295 self.name__ = name 

296 

297 @property 

298 def r(self): 

299 ''' 

300 Red floating point color component 

301 ''' 

302 

303 return self.r__ 

304 

305 @r.setter 

306 def r(self, r): 

307 self.name__ = None 

308 self.r__ = r 

309 

310 @property 

311 def g(self): 

312 ''' 

313 Green floating point color component 

314 ''' 

315 

316 return self.g__ 

317 

318 @g.setter 

319 def g(self, g): 

320 self.name__ = None 

321 self.g__ = g 

322 

323 @property 

324 def b(self): 

325 ''' 

326 Blue floating point color component 

327 ''' 

328 

329 return self.b__ 

330 

331 @b.setter 

332 def b(self, b): 

333 self.name__ = None 

334 self.b__ = b 

335 

336 @property 

337 def a(self): 

338 ''' 

339 Transparency (alpha) floating point color component 

340 ''' 

341 

342 return self.a__ 

343 

344 @a.setter 

345 def a(self, a): 

346 self.name__ = None 

347 self.a__ = a 

348 

349 @property 

350 def rgb(self): 

351 ''' 

352 Red, green and blue floating point color components 

353 ''' 

354 

355 return self.r__, self.g__, self.b__ 

356 

357 @rgb.setter 

358 def rgb(self, rgb): 

359 self.r__, self.g__, self.b__ = rgb 

360 self.name__ = None 

361 

362 @property 

363 def rgba(self): 

364 ''' 

365 Red, green, blue and alpha floating point color components 

366 ''' 

367 

368 return self.r__, self.g__, self.b__, self.a__ 

369 

370 @rgba.setter 

371 def rgba(self, rgba): 

372 self.r__, self.g__, self.b__, self.a__ = rgba 

373 self.name__ = None 

374 

375 @property 

376 def RGB(self): 

377 ''' 

378 Red, green and blue integer color components 

379 ''' 

380 

381 return tuple(to_int_255(x) for x in self.rgb) 

382 

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 

387 

388 @property 

389 def RGBA(self): 

390 ''' 

391 Red, green, blue and alpha integer color components 

392 ''' 

393 

394 return tuple(to_int_255(x) for x in self.rgba) 

395 

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 

400 

401 @property 

402 def str_hex(self): 

403 ''' 

404 Hex color string 

405 ''' 

406 

407 return simplify_hex('#%02x%02x%02x%02x' % self.RGBA) 

408 

409 def use_hex_name(self): 

410 self.name__ = simplify_hex('#%02x%02x%02x%02x' % self.RGBA) 

411 

412 @property 

413 def str_rgb(self): 

414 ''' 

415 red, green and blue floating point color components as string 

416 

417 Output will be of type 'rgb(<red>, <green>, <blue>)' 

418 ''' 

419 

420 return 'rgb(%5.3f, %5.3f, %5.3f)' % self.rgb 

421 

422 @property 

423 def str_RGB(self): 

424 ''' 

425 Red, green and blue integer color components as string 

426 

427 Output will be of type 'RGB(<red>, <green>, <blue>)' 

428 ''' 

429 

430 return 'RGB(%i, %i, %i)' % self.RGB 

431 

432 @property 

433 def str_rgba(self): 

434 ''' 

435 Red, green, blue and alpha floating point color components as string 

436 

437 Output will be of type 'rgba(<red>, <green>, <blue>, <alpha>)' 

438 ''' 

439 

440 return 'rgba(%5.3f, %5.3f, %5.3f, %5.3f)' % self.rgba 

441 

442 @property 

443 def str_RGBA(self): 

444 ''' 

445 Red, green, blue and alpha integer color components as string 

446 

447 Output will be of type 'RGBA(<red>, <green>, <blue>, <alpha>)' 

448 ''' 

449 

450 return 'RGBA(%i, %i, %i, %i)' % self.RGBA 

451 

452 def describe(self): 

453 ''' 

454 Returns all possible definitions of the color 

455 ''' 

456 

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)) 

469 

470 def __str__(self): 

471 return self.name__ if self.name__ is not None else self.str_rgba 

472 

473 

474class ColorGroup(Object): 

475 ''' 

476 Group of predefined colors. 

477 

478 Each ColorGroup has a name and a set of colornames and referring Color 

479 Objects 

480 ''' 

481 

482 name = String.T(optional=True) 

483 mapping = Dict.T(String.T(), Color.T()) 

484 

485 

486g_groups = [] 

487 

488for name, color_dict in [ 

489 ('tango', g_tango_colors), 

490 ('standard', g_standard_colors)]: 

491 

492 g_groups.append(ColorGroup( 

493 name=name, 

494 mapping=dict((k, Color(*v)) for (k, v) in color_dict.items()))) 

495 

496 for color in g_groups[-1].mapping.values(): 

497 color.use_hex_name() 

498 

499 

500def interpolate(a, b, blend, method='rgb'): 

501 assert method == 'rgb' 

502 assert 0.0 <= blend <= 1.0 

503 

504 c_rgba = tuple( 

505 a * (1.0-blend) + b * blend 

506 for (a, b) in zip(a.rgba, b.rgba)) 

507 

508 return Color(*c_rgba) 

509 

510 

511if __name__ == '__main__': 

512 

513 import sys 

514 

515 for g in g_groups: 

516 print(load_string(str(g))) 

517 

518 for s in sys.argv[1:]: 

519 

520 try: 

521 color = Color(s) 

522 except ColorError as e: 

523 sys.exit(str(e)) 

524 

525 print(color.describe())