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
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1"""Internal module for human-friendly color generation.
3.. important::
4 End users of this library should not use anything in this module.
6Code adapted from:
7- https://github.com/davidmerfield/randomColor (CC0)
8- https://github.com/kevinwuhoo/randomcolor-py (MIT License)
10Additional reference from:
11- https://en.wikipedia.org/wiki/HSL_and_HSV
12"""
14import colorsys
15import math
16import random
17import sys
19from typing import TYPE_CHECKING, Dict, Hashable, Optional, Sequence, Tuple
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
24from ...typing import HueType
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}
129class RandomColor:
130 """Implement random color generation in a human-friendly way.
132 This helper class encapsulates the internal implementation and logic of the
133 :meth:`color() <faker.providers.color.Provider.color>` method.
134 """
136 def __init__(self, generator: Optional["Generator"] = None, seed: Optional[Hashable] = None) -> None:
137 self.colormap = COLOR_MAP
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)
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]
152 self.colormap[color_name]["saturation_range"] = [(s_min, s_max)]
153 self.colormap[color_name]["brightness_range"] = [(b_min, b_max)]
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.
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)
170 # Then use H to determine saturation (S)
171 s = self.pick_saturation(h, hue, luminosity)
173 # Then use S and H to determine brightness (B).
174 b = self.pick_brightness(h, s, luminosity)
176 # Then we return the HSB color in the desired format
177 return self.set_format((h, s, b), color_format)
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))
183 # Instead of storing red as two separate ranges,
184 # we group them, using negative numbers
185 if hue_ < 0:
186 hue_ += 360
188 return hue_
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))
197 if isinstance(hue_name, str) and hue_name == "monochrome":
198 return 0
200 s_min, s_max = self.get_saturation_range(hue)
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
209 return self.random_within((s_min, s_max))
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 = ""
216 b_min = self.get_minimum_brightness(h, s)
217 b_max = 100
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
227 return self.random_within((b_min, b_max))
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]})"
234 elif color_format == "hsl":
235 hsl = self.hsv_to_hsl(hsv)
236 color = f"hsl({hsl[0]}, {hsl[1]}, {hsl[2]})"
238 elif color_format == "rgb":
239 rgb = self.hsv_to_rgb(hsv)
240 color = f"rgb({rgb[0]}, {rgb[1]}, {rgb[2]})"
242 else:
243 rgb = self.hsv_to_rgb(hsv)
244 color = f"#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}"
246 return color
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"]
252 for i in range(len(lower_bounds) - 1):
253 s1, v1 = lower_bounds[i]
254 s2, v2 = lower_bounds[i + 1]
256 if s1 <= s <= s2:
257 m: float = (v2 - v1) / (s2 - s1)
258 b: float = v1 - m * s1
260 return int(m * s + b)
262 return 0
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)
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])
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.")
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]
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
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)
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]))
312 @classmethod
313 def hsv_to_rgb(cls, hsv: Tuple[int, int, int]) -> Tuple[int, int, int]:
314 """Convert HSV to RGB.
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)
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))
326 @classmethod
327 def hsv_to_hsl(cls, hsv: Tuple[int, int, int]) -> Tuple[int, int, int]:
328 """Convert HSV to HSL.
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
335 s_: float = s / 100.0
336 v_: float = v / 100.0
337 l = 0.5 * (v_) * (2 - s_) # noqa: E741
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))