1# https://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

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

5 

6import re 

7 

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

9 ValidationError, load_string 

10 

11 

12guts_prefix = 'pf' 

13 

14 

15class ColorError(ValueError): 

16 pass 

17 

18 

19class InvalidColorString(ColorError): 

20 def __init__(self, s): 

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

22 

23 

24g_pattern_hex = re.compile( 

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

26 

27 

28g_pattern_rgb = re.compile( 

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

30 

31 

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

60 

61 

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

68 

69 

70g_named_colors = {} 

71 

72g_named_colors.update(g_tango_colors) 

73g_named_colors.update(g_standard_colors) 

74 

75 

76def parse_color(s): 

77 ''' 

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

79 

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. 

92 

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

94 :type s: string 

95 

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

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

98 ''' 

99 

100 orig_s = s 

101 

102 rgba = None 

103 

104 if s in g_named_colors: 

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

106 

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] 

113 

114 if len(s) == 4: 

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

116 

117 if len(s) == 6: 

118 s = s + 'FF' 

119 

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) 

127 

128 else: 

129 raise InvalidColorString(orig_s) 

130 

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. 

139 

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) 

146 

147 except ValueError: 

148 raise InvalidColorString(orig_s) 

149 

150 if rgba is None: 

151 raise InvalidColorString(orig_s) 

152 

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

154 raise InvalidColorString(orig_s) 

155 

156 return rgba 

157 

158 

159def to_int_255(f): 

160 ''' 

161 Convert floating point to integer color component 

162 

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

164 color component (range [0, 255]) 

165 

166 :param f: rgba floating point color component 

167 :type f: float 

168 

169 :return: RGBA integer color component 

170 :rtype: int 

171 ''' 

172 

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

176 

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

178 

179 

180def to_float_1(i): 

181 ''' 

182 Convert floating point to integer color component 

183 

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

185 color component (range [0.0, 1.0]) 

186 

187 :param i: RGBA integer color component 

188 :type i: int 

189 

190 :return: rgba floating point color component 

191 :rtype: float 

192 ''' 

193 

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

195 raise ColorError( 

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

197 

198 return i / 255. 

199 

200 

201def simplify_hex(s): 

202 ''' 

203 Simplifiy a hex color code if possible 

204 

205 E.g.: 

206 - #ffffffff -> #fff 

207 - #11aabbff -> #1ab 

208 

209 :param s: hex color string 

210 :type s: str 

211 

212 :return: simplified hex color string 

213 :rtype: str 

214 ''' 

215 

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

218 

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

220 

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

222 s = s[:7] 

223 

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

225 s = s[:4] 

226 

227 return s 

228 

229 

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

236 

237 

238class Color(SObject): 

239 ''' 

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

241 

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

243 ''' 

244 

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) 

250 

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

252 if len(args) == 1: 

253 SObject.__init__(self, init_props=False) 

254 self.name = args[0] 

255 

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

257 SObject.__init__(self, init_props=False) 

258 

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

260 if len(args) == 3: 

261 args = args + (255,) 

262 

263 self.RGBA = args 

264 

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

266 if len(args) == 3: 

267 args = args + (1.0,) 

268 

269 self.rgba = args 

270 

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) 

278 

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__ 

285 

286 @property 

287 def name(self): 

288 return self.name__ or '' 

289 

290 @name.setter 

291 def name(self, name): 

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

293 self.name__ = name 

294 

295 @property 

296 def r(self): 

297 ''' 

298 Red floating point color component 

299 ''' 

300 

301 return self.r__ 

302 

303 @r.setter 

304 def r(self, r): 

305 self.name__ = None 

306 self.r__ = r 

307 

308 @property 

309 def g(self): 

310 ''' 

311 Green floating point color component 

312 ''' 

313 

314 return self.g__ 

315 

316 @g.setter 

317 def g(self, g): 

318 self.name__ = None 

319 self.g__ = g 

320 

321 @property 

322 def b(self): 

323 ''' 

324 Blue floating point color component 

325 ''' 

326 

327 return self.b__ 

328 

329 @b.setter 

330 def b(self, b): 

331 self.name__ = None 

332 self.b__ = b 

333 

334 @property 

335 def a(self): 

336 ''' 

337 Transparency (alpha) floating point color component 

338 ''' 

339 

340 return self.a__ 

341 

342 @a.setter 

343 def a(self, a): 

344 self.name__ = None 

345 self.a__ = a 

346 

347 @property 

348 def rgb(self): 

349 ''' 

350 Red, green and blue floating point color components 

351 ''' 

352 

353 return self.r__, self.g__, self.b__ 

354 

355 @rgb.setter 

356 def rgb(self, rgb): 

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

358 self.name__ = None 

359 

360 @property 

361 def rgba(self): 

362 ''' 

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

364 ''' 

365 

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

367 

368 @rgba.setter 

369 def rgba(self, rgba): 

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

371 self.name__ = None 

372 

373 @property 

374 def RGB(self): 

375 ''' 

376 Red, green and blue integer color components 

377 ''' 

378 

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

380 

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 

385 

386 @property 

387 def RGBA(self): 

388 ''' 

389 Red, green, blue and alpha integer color components 

390 ''' 

391 

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

393 

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 

398 

399 @property 

400 def str_hex(self): 

401 ''' 

402 Hex color string 

403 ''' 

404 

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

406 

407 def use_hex_name(self): 

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

409 

410 @property 

411 def str_rgb(self): 

412 ''' 

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

414 

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

416 ''' 

417 

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

419 

420 @property 

421 def str_RGB(self): 

422 ''' 

423 Red, green and blue integer color components as string 

424 

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

426 ''' 

427 

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

429 

430 @property 

431 def str_rgba(self): 

432 ''' 

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

434 

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

436 ''' 

437 

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

439 

440 @property 

441 def str_RGBA(self): 

442 ''' 

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

444 

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

446 ''' 

447 

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

449 

450 def describe(self): 

451 ''' 

452 Returns all possible definitions of the color 

453 ''' 

454 

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

467 

468 def __str__(self): 

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

470 

471 

472class ColorGroup(Object): 

473 ''' 

474 Group of predefined colors. 

475 

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

477 Objects 

478 ''' 

479 

480 name = String.T(optional=True) 

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

482 

483 

484g_groups = [] 

485 

486for name, color_dict in [ 

487 ('tango', g_tango_colors), 

488 ('standard', g_standard_colors)]: 

489 

490 g_groups.append(ColorGroup( 

491 name=name, 

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

493 

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

495 color.use_hex_name() 

496 

497 

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

499 assert method == 'rgb' 

500 assert 0.0 <= blend <= 1.0 

501 

502 c_rgba = tuple( 

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

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

505 

506 return Color(*c_rgba) 

507 

508 

509if __name__ == '__main__': 

510 

511 import sys 

512 

513 for g in g_groups: 

514 print(load_string(str(g))) 

515 

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

517 

518 try: 

519 color = Color(s) 

520 except ColorError as e: 

521 sys.exit(str(e)) 

522 

523 print(color.describe())