Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/faker/providers/color/color.py: 14%

128 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1"""Internal module for human-friendly color generation. 

2 

3.. important:: 

4 End users of this library should not use anything in this module. 

5 

6Code adapted from: 

7- https://github.com/davidmerfield/randomColor (CC0) 

8- https://github.com/kevinwuhoo/randomcolor-py (MIT License) 

9 

10Additional reference from: 

11- https://en.wikipedia.org/wiki/HSL_and_HSV 

12""" 

13 

14import colorsys 

15import math 

16import random 

17import sys 

18 

19from typing import TYPE_CHECKING, Dict, Hashable, Optional, Sequence, Tuple 

20 

21if TYPE_CHECKING: 21 ↛ 22line 21 didn't jump to line 22, because the condition on line 21 was never true

22 from ...factory import Generator 

23 

24from ...typing import HueType 

25 

26COLOR_MAP: Dict[str, Dict[str, Sequence[Tuple[int, int]]]] = { 

27 "monochrome": { 

28 "hue_range": [(0, 0)], 

29 "lower_bounds": [ 

30 (0, 0), 

31 (100, 0), 

32 ], 

33 }, 

34 "red": { 

35 "hue_range": [(-26, 18)], 

36 "lower_bounds": [ 

37 (20, 100), 

38 (30, 92), 

39 (40, 89), 

40 (50, 85), 

41 (60, 78), 

42 (70, 70), 

43 (80, 60), 

44 (90, 55), 

45 (100, 50), 

46 ], 

47 }, 

48 "orange": { 

49 "hue_range": [(19, 46)], 

50 "lower_bounds": [ 

51 (20, 100), 

52 (30, 93), 

53 (40, 88), 

54 (50, 86), 

55 (60, 85), 

56 (70, 70), 

57 (100, 70), 

58 ], 

59 }, 

60 "yellow": { 

61 "hue_range": [(47, 62)], 

62 "lower_bounds": [ 

63 (25, 100), 

64 (40, 94), 

65 (50, 89), 

66 (60, 86), 

67 (70, 84), 

68 (80, 82), 

69 (90, 80), 

70 (100, 75), 

71 ], 

72 }, 

73 "green": { 

74 "hue_range": [(63, 178)], 

75 "lower_bounds": [ 

76 (30, 100), 

77 (40, 90), 

78 (50, 85), 

79 (60, 81), 

80 (70, 74), 

81 (80, 64), 

82 (90, 50), 

83 (100, 40), 

84 ], 

85 }, 

86 "blue": { 

87 "hue_range": [(179, 257)], 

88 "lower_bounds": [ 

89 (20, 100), 

90 (30, 86), 

91 (40, 80), 

92 (50, 74), 

93 (60, 60), 

94 (70, 52), 

95 (80, 44), 

96 (90, 39), 

97 (100, 35), 

98 ], 

99 }, 

100 "purple": { 

101 "hue_range": [(258, 282)], 

102 "lower_bounds": [ 

103 (20, 100), 

104 (30, 87), 

105 (40, 79), 

106 (50, 70), 

107 (60, 65), 

108 (70, 59), 

109 (80, 52), 

110 (90, 45), 

111 (100, 42), 

112 ], 

113 }, 

114 "pink": { 

115 "hue_range": [(283, 334)], 

116 "lower_bounds": [ 

117 (20, 100), 

118 (30, 90), 

119 (40, 86), 

120 (60, 84), 

121 (80, 80), 

122 (90, 75), 

123 (100, 73), 

124 ], 

125 }, 

126} 

127 

128 

129class RandomColor: 

130 """Implement random color generation in a human-friendly way. 

131 

132 This helper class encapsulates the internal implementation and logic of the 

133 :meth:`color() <faker.providers.color.Provider.color>` method. 

134 """ 

135 

136 def __init__(self, generator: Optional["Generator"] = None, seed: Optional[Hashable] = None) -> None: 

137 self.colormap = COLOR_MAP 

138 

139 # Option to specify a seed was not removed so this class 

140 # can still be tested independently w/o generators 

141 if generator: 

142 self.random = generator.random 

143 else: 

144 self.seed = seed if seed else random.randint(0, sys.maxsize) 

145 self.random = random.Random(self.seed) 

146 

147 for color_name, color_attrs in self.colormap.items(): 

148 lower_bounds: Sequence[Tuple[int, int]] = color_attrs["lower_bounds"] 

149 s_min, b_max = lower_bounds[0] 

150 s_max, b_min = lower_bounds[-1] 

151 

152 self.colormap[color_name]["saturation_range"] = [(s_min, s_max)] 

153 self.colormap[color_name]["brightness_range"] = [(b_min, b_max)] 

154 

155 def generate( 

156 self, 

157 hue: Optional[HueType] = None, 

158 luminosity: Optional[str] = None, 

159 color_format: str = "hex", 

160 ) -> str: 

161 """Generate a color. 

162 

163 Whenever :meth:`color() <faker.providers.color.Provider.color>` is 

164 called, the arguments used are simply passed into this method, and this 

165 method handles the rest. 

166 """ 

167 # First we pick a hue (H) 

168 h = self.pick_hue(hue) 

169 

170 # Then use H to determine saturation (S) 

171 s = self.pick_saturation(h, hue, luminosity) 

172 

173 # Then use S and H to determine brightness (B). 

174 b = self.pick_brightness(h, s, luminosity) 

175 

176 # Then we return the HSB color in the desired format 

177 return self.set_format((h, s, b), color_format) 

178 

179 def pick_hue(self, hue: Optional[HueType]) -> int: 

180 """Return a numerical hue value.""" 

181 hue_ = self.random_within(self.get_hue_range(hue)) 

182 

183 # Instead of storing red as two separate ranges, 

184 # we group them, using negative numbers 

185 if hue_ < 0: 

186 hue_ += 360 

187 

188 return hue_ 

189 

190 def pick_saturation(self, hue: int, hue_name: Optional[HueType], luminosity: Optional[str]) -> int: 

191 """Return a numerical saturation value.""" 

192 if luminosity is None: 

193 luminosity = "" 

194 if luminosity == "random": 

195 return self.random_within((0, 100)) 

196 

197 if isinstance(hue_name, str) and hue_name == "monochrome": 

198 return 0 

199 

200 s_min, s_max = self.get_saturation_range(hue) 

201 

202 if luminosity == "bright": 

203 s_min = 55 

204 elif luminosity == "dark": 

205 s_min = s_max - 10 

206 elif luminosity == "light": 

207 s_max = 55 

208 

209 return self.random_within((s_min, s_max)) 

210 

211 def pick_brightness(self, h: int, s: int, luminosity: Optional[str]) -> int: 

212 """Return a numerical brightness value.""" 

213 if luminosity is None: 

214 luminosity = "" 

215 

216 b_min = self.get_minimum_brightness(h, s) 

217 b_max = 100 

218 

219 if luminosity == "dark": 

220 b_max = b_min + 20 

221 elif luminosity == "light": 

222 b_min = (b_max + b_min) // 2 

223 elif luminosity == "random": 

224 b_min = 0 

225 b_max = 100 

226 

227 return self.random_within((b_min, b_max)) 

228 

229 def set_format(self, hsv: Tuple[int, int, int], color_format: str) -> str: 

230 """Handle conversion of HSV values into desired format.""" 

231 if color_format == "hsv": 

232 color = f"hsv({hsv[0]}, {hsv[1]}, {hsv[2]})" 

233 

234 elif color_format == "hsl": 

235 hsl = self.hsv_to_hsl(hsv) 

236 color = f"hsl({hsl[0]}, {hsl[1]}, {hsl[2]})" 

237 

238 elif color_format == "rgb": 

239 rgb = self.hsv_to_rgb(hsv) 

240 color = f"rgb({rgb[0]}, {rgb[1]}, {rgb[2]})" 

241 

242 else: 

243 rgb = self.hsv_to_rgb(hsv) 

244 color = f"#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}" 

245 

246 return color 

247 

248 def get_minimum_brightness(self, h: int, s: int) -> int: 

249 """Return the minimum allowed brightness for ``h`` and ``s``.""" 

250 lower_bounds: Sequence[Tuple[int, int]] = self.get_color_info(h)["lower_bounds"] 

251 

252 for i in range(len(lower_bounds) - 1): 

253 s1, v1 = lower_bounds[i] 

254 s2, v2 = lower_bounds[i + 1] 

255 

256 if s1 <= s <= s2: 

257 m: float = (v2 - v1) / (s2 - s1) 

258 b: float = v1 - m * s1 

259 

260 return int(m * s + b) 

261 

262 return 0 

263 

264 def get_hue_range(self, color_input: Optional[HueType]) -> Tuple[int, int]: 

265 """Return the hue range for a given ``color_input``.""" 

266 if isinstance(color_input, (int, float)) and 0 <= color_input <= 360: 

267 color_input = int(color_input) 

268 return (color_input, color_input) 

269 elif isinstance(color_input, str) and color_input in self.colormap: 

270 return self.colormap[color_input]["hue_range"][0] 

271 elif color_input is None: 

272 return (0, 360) 

273 

274 if isinstance(color_input, list): 

275 color_input = tuple(color_input) 

276 if ( 

277 isinstance(color_input, tuple) 

278 and len(color_input) == 2 

279 and all(isinstance(c, (float, int)) for c in color_input) 

280 ): 

281 v1 = int(color_input[0]) 

282 v2 = int(color_input[1]) 

283 

284 if v2 < v1: 

285 v1, v2 = v2, v1 

286 v1 = max(v1, 0) 

287 v2 = min(v2, 360) 

288 return (v1, v2) 

289 raise TypeError("Hue must be a valid string, numeric type, or a tuple/list of 2 numeric types.") 

290 

291 def get_saturation_range(self, hue: int) -> Tuple[int, int]: 

292 """Return the saturation range for a given numerical ``hue`` value.""" 

293 return self.get_color_info(hue)["saturation_range"][0] 

294 

295 def get_color_info(self, hue: int) -> Dict[str, Sequence[Tuple[int, int]]]: 

296 """Return the color info for a given numerical ``hue`` value.""" 

297 # Maps red colors to make picking hue easier 

298 if 334 <= hue <= 360: 

299 hue -= 360 

300 

301 for color_name, color in self.colormap.items(): 

302 hue_range: Tuple[int, int] = color["hue_range"][0] 

303 if hue_range[0] <= hue <= hue_range[1]: 

304 return self.colormap[color_name] 

305 else: 

306 raise ValueError("Value of hue `%s` is invalid." % hue) 

307 

308 def random_within(self, r: Sequence[int]) -> int: 

309 """Return a random integer within the range ``r``.""" 

310 return self.random.randint(int(r[0]), int(r[1])) 

311 

312 @classmethod 

313 def hsv_to_rgb(cls, hsv: Tuple[int, int, int]) -> Tuple[int, int, int]: 

314 """Convert HSV to RGB. 

315 

316 This method expects ``hsv`` to be a 3-tuple of H, S, and V values, and 

317 it will return a 3-tuple of the equivalent R, G, and B values. 

318 """ 

319 h, s, v = hsv 

320 h = max(h, 1) 

321 h = min(h, 359) 

322 

323 r, g, b = colorsys.hsv_to_rgb(h / 360, s / 100, v / 100) 

324 return (int(r * 255), int(g * 255), int(b * 255)) 

325 

326 @classmethod 

327 def hsv_to_hsl(cls, hsv: Tuple[int, int, int]) -> Tuple[int, int, int]: 

328 """Convert HSV to HSL. 

329 

330 This method expects ``hsv`` to be a 3-tuple of H, S, and V values, and 

331 it will return a 3-tuple of the equivalent H, S, and L values. 

332 """ 

333 h, s, v = hsv 

334 

335 s_: float = s / 100.0 

336 v_: float = v / 100.0 

337 l = 0.5 * (v_) * (2 - s_) # noqa: E741 

338 

339 s_ = 0.0 if l in [0, 1] else v_ * s_ / (1 - math.fabs(2 * l - 1)) 

340 return (int(h), int(s_ * 100), int(l * 100))