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

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

5 

6from dataclasses import dataclass 

7from typing import Union, Optional, Sequence, Mapping, OrderedDict 

8import collections 

9import hashlib 

10 

11@dataclass(frozen=True) 

12class Color: 

13 ''' A color in the red, blue, green space. 

14 

15 The color coordinates are integers in the [0; 255] range. 

16 

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. ''' 

31 

32 @classmethod 

33 def from_hex(cls, value: Union[str, int]) -> 'Color': 

34 ''' Creates a color object from its hexadecimal representation. 

35 

36 Args: 

37 value: integer or HTML color string 

38 

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

51 

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. 

57 

58 Args: 

59 value: The value to be converted. 

60 

61 Returns: 

62 The new Color object. 

63 

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

88 

89 @classmethod 

90 def from_id(cls, identifier: int, salt: str = 'salt') -> 'Color': 

91 ''' Creates a pseudo-random color from an integer identifier. 

92 

93 For the same identifier and salt the same color will be generated. 

94 

95 Args: 

96 identifier: the identifier 

97 salt: the salt, change this if you want a different color for the same identifier 

98 

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

104 

105 def brightness(self, brightness: float) -> 'Color': 

106 ''' Returns a new Color instance with changed brightness. 

107 

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. 

111 

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) 

117 

118 def with_alpha(self, alpha: float) -> 'Color': 

119 ''' Returns a new Color instance with changed alpha. 

120 

121 Args: 

122 alpha: The new alpha. 

123 

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) 

128 

129 

130class ColorMap: 

131 ''' A simply color map implementation. ''' 

132 

133 colors: Mapping[str, Color] = OrderedDict() 

134 ''' A map of colors by name. Subclasses are supposed to override this argument. ''' 

135 

136 @classmethod 

137 def from_name(cls, name: str) -> Optional[Color]: 

138 ''' Returns a color from its name. ''' 

139 return cls.colors.get(name) 

140 

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

145 

146 @classmethod 

147 def color_from_id(cls, identifier: int) -> Color: 

148 colors = cls.as_list() 

149 return colors[identifier % len(colors)] 

150 

151 

152class HTMLColors(ColorMap): 

153 ''' HTML Basic colors as of https://en.wikipedia.org/wiki/Web_colors#HTML_color_names ''' 

154 

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) 

171 

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