Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/color.py: 76%

188 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-10-06 06:59 +0000

1# https://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

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

5 

6''' 

7Color utilities and built-in color palettes for a consistent look. 

8 

9.. _color: 

10 

11Color formats 

12............. 

13 

14.. list-table:: 

15 :widths: 15 85 

16 

17 * - ``RGB``, ``RGBA`` 

18 - Integer values for red, green blue, alpha, in the range ``[0, 255]``.:: 

19 

20 Color('RGBA(255, 255, 255, 255)') # from string 

21 Color('RGB(255, 255, 255)') 

22 Color(255, 255, 255, 255) # from values 

23 Color(255, 255, 255) 

24 

25 * - ``rgb``, ``rgba`` 

26 - Floating point values in the range ``[0.0, 1.0]``.:: 

27 

28 Color('rgba(1, 1, 1, 1)') # from string 

29 Color('rgb(1, 1, 1)') 

30 Color(1.0, 1.0, 1.0, 1.0) # from values 

31 Color(1.0, 1.0, 1.0) 

32 

33 * - ``hex`` 

34 - Hexadecimal value with 3, 4, 6 or 8 digits.:: 

35 

36 Color('#fff') 

37 Color('#ffff') 

38 Color('#ffffff') 

39 Color('#ffffffff') 

40 

41 * - ``name`` 

42 - See :py:data:`g_named_colors` for a complete list of 

43 available colors.:: 

44 

45 Color('white') 

46 Color('butter1') 

47 Color('skyblue2') 

48 

49 

50.. py:data:: g_named_colors 

51 

52 Dict mapping color names to :py:class:`Color` objects. 

53''' 

54 

55 

56import re 

57 

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

59 ValidationError, load_string 

60 

61 

62guts_prefix = 'pf' 

63 

64 

65class ColorError(ValueError): 

66 ''' 

67 Raised for invalid color specifications and conversions. 

68 ''' 

69 pass 

70 

71 

72class InvalidColorString(ColorError): 

73 ''' 

74 Raised for invalid color string definitions. 

75 ''' 

76 def __init__(self, s): 

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

78 

79 

