Coverage for backpack/annotation/color.py: 88%
73 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-30 23:12 +0000
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-30 23:12 +0000
1''' This module defines the :class:`~backpack.annotation.color.Color` class, an abstraction over
2RGBA colors, as well ass :class:`~backpack.annotation.color.ColorMap`, that lets you easily access
3frequently used colors.
4'''
6from dataclasses import dataclass
7from typing import Union, Optional, Sequence, Mapping, OrderedDict
8import collections
9import hashlib
11@dataclass(frozen=True)
12class Color:
13 ''' A color in the red, blue, green space.
15 The color coordinates are integers in the [0; 255] range.
17 Args:
18 r (int): The red component of the color
19 g (int): The green component of the color
20 b (int): The blue component of the color
21 alpha (float): The alpha channel of transparency, ranged from `0` to `1`
22 '''
23 r: int
24 ''' The red component of the color. '''
25 g: int
26 ''' The green component of the color. '''
27 b: int
28 ''' The blue component of the color. '''
29 alpha: float = 1.0
30 ''' The alpha component of transparency. '''
32 @classmethod
33 def from_hex(cls, value: Union[str, int]) -> 'Color':
34 ''' Creates a color object from its hexadecimal representation.
36 Args:
37 value: integer or HTML color string
39 Returns:
40 a new color object
41 '''
42 if isinstance(value, str):
43 value = value.lstrip('#')
44 rgb = tuple(int(value[i:i+2], 16) for i in (0, 2, 4))
45 return cls(*rgb)
46 elif isinstance(value, int):
47 rgb = tuple((value & (0xff << (i * 8))) >> (i * 8) for i in (2, 1, 0))
48 return cls(*rgb)
49 else:
50 raise ValueError('Value argument must be str or int')
52 @classmethod
53 def from_value(cls, value: Union[str, int, Sequence, Mapping, 'Color']):
54 ''' Converts an integer (interpreted as 3 bytes hex value), a HTML color string, a
55 sequence of 3 or 4 integers, or a dictionary containing 'r', 'g', 'b' and optionally
56 'alpha' keys to a Color object.
58 Args:
59 value: The value to be converted.
61 Returns:
62 The new Color object.
64 Raises:
65 ValueError: If the conversion was not successful.
66 '''
67 if isinstance(value, Color):
68 return value
69 if isinstance(value, (str, int)):
70 return cls.from_hex(value)
71 elif (
72 isinstance(value, collections.abc.Sequence) and
73 (len(value) == 3 or len(value) == 4) and
74 all(isinstance(e, int if idx < 3 else float) for idx, e in enumerate(value))
75 ):
76 return cls(*value)
77 elif (
78 isinstance(value, collections.abc.Mapping) and
79 'r' in value and 'g' in value and 'b' in value
80 ):
81 params = {k: v for k, v in value.items() if k in ('r', 'g', 'b')}
82 alpha = value.get('a', value.get('alpha'))
83 if alpha is not None:
84 params['alpha'] = alpha
85 return cls(**params)
86 else:
87 raise ValueError(f'Could not convert {value} to a Color')
89 @classmethod
90 def from_id(cls, identifier: int, salt: str = 'salt') -> 'Color':
91 ''' Creates a pseudo-random color from an integer identifier.
93 For the same identifier and salt the same color will be generated.
95 Args:
96 identifier: the identifier
97 salt: the salt, change this if you want a different color for the same identifier
99 Returns:
100 A pseudo-random color based on the identifier and the salt.
101 '''
102 h = hashlib.md5((salt + str(identifier)).encode('utf-8')).digest()
103 return Color(h[0], h[1], h[2])
105 def brightness(self, brightness: float) -> 'Color':
106 ''' Returns a new Color instance with changed brightness.
108 Args:
109 brightness: The new brightness, if greater than 1, a brighter color will be returned,
110 if smaller than 1, a darker color.
112 Returns:
113 A new color instance with changed brightness.
114 '''
115 conv = lambda ch: min(255, int(ch * brightness))
116 return Color(r=conv(self.r), g=conv(self.g), b=conv(self.b), alpha=self.alpha)
118 def with_alpha(self, alpha: float) -> 'Color':
119 ''' Returns a new Color instance with changed alpha.
121 Args:
122 alpha: The new alpha.
124 Returns:
125 A new color instance with changed alpha.
126 '''
127 return Color(r=self.r, g=self.g, b=self.b, alpha=alpha)
130class ColorMap:
131 ''' A simply color map implementation. '''
133 colors: Mapping[str, Color] = OrderedDict()
134 ''' A map of colors by name. Subclasses are supposed to override this argument. '''
136 @classmethod
137 def from_name(cls, name: str) -> Optional[Color]:
138 ''' Returns a color from its name. '''
139 return cls.colors.get(name)
141 @classmethod
142 def as_list(cls) -> Sequence[Color]:
143 ''' Returns the colors of this color map as a list. '''
144 return list(cls.colors.values())
146 @classmethod
147 def color_from_id(cls, identifier: int) -> Color:
148 colors = cls.as_list()
149 return colors[identifier % len(colors)]
152class HTMLColors(ColorMap):
153 ''' HTML Basic colors as of https://en.wikipedia.org/wiki/Web_colors#HTML_color_names '''
155 WHITE = Color(255, 255, 255)
156 SILVER = Color(192, 192, 192)
157 GRAY = Color(128, 128, 128)
158 BLACK = Color( 0, 0, 0)
159 RED = Color(255, 0, 0)
160 MAROON = Color(128, 128, 128)
161 YELLOW = Color(255, 255, 0)
162 OLIVE = Color(128, 128, 0)
163 LIME = Color( 0, 255, 0)
164 GREEN = Color( 0, 128, 0)
165 AQUA = Color( 0, 255, 255)
166 TEAL = Color(255, 128, 128)
167 BLUE = Color( 0, 0, 255)
168 NAVY = Color( 0, 0, 128)
169 FUCHSIA = Color(255, 0, 255)
170 PURPLE = Color(128, 0, 128)
172 colors: Mapping[str, Color] = OrderedDict([
173 ('white', WHITE),
174 ('silver', SILVER),
175 ('gray', GRAY),
176 ('black', BLACK),
177 ('red', RED),
178 ('maroon', MAROON),
179 ('yellow', YELLOW),
180 ('olive', OLIVE),
181 ('lime', LIME),
182 ('green', GREEN),
183 ('aqua', AQUA),
184 ('teal', TEAL),
185 ('blue', BLUE),
186 ('navy', NAVY),
187 ('fuchsia', FUCHSIA),
188 ('purple', PURPLE)
189 ])