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

84 

85g_named_colors = {} 

86 

87g_named_colors.update(g_tango_colors) 

88g_named_colors.update(g_standard_colors) 

89g_named_colors.update(g_nat_colors) 

90 

91 

92def parse_color(s): 

93 ''' 

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

95 

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. 

108 

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

110 :type s: string 

111 

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

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

114 ''' 

115 

116 orig_s = s 

117 

118 rgba = None 

119 

120 if s in g_named_colors: 

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

122 

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] 

129 

130 if len(s) == 4: 

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

132 

133 if len(s) == 6: 

134 s = s + 'FF' 

135 

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) 

143 

144 else: 

145 raise InvalidColorString(orig_s) 

146 

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. 

155 

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) 

162 

163 except ValueError: 

164 raise InvalidColorString(orig_s) 

165 

166 if rgba is None: 

167 raise InvalidColorString(orig_s) 

168 

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

170 raise InvalidColorString(orig_s) 

171 

172 return rgba 

173 

174 

175def to_int_255(f): 

176 ''' 

177 Convert floating point to integer color component 

178 

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

180 color component (range [0, 255]) 

181 

182 :param f: rgba floating point color component 

183 :type f: float 

184 

185 :return: RGBA integer color component 

186 :rtype: int 

187 ''' 

188 

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

192 

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

194 

195 

196def to_float_1(i): 

197 ''' 

198 Convert floating point to integer color component 

199 

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

201 color component (range [0.0, 1.0]) 

202 

203 :param i: RGBA integer color component 

204 :type i: int 

205 

206 :return: rgba floating point color component 

207 :rtype: float 

208 ''' 

209 

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

211 raise ColorError( 

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

213 

214 return i / 255. 

215 

216 

217def simplify_hex(s): 

218 ''' 

219 Simplifiy a hex color code if possible 

220 

221 E.g.: 

222 - #ffffffff -> #fff 

223 - #11aabbff -> #1ab 

224 

225 :param s: hex color string 

226 :type s: str 

227 

228 :return: simplified hex color string 

229 :rtype: str 

230 ''' 

231 

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

234 

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

236 

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

238 s = s[:7] 

239 

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

241 s = s[:4] 

242 

243 return s 

244 

245 

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

252 

253 

254class Color(SObject): 

255 ''' 

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

257 

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

259 ''' 

260 

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) 

266 

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

268 if len(args) == 1: 

269 SObject.__init__(self, init_props=False) 

270 self.name = args[0] 

271 

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

273 SObject.__init__(self, init_props=False) 

274 

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

276 if len(args) == 3: 

277 args = args + (255,) 

278 

279 self.RGBA = args 

280 

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

282 if len(args) == 3: 

283 args = args + (1.0,) 

284 

285 self.rgba = args 

286 

287 else: 

288 SObject.__init__(self, **kwargs) 

289 

290 @property 

291 def name(self): 

292 return self.name__ or '' 

293 

294 @name.setter 

295 def name(self, name): 

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

297 self.name__ = name 

298 

299 @property 

300 def r(self): 

301 ''' 

302 Red floating point color component 

303 ''' 

304 

305 return self.r__ 

306 

307 @r.setter 

308 def r(self, r): 

309 self.name__ = None 

310 self.r__ = r 

311 

312 @property 

313 def g(self): 

314 ''' 

315 Green floating point color component 

316 ''' 

317 

318 return self.g__ 

319 

320 @g.setter 

321 def g(self, g): 

322 self.name__ = None 

323 self.g__ = g 

324 

325 @property 

326 def b(self): 

327 ''' 

328 Blue floating point color component 

329 ''' 

330 

331 return self.b__ 

332 

333 @b.setter 

334 def b(self, b): 

335 self.name__ = None 

336 self.b__ = b 

337 

338 @property 

339 def a(self): 

340 ''' 

341 Transparency (alpha) floating point color component 

342 ''' 

343 

344 return self.a__ 

345 

346 @a.setter 

347 def a(self, a): 

348 self.name__ = None 

349 self.a__ = a 

350 

351 @property 

352 def rgb(self): 

353 ''' 

354 Red, green and blue floating point color components 

355 ''' 

356 

357 return self.r__, self.g__, self.b__ 

358 

359 @rgb.setter 

360 def rgb(self, rgb): 

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

362 self.name__ = None 

363 

364 @property 

365 def rgba(self): 

366 ''' 

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

368 ''' 

369 

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

371 

372 @rgba.setter 

373 def rgba(self, rgba): 

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

375 self.name__ = None 

376 

377 @property 

378 def RGB(self): 

379 ''' 

380 Red, green and blue integer color components 

381 ''' 

382 

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

384 

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 

389 

390 @property 

391 def RGBA(self): 

392 ''' 

393 Red, green, blue and alpha integer color components 

394 ''' 

395 

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

397 

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 

402 

403 @property 

404 def str_hex(self): 

405 ''' 

406 Hex color string 

407 ''' 

408 

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

410 

411 def use_hex_name(self): 

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

413 

414 @property 

415 def str_rgb(self): 

416 ''' 

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

418 

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

420 ''' 

421 

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

423 

424 @property 

425 def str_RGB(self): 

426 ''' 

427 Red, green and blue integer color components as string 

428 

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

430 ''' 

431 

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

433 

434 @property 

435 def str_rgba(self): 

436 ''' 

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

438 

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

440 ''' 

441 

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

443 

444 @property 

445 def str_RGBA(self): 

446 ''' 

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

448 

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

450 ''' 

451 

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

453 

454 def describe(self): 

455 ''' 

456 Returns all possible definitions of the color 

457 ''' 

458 

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

471 

472 def __str__(self): 

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

474 

475 

476class ColorGroup(Object): 

477 ''' 

478 Group of predefined colors. 

479 

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

481 Objects 

482 ''' 

483 

484 name = String.T(optional=True) 

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

486 

487 

488g_groups = [] 

489 

490for name, color_dict in [ 

491 ('tango', g_tango_colors), 

492 ('standard', g_standard_colors), 

493 ('nat', g_nat_colors)]: 

494 

495 g_groups.append(ColorGroup( 

496 name=name, 

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

498 

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

500 color.use_hex_name() 

501 

502if __name__ == '__main__': 

503 

504 import sys 

505 

506 for g in g_groups: 

507 print(load_string(str(g))) 

508 

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

510 

511 try: 

512 color = Color(s) 

513 except ColorError as e: 

514 sys.exit(str(e)) 

515 

516 print(color.describe())