80g_pattern_hex = re.compile( 

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

82 

83 

84g_pattern_rgb = re.compile( 

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

86 

87 

88g_tango_colors = { 

89 'butter1': (252, 233, 79), 

90 'butter2': (237, 212, 0), 

91 'butter3': (196, 160, 0), 

92 'chameleon1': (138, 226, 52), 

93 'chameleon2': (115, 210, 22), 

94 'chameleon3': (78, 154, 6), 

95 'orange1': (252, 175, 62), 

96 'orange2': (245, 121, 0), 

97 'orange3': (206, 92, 0), 

98 'skyblue1': (114, 159, 207), 

99 'skyblue2': (52, 101, 164), 

100 'skyblue3': (32, 74, 135), 

101 'plum1': (173, 127, 168), 

102 'plum2': (117, 80, 123), 

103 'plum3': (92, 53, 102), 

104 'chocolate1': (233, 185, 110), 

105 'chocolate2': (193, 125, 17), 

106 'chocolate3': (143, 89, 2), 

107 'scarletred1': (239, 41, 41), 

108 'scarletred2': (204, 0, 0), 

109 'scarletred3': (164, 0, 0), 

110 'aluminium1': (238, 238, 236), 

111 'aluminium2': (211, 215, 207), 

112 'aluminium3': (186, 189, 182), 

113 'aluminium4': (136, 138, 133), 

114 'aluminium5': (85, 87, 83), 

115 'aluminium6': (46, 52, 54)} 

116 

117 

118g_standard_colors = { 

119 'white': (255, 255, 255), 

120 'black': (0, 0, 0), 

121 'red': (255, 0, 0), 

122 'green': (0, 255, 0), 

123 'blue': (0, 0, 255)} 

124 

125 

126g_named_colors = {} 

127 

128g_named_colors.update(g_tango_colors) 

129g_named_colors.update(g_standard_colors) 

130 

131 

132def parse_color(s): 

133 ''' 

134 Translate color string into :ref:`rgba tuple <color>`. 

135 

136 :param s: :ref:`Color definition <color>`. 

137 :type s: str 

138 

139 :return: Color value in ``rgba`` form. 

140 :rtype: :py:class:`tuple` of 4 :py:class:`float` 

141 ''' 

142 

143 orig_s = s 

144 

145 rgba = None 

146 

147 if s in g_named_colors: 

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

149 

150 else: 

151 m = g_pattern_hex.match(s) 

152 if m: 

153 s = s[1:] 

154 if len(s) == 3: 

155 s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2] 

156 

157 if len(s) == 4: 

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

159 

160 if len(s) == 6: 

161 s = s + 'FF' 

162 

163 if len(s) == 8: 

164 try: 

165 rgba = tuple( 

166 int(s[i*2:i*2+2], base=16) / 255. 

167 for i in range(4)) 

168 except ValueError: 

169 raise InvalidColorString(orig_s) 

170 

171 else: 

172 raise InvalidColorString(orig_s) 

173 

174 m = g_pattern_rgb.match(s) 

175 if m: 

176 rgb_mode = m.group(1) 

177 if rgb_mode.startswith('rgb'): 

178 typ = float 

179 else: 

180 def typ(x): 

181 return int(x) / 255. 

182 

183 try: 

184 rgba = ( 

185 typ(m.group(2)), 

186 typ(m.group(3)), 

187 typ(m.group(4)), 

188 typ(m.group(6)) if m.group(6) else 1.0) 

189 

190 except ValueError: 

191 raise InvalidColorString(orig_s) 

192 

193 if rgba is None: 

194 raise InvalidColorString(orig_s) 

195 

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

197 raise InvalidColorString(orig_s) 

198 

199 return rgba 

200 

201 

202def to_int_255(f): 

203 ''' 

204 Convert floating point to integer color component 

205 

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

207 color component (range [0, 255]) 

208 

209 :param f: rgba floating point color component 

210 :type f: float 

211 

212 :return: RGBA integer color component 

213 :rtype: int 

214 ''' 

215 

216 if not (0.0 <= f <= 1.0): 

217 raise ColorError( 

218 'Floating point color component must be in the range [0.0, 1.0]') 

219 

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

221 

222 

223def to_float_1(i): 

224 ''' 

225 Convert integer to floating point color component 

226 

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

228 color component (range [0.0, 1.0]) 

229 

230 :param i: RGBA integer color component 

231 :type i: int 

232 

233 :return: rgba floating point color component 

234 :rtype: float 

235 ''' 

236 

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

238 raise ColorError( 

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

240 

241 return i / 255. 

242 

243 

244def simplify_hex(s): 

245 ''' 

246 Simplifiy a hex color code if possible 

247 

248 E.g.: 

249 - #ffffffff -> #fff 

250 - #11aabbff -> #1ab 

251 

252 :param s: hex color string 

253 :type s: str 

254 

255 :return: simplified hex color string 

256 :rtype: str 

257 ''' 

258 

259 if s[1] == s[2] and s[3] == s[4] and s[5] == s[6] \ 

260 and (len(s) == 9 and s[7] == s[8]): 

261 

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

263 

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

265 s = s[:7] 

266 

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

268 s = s[:4] 

269 

270 return s 

271 

272 

273class Component(Float): 

274 class __T(TBase): 

275 def validate_extra(self, x): 

276 if not (0.0 <= x <= 1.0): 

277 raise ValidationError( 

278 'Color component must be in the range [0.0, 1.0]') 

279 

280 

281class Color(SObject): 

282 ''' 

283 Color with red, green, blue and alpha values. 

284 

285 The color can be specified as a :ref:`color string <color>` or by giving 

286 the component values in :py:class:`int` or :py:class:`float` format. 

287 

288 Examples:: 

289 

290 Color('black') 

291 Color('RGBA(255, 255, 255, 255)') 

292 Color('RGB(255, 255, 255)') 

293 Color('rgba(1.0, 1.0, 1.0, 1.0')') 

294 Color('rgb(1.0, 1.0, 1.0')') 

295 Color(255, 255, 255, 255) 

296 Color(255, 255, 255) 

297 Color(1.0, 1.0, 1.0, 1.0) 

298 Color(1.0, 1.0, 1.0) 

299 Color(r=1.0, g=1.0, b=1.0, a=1.0) 

300 

301 The internal representation is ``rgba``. 

302 ''' 

303 

304 name__ = String.T(optional=True) 

305 r__ = Component.T( 

306 default=0.0, 

307 help='Red component ``[0., 1.]``.') 

308 g__ = Component.T( 

309 default=0.0, 

310 help='Green component ``[0., 1.]``.') 

311 b__ = Component.T( 

312 default=0.0, 

313 help='Blue component ``[0., 1.]``.') 

314 a__ = Component.T( 

315 default=1.0, 

316 help='Alpha (opacity) component ``[0., 1.]``.') 

317 

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

319 if len(args) == 1: 

320 SObject.__init__(self, init_props=False) 

321 self.name = args[0] 

322 

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

324 SObject.__init__(self, init_props=False) 

325 

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

327 if len(args) == 3: 

328 args = args + (255,) 

329 

330 self.RGBA = args 

331 

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

333 if len(args) == 3: 

334 args = args + (1.0,) 

335 

336 self.rgba = args 

337 

338 else: 

339 SObject.__init__(self, init_props=False) 

340 self.name__ = kwargs.get('name', None) 

341 self.r__ = kwargs.get('r', 0.0) 

342 self.g__ = kwargs.get('g', 0.0) 

343 self.b__ = kwargs.get('b', 0.0) 

344 self.a__ = kwargs.get('a', 1.0) 

345 

346 def __eq__(self, other): 

347 return self.name__ == other.name__ \ 

348 and self.r__ == other.r__ \ 

349 and self.g__ == other.g__ \ 

350 and self.b__ == other.b__ \ 

351 and self.a__ == other.a__ 

352 

353 @property 

354 def name(self): 

355 return self.name__ or '' 

356 

357 @name.setter 

358 def name(self, name): 

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

360 self.name__ = name 

361 

362 @property 

363 def r(self): 

364 return self.r__ 

365 

366 @r.setter 

367 def r(self, r): 

368 self.name__ = None 

369 self.r__ = r 

370 

371 @property 

372 def g(self): 

373 return self.g__ 

374 

375 @g.setter 

376 def g(self, g): 

377 self.name__ = None 

378 self.g__ = g 

379 

380 @property 

381 def b(self): 

382 return self.b__ 

383 

384 @b.setter 

385 def b(self, b): 

386 self.name__ = None 

387 self.b__ = b 

388 

389 @property 

390 def a(self): 

391 return self.a__ 

392 

393 @a.setter 

394 def a(self, a): 

395 self.name__ = None 

396 self.a__ = a 

397 

398 @property 

399 def rgb(self): 

400 ''' 

401 Red, green and blue floating point color components. 

402 ''' 

403 

404 return self.r__, self.g__, self.b__ 

405 

406 @rgb.setter 

407 def rgb(self, rgb): 

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

409 self.name__ = None 

410 

411 @property 

412 def rgba(self): 

413 ''' 

414 Red, green, blue and alpha floating point color components. 

415 ''' 

416 

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

418 

419 @rgba.setter 

420 def rgba(self, rgba): 

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

422 self.name__ = None 

423 

424 @property 

425 def RGB(self): 

426 ''' 

427 Red, green and blue integer color components. 

428 ''' 

429 

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

431 

432 @RGB.setter 

433 def RGB(self, RGB): 

434 self.r__, self.g__, self.b__ = (to_float_1(x) for x in RGB) 

435 self.name__ = None 

436 

437 @property 

438 def RGBA(self): 

439 ''' 

440 Red, green, blue and alpha integer color components. 

441 ''' 

442 

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

444 

445 @RGBA.setter 

446 def RGBA(self, RGBA): 

447 self.r__, self.g__, self.b__, self.a__ = (to_float_1(x) for x in RGBA) 

448 self.name__ = None 

449 

450 @property 

451 def str_hex(self): 

452 ''' 

453 Hex color string. 

454 ''' 

455 

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

457 

458 def use_hex_name(self): 

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

460 

461 @property 

462 def str_rgb(self): 

463 ''' 

464 Red, green and blue floating point color components as string. 

465 

466 Output will be like ``'rgb(<red>, <green>, <blue>)'.`` 

467 ''' 

468 

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

470 

471 @property 

472 def str_RGB(self): 

473 ''' 

474 Red, green and blue integer color components as string. 

475 

476 Output will be like ``'RGB(<red>, <green>, <blue>)'`` 

477 ''' 

478 

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

480 

481 @property 

482 def str_rgba(self): 

483 ''' 

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

485 

486 Output will be like ``'rgba(<red>, <green>, <blue>, <alpha>)'``. 

487 ''' 

488 

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

490 

491 @property 

492 def str_RGBA(self): 

493 ''' 

494 Red, green, blue and alpha integer color components as string' 

495 

496 Output will be like ``'RGBA(<red>, <green>, <blue>, <alpha>)'``. 

497 ''' 

498 

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

500 

501 def describe(self): 

502 ''' 

503 Returns all possible definitions of the color. 

504 ''' 

505 

506 return ''' 

507 name: %s 

508 hex: %s 

509 RGBA: %s 

510 rgba: %s 

511 str: %s 

512''' % ( 

513 self.name, 

514 self.str_hex, 

515 self.str_RGBA, 

516 self.str_rgba, 

517 str(self)) 

518 

519 def __str__(self): 

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

521 

522 

523class ColorGroup(Object): 

524 ''' 

525 Group of predefined colors. 

526 

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

528 Objects 

529 ''' 

530 

531 name = String.T(optional=True) 

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

533 

534 

535g_groups = [] 

536 

537for name, color_dict in [ 

538 ('tango', g_tango_colors), 

539 ('standard', g_standard_colors)]: 

540 

541 g_groups.append(ColorGroup( 

542 name=name, 

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

544 

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

546 color.use_hex_name() 

547 

548 

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

550 assert method == 'rgb' 

551 assert 0.0 <= blend <= 1.0 

552 

553 c_rgba = tuple( 

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

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

556 

557 return Color(*c_rgba) 

558 

559 

560if __name__ == '__main__': 

561 

562 import sys 

563 

564 for g in g_groups: 

565 print(load_string(str(g))) 

566 

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

568 

569 try: 

570 color = Color(s) 

571 except ColorError as e: 

572 sys.exit(str(e)) 

573 

574 print(color.describe())