xulbux 1.6.8__py3-none-any.whl → 1.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of xulbux might be problematic. Click here for more details.
- xulbux/__init__.py +3 -35
- xulbux/_cli_.py +21 -28
- xulbux/_consts_.py +1 -0
- xulbux/xx_code.py +62 -46
- xulbux/xx_color.py +223 -159
- xulbux/xx_console.py +152 -78
- xulbux/xx_data.py +79 -71
- xulbux/xx_env_path.py +6 -9
- xulbux/xx_file.py +22 -26
- xulbux/xx_format_codes.py +55 -33
- xulbux/xx_json.py +107 -51
- xulbux/xx_path.py +74 -24
- xulbux/xx_regex.py +11 -10
- xulbux/xx_string.py +4 -1
- xulbux/xx_system.py +6 -10
- {xulbux-1.6.8.dist-info → xulbux-1.7.0.dist-info}/METADATA +18 -39
- xulbux-1.7.0.dist-info/RECORD +21 -0
- {xulbux-1.6.8.dist-info → xulbux-1.7.0.dist-info}/WHEEL +1 -1
- xulbux-1.6.8.dist-info/RECORD +0 -21
- {xulbux-1.6.8.dist-info → xulbux-1.7.0.dist-info}/entry_points.txt +0 -0
- {xulbux-1.6.8.dist-info → xulbux-1.7.0.dist-info/licenses}/LICENSE +0 -0
- {xulbux-1.6.8.dist-info → xulbux-1.7.0.dist-info}/top_level.txt +0 -0
xulbux/xx_color.py
CHANGED
|
@@ -33,10 +33,36 @@ The `Color` class, which contains all sorts of different color-related methods:
|
|
|
33
33
|
|
|
34
34
|
from .xx_regex import Regex
|
|
35
35
|
|
|
36
|
-
from typing import Optional
|
|
36
|
+
from typing import Annotated, TypeAlias, Iterator, Optional, Literal, Union
|
|
37
37
|
import re as _re
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
Int_0_100 = Annotated[int, "An integer value between 0 and 100, inclusive."]
|
|
41
|
+
Int_0_255 = Annotated[int, "An integer value between 0 and 255, inclusive."]
|
|
42
|
+
Int_0_360 = Annotated[int, "An integer value between 0 and 360, inclusive."]
|
|
43
|
+
Float_0_1 = Annotated[float, "A float value between 0.0 and 1.0, inclusive."]
|
|
44
|
+
|
|
45
|
+
Rgba: TypeAlias = Union[
|
|
46
|
+
tuple[Int_0_255, Int_0_255, Int_0_255],
|
|
47
|
+
tuple[Int_0_255, Int_0_255, Int_0_255, Float_0_1],
|
|
48
|
+
list[Int_0_255],
|
|
49
|
+
list[Union[Int_0_255, Float_0_1]],
|
|
50
|
+
dict[str, Union[int, float]],
|
|
51
|
+
"rgba",
|
|
52
|
+
str,
|
|
53
|
+
]
|
|
54
|
+
Hsla: TypeAlias = Union[
|
|
55
|
+
tuple[Int_0_360, Int_0_100, Int_0_100],
|
|
56
|
+
tuple[Int_0_360, Int_0_100, Int_0_100, Float_0_1],
|
|
57
|
+
list[Union[Int_0_360, Int_0_100]],
|
|
58
|
+
list[Union[Int_0_360, Int_0_100, Float_0_1]],
|
|
59
|
+
dict[str, Union[int, float]],
|
|
60
|
+
"hsla",
|
|
61
|
+
str,
|
|
62
|
+
]
|
|
63
|
+
Hexa: TypeAlias = Union[str, int, "hexa"]
|
|
64
|
+
|
|
65
|
+
|
|
40
66
|
class rgba:
|
|
41
67
|
"""An RGB/RGBA color: is a tuple of 3 integers, representing the red (`0`-`255`), green (`0`-`255`), and blue (`0`-`255`).\n
|
|
42
68
|
Also includes an optional 4th param, which is a float, that represents the alpha channel (`0.0`-`1.0`).\n
|
|
@@ -60,7 +86,11 @@ class rgba:
|
|
|
60
86
|
- `with_alpha(alpha)` to create a new color with different alpha
|
|
61
87
|
- `complementary()` to get the complementary color"""
|
|
62
88
|
|
|
63
|
-
def __init__(self, r: int, g: int, b: int, a: float = None, _validate: bool = True):
|
|
89
|
+
def __init__(self, r: int, g: int, b: int, a: Optional[float] = None, _validate: bool = True):
|
|
90
|
+
self.r: int
|
|
91
|
+
self.g: int
|
|
92
|
+
self.b: int
|
|
93
|
+
self.a: Optional[float]
|
|
64
94
|
if not _validate:
|
|
65
95
|
self.r, self.g, self.b, self.a = r, g, b, a
|
|
66
96
|
return
|
|
@@ -79,13 +109,10 @@ class rgba:
|
|
|
79
109
|
def __len__(self) -> int:
|
|
80
110
|
return 3 if self.a is None else 4
|
|
81
111
|
|
|
82
|
-
def __iter__(self) ->
|
|
112
|
+
def __iter__(self) -> Iterator:
|
|
83
113
|
return iter((self.r, self.g, self.b) + (() if self.a is None else (self.a, )))
|
|
84
114
|
|
|
85
|
-
def
|
|
86
|
-
return self.dict()
|
|
87
|
-
|
|
88
|
-
def __getitem__(self, index: int) -> int:
|
|
115
|
+
def __getitem__(self, index: int) -> int | float:
|
|
89
116
|
return ((self.r, self.g, self.b) + (() if self.a is None else (self.a, )))[index]
|
|
90
117
|
|
|
91
118
|
def __repr__(self) -> str:
|
|
@@ -94,15 +121,10 @@ class rgba:
|
|
|
94
121
|
def __str__(self) -> str:
|
|
95
122
|
return f'({self.r}, {self.g}, {self.b}{"" if self.a is None else f", {self.a}"})'
|
|
96
123
|
|
|
97
|
-
def __eq__(self, other: "rgba") -> bool:
|
|
124
|
+
def __eq__(self, other: "rgba") -> bool: # type: ignore[override]
|
|
98
125
|
if not isinstance(other, rgba):
|
|
99
126
|
return False
|
|
100
|
-
return (self.r, self.g, self.b, self.a) == (
|
|
101
|
-
other[0],
|
|
102
|
-
other[1],
|
|
103
|
-
other[2],
|
|
104
|
-
other[3],
|
|
105
|
-
)
|
|
127
|
+
return (self.r, self.g, self.b, self.a) == (other.r, other.g, other.b, other.a)
|
|
106
128
|
|
|
107
129
|
def dict(self) -> dict:
|
|
108
130
|
"""Returns the color components as a dictionary with keys `'r'`, `'g'`, `'b'` and optionally `'a'`"""
|
|
@@ -114,7 +136,7 @@ class rgba:
|
|
|
114
136
|
|
|
115
137
|
def to_hsla(self) -> "hsla":
|
|
116
138
|
"""Returns the color as a `hsla()` color"""
|
|
117
|
-
return hsla(*self._rgb_to_hsl(self.r, self.g, self.b), self.a, _validate=False)
|
|
139
|
+
return hsla(*self._rgb_to_hsl(self.r, self.g, self.b), self.a, _validate=False) # type: ignore[positional-arguments]
|
|
118
140
|
|
|
119
141
|
def to_hexa(self) -> "hexa":
|
|
120
142
|
"""Returns the color as a `hexa()` color"""
|
|
@@ -152,16 +174,22 @@ class rgba:
|
|
|
152
174
|
def invert(self, invert_alpha: bool = False) -> "rgba":
|
|
153
175
|
"""Inverts the color by rotating hue by 180 degrees and inverting lightness"""
|
|
154
176
|
self.r, self.g, self.b = 255 - self.r, 255 - self.g, 255 - self.b
|
|
155
|
-
if invert_alpha:
|
|
177
|
+
if invert_alpha and self.a is not None:
|
|
156
178
|
self.a = 1 - self.a
|
|
157
179
|
return rgba(self.r, self.g, self.b, self.a, _validate=False)
|
|
158
180
|
|
|
159
|
-
def grayscale(self) -> "rgba":
|
|
160
|
-
"""Converts the color to grayscale using the luminance formula
|
|
161
|
-
|
|
181
|
+
def grayscale(self, method: str = "wcag2") -> "rgba":
|
|
182
|
+
"""Converts the color to grayscale using the luminance formula.\n
|
|
183
|
+
------------------------------------------------------------------
|
|
184
|
+
The `method` is the luminance calculation method to use:
|
|
185
|
+
- `"wcag2"` WCAG 2.0 standard (default and most accurate for perception)
|
|
186
|
+
- `"wcag3"` Draft WCAG 3.0 standard with improved coefficients
|
|
187
|
+
- `"simple"` Simple arithmetic mean (less accurate)
|
|
188
|
+
- `"bt601"` ITU-R BT.601 standard (older TV standard)"""
|
|
189
|
+
self.r = self.g = self.b = int(Color.luminance(self.r, self.g, self.b, method=method))
|
|
162
190
|
return rgba(self.r, self.g, self.b, self.a, _validate=False)
|
|
163
191
|
|
|
164
|
-
def blend(self, other:
|
|
192
|
+
def blend(self, other: Rgba, ratio: float = 0.5, additive_alpha: bool = False) -> "rgba":
|
|
165
193
|
"""Blends the current color with another color using the specified ratio (`0.0`-`1.0`):
|
|
166
194
|
- if `ratio` is `0.0` it means 100% of the current color and 0% of the `other` color (2:0 mixture)
|
|
167
195
|
- if `ratio` is `0.5` it means 50% of both colors (1:1 mixture)
|
|
@@ -170,7 +198,7 @@ class rgba:
|
|
|
170
198
|
raise ValueError("'ratio' must be a float/int in [0.0, 1.0]")
|
|
171
199
|
elif not isinstance(other, rgba):
|
|
172
200
|
if Color.is_valid_rgba(other):
|
|
173
|
-
other =
|
|
201
|
+
other = Color.to_rgba(other)
|
|
174
202
|
else:
|
|
175
203
|
raise TypeError("'other' must be a valid RGBA color")
|
|
176
204
|
ratio *= 2
|
|
@@ -216,20 +244,20 @@ class rgba:
|
|
|
216
244
|
return self.to_hsla().complementary().to_rgba()
|
|
217
245
|
|
|
218
246
|
def _rgb_to_hsl(self, r: int, g: int, b: int) -> tuple:
|
|
219
|
-
|
|
220
|
-
max_c, min_c = max(
|
|
247
|
+
_r, _g, _b = r / 255.0, g / 255.0, b / 255.0
|
|
248
|
+
max_c, min_c = max(_r, _g, _b), min(_r, _g, _b)
|
|
221
249
|
l = (max_c + min_c) / 2
|
|
222
250
|
if max_c == min_c:
|
|
223
251
|
h = s = 0
|
|
224
252
|
else:
|
|
225
253
|
delta = max_c - min_c
|
|
226
254
|
s = delta / (1 - abs(2 * l - 1))
|
|
227
|
-
if max_c ==
|
|
228
|
-
h = ((
|
|
229
|
-
elif max_c ==
|
|
230
|
-
h = ((
|
|
255
|
+
if max_c == _r:
|
|
256
|
+
h = ((_g - _b) / delta) % 6
|
|
257
|
+
elif max_c == _g:
|
|
258
|
+
h = ((_b - _r) / delta) + 2
|
|
231
259
|
else:
|
|
232
|
-
h = ((
|
|
260
|
+
h = ((_r - _g) / delta) + 4
|
|
233
261
|
h /= 6
|
|
234
262
|
return int(round(h * 360)), int(round(s * 100)), int(round(l * 100))
|
|
235
263
|
|
|
@@ -257,7 +285,11 @@ class hsla:
|
|
|
257
285
|
- `with_alpha(alpha)` to create a new color with different alpha
|
|
258
286
|
- `complementary()` to get the complementary color"""
|
|
259
287
|
|
|
260
|
-
def __init__(self, h: int, s: int, l: int, a: float = None, _validate: bool = True):
|
|
288
|
+
def __init__(self, h: int, s: int, l: int, a: Optional[float] = None, _validate: bool = True):
|
|
289
|
+
self.h: int
|
|
290
|
+
self.s: int
|
|
291
|
+
self.l: int
|
|
292
|
+
self.a: Optional[float]
|
|
261
293
|
if not _validate:
|
|
262
294
|
self.h, self.s, self.l, self.a = h, s, l, a
|
|
263
295
|
return
|
|
@@ -276,30 +308,22 @@ class hsla:
|
|
|
276
308
|
def __len__(self) -> int:
|
|
277
309
|
return 3 if self.a is None else 4
|
|
278
310
|
|
|
279
|
-
def __iter__(self) ->
|
|
311
|
+
def __iter__(self) -> Iterator:
|
|
280
312
|
return iter((self.h, self.s, self.l) + (() if self.a is None else (self.a, )))
|
|
281
313
|
|
|
282
|
-
def
|
|
283
|
-
return self.dict()
|
|
284
|
-
|
|
285
|
-
def __getitem__(self, index: int) -> int:
|
|
314
|
+
def __getitem__(self, index: int) -> int | float:
|
|
286
315
|
return ((self.h, self.s, self.l) + (() if self.a is None else (self.a, )))[index]
|
|
287
316
|
|
|
288
317
|
def __repr__(self) -> str:
|
|
289
|
-
return f'hsla({self.h}
|
|
318
|
+
return f'hsla({self.h}°, {self.s}%, {self.l}%{"" if self.a is None else f", {self.a}"})'
|
|
290
319
|
|
|
291
320
|
def __str__(self) -> str:
|
|
292
|
-
return f'({self.h}
|
|
321
|
+
return f'({self.h}°, {self.s}%, {self.l}%{"" if self.a is None else f", {self.a}"})'
|
|
293
322
|
|
|
294
|
-
def __eq__(self, other: "hsla") -> bool:
|
|
323
|
+
def __eq__(self, other: "hsla") -> bool: # type: ignore[override]
|
|
295
324
|
if not isinstance(other, hsla):
|
|
296
325
|
return False
|
|
297
|
-
return (self.h, self.s, self.l, self.a) == (
|
|
298
|
-
other[0],
|
|
299
|
-
other[1],
|
|
300
|
-
other[2],
|
|
301
|
-
other[3],
|
|
302
|
-
)
|
|
326
|
+
return (self.h, self.s, self.l, self.a) == (other.h, other.s, other.l, other.a)
|
|
303
327
|
|
|
304
328
|
def dict(self) -> dict:
|
|
305
329
|
"""Returns the color components as a dictionary with keys `'h'`, `'s'`, `'l'` and optionally `'a'`"""
|
|
@@ -311,7 +335,7 @@ class hsla:
|
|
|
311
335
|
|
|
312
336
|
def to_rgba(self) -> "rgba":
|
|
313
337
|
"""Returns the color as a `rgba()` color"""
|
|
314
|
-
return rgba(*self._hsl_to_rgb(self.h, self.s, self.l), self.a, _validate=False)
|
|
338
|
+
return rgba(*self._hsl_to_rgb(self.h, self.s, self.l), self.a, _validate=False) # type: ignore[positional-arguments]
|
|
315
339
|
|
|
316
340
|
def to_hexa(self) -> "hexa":
|
|
317
341
|
"""Returns the color as a `hexa()` color"""
|
|
@@ -359,17 +383,23 @@ class hsla:
|
|
|
359
383
|
"""Inverts the color by rotating hue by 180 degrees and inverting lightness"""
|
|
360
384
|
self.h = (self.h + 180) % 360
|
|
361
385
|
self.l = 100 - self.l
|
|
362
|
-
if invert_alpha:
|
|
386
|
+
if invert_alpha and self.a is not None:
|
|
363
387
|
self.a = 1 - self.a
|
|
364
388
|
return hsla(self.h, self.s, self.l, self.a, _validate=False)
|
|
365
389
|
|
|
366
|
-
def grayscale(self) -> "hsla":
|
|
367
|
-
"""Converts the color to grayscale using the luminance formula
|
|
368
|
-
|
|
390
|
+
def grayscale(self, method: str = "wcag2") -> "hsla":
|
|
391
|
+
"""Converts the color to grayscale using the luminance formula.\n
|
|
392
|
+
------------------------------------------------------------------
|
|
393
|
+
The `method` is the luminance calculation method to use:
|
|
394
|
+
- `"wcag2"` WCAG 2.0 standard (default and most accurate for perception)
|
|
395
|
+
- `"wcag3"` Draft WCAG 3.0 standard with improved coefficients
|
|
396
|
+
- `"simple"` Simple arithmetic mean (less accurate)
|
|
397
|
+
- `"bt601"` ITU-R BT.601 standard (older TV standard)"""
|
|
398
|
+
l = int(Color.luminance(*self._hsl_to_rgb(self.h, self.s, self.l), method=method))
|
|
369
399
|
self.h, self.s, self.l, _ = rgba(l, l, l, _validate=False).to_hsla().values()
|
|
370
400
|
return hsla(self.h, self.s, self.l, self.a, _validate=False)
|
|
371
401
|
|
|
372
|
-
def blend(self, other:
|
|
402
|
+
def blend(self, other: Hsla, ratio: float = 0.5, additive_alpha: bool = False) -> "hsla":
|
|
373
403
|
"""Blends the current color with another color using the specified ratio (`0.0`-`1.0`):
|
|
374
404
|
- if `ratio` is `0.0` it means 100% of the current color and 0% of the `other` color (2:0 mixture)
|
|
375
405
|
- if `ratio` is `0.5` it means 50% of both colors (1:1 mixture)
|
|
@@ -404,9 +434,9 @@ class hsla:
|
|
|
404
434
|
return hsla((self.h + 180) % 360, self.s, self.l, self.a, _validate=False)
|
|
405
435
|
|
|
406
436
|
def _hsl_to_rgb(self, h: int, s: int, l: int) -> tuple:
|
|
407
|
-
|
|
408
|
-
if
|
|
409
|
-
r = g = b = int(
|
|
437
|
+
_h, _s, _l = h / 360, s / 100, l / 100
|
|
438
|
+
if _s == 0:
|
|
439
|
+
r = g = b = int(_l * 255)
|
|
410
440
|
else:
|
|
411
441
|
|
|
412
442
|
def hue_to_rgb(p, q, t):
|
|
@@ -422,11 +452,11 @@ class hsla:
|
|
|
422
452
|
return p + (q - p) * (2 / 3 - t) * 6
|
|
423
453
|
return p
|
|
424
454
|
|
|
425
|
-
q =
|
|
426
|
-
p = 2 *
|
|
427
|
-
r = int(round(hue_to_rgb(p, q,
|
|
428
|
-
g = int(round(hue_to_rgb(p, q,
|
|
429
|
-
b = int(round(hue_to_rgb(p, q,
|
|
455
|
+
q = _l * (1 + _s) if _l < 0.5 else _l + _s - _l * _s
|
|
456
|
+
p = 2 * _l - q
|
|
457
|
+
r = int(round(hue_to_rgb(p, q, _h + 1 / 3) * 255))
|
|
458
|
+
g = int(round(hue_to_rgb(p, q, _h) * 255))
|
|
459
|
+
b = int(round(hue_to_rgb(p, q, _h - 1 / 3) * 255))
|
|
430
460
|
return r, g, b
|
|
431
461
|
|
|
432
462
|
|
|
@@ -453,9 +483,20 @@ class hexa:
|
|
|
453
483
|
- `with_alpha(alpha)` to create a new color with different alpha
|
|
454
484
|
- `complementary()` to get the complementary color"""
|
|
455
485
|
|
|
456
|
-
def __init__(
|
|
486
|
+
def __init__(
|
|
487
|
+
self,
|
|
488
|
+
color: str | int,
|
|
489
|
+
_r: Optional[int] = None,
|
|
490
|
+
_g: Optional[int] = None,
|
|
491
|
+
_b: Optional[int] = None,
|
|
492
|
+
_a: Optional[float] = None,
|
|
493
|
+
):
|
|
494
|
+
self.r: int
|
|
495
|
+
self.g: int
|
|
496
|
+
self.b: int
|
|
497
|
+
self.a: Optional[float]
|
|
457
498
|
if all(x is not None for x in (_r, _g, _b)):
|
|
458
|
-
self.r, self.g, self.b, self.a = _r, _g, _b, _a
|
|
499
|
+
self.r, self.g, self.b, self.a = _r, _g, _b, _a # type: ignore[assignment]
|
|
459
500
|
return
|
|
460
501
|
if isinstance(color, hexa):
|
|
461
502
|
raise ValueError("Color is already a hexa() color")
|
|
@@ -502,14 +543,11 @@ class hexa:
|
|
|
502
543
|
def __len__(self) -> int:
|
|
503
544
|
return 3 if self.a is None else 4
|
|
504
545
|
|
|
505
|
-
def __iter__(self) ->
|
|
546
|
+
def __iter__(self) -> Iterator:
|
|
506
547
|
return iter((f"{self.r:02X}", f"{self.g:02X}", f"{self.b:02X}")
|
|
507
548
|
+ (() if self.a is None else (f"{int(self.a * 255):02X}", )))
|
|
508
549
|
|
|
509
|
-
def
|
|
510
|
-
return self.dict()
|
|
511
|
-
|
|
512
|
-
def __getitem__(self, index: int) -> int:
|
|
550
|
+
def __getitem__(self, index: int) -> str | int:
|
|
513
551
|
return ((f"{self.r:02X}", f"{self.g:02X}", f"{self.b:02X}") + (() if self.a is None else
|
|
514
552
|
(f"{int(self.a * 255):02X}", )))[index]
|
|
515
553
|
|
|
@@ -519,15 +557,11 @@ class hexa:
|
|
|
519
557
|
def __str__(self) -> str:
|
|
520
558
|
return f'#{self.r:02X}{self.g:02X}{self.b:02X}{"" if self.a is None else f"{int(self.a * 255):02X}"}'
|
|
521
559
|
|
|
522
|
-
def __eq__(self, other: "hexa") -> bool:
|
|
560
|
+
def __eq__(self, other: "hexa") -> bool: # type: ignore[override]
|
|
561
|
+
"""Returns whether the other color is equal to this one."""
|
|
523
562
|
if not isinstance(other, hexa):
|
|
524
563
|
return False
|
|
525
|
-
return (self.r, self.g, self.b, self.a) == (
|
|
526
|
-
other[0],
|
|
527
|
-
other[1],
|
|
528
|
-
other[2],
|
|
529
|
-
other[3],
|
|
530
|
-
)
|
|
564
|
+
return (self.r, self.g, self.b, self.a) == (other.r, other.g, other.b, other.a)
|
|
531
565
|
|
|
532
566
|
def dict(self) -> dict:
|
|
533
567
|
"""Returns the color components as a dictionary with hex string values for keys `'r'`, `'g'`, `'b'` and optionally `'a'`"""
|
|
@@ -540,9 +574,9 @@ class hexa:
|
|
|
540
574
|
)
|
|
541
575
|
)
|
|
542
576
|
|
|
543
|
-
def values(self) -> tuple:
|
|
577
|
+
def values(self, round_alpha: bool = True) -> tuple:
|
|
544
578
|
"""Returns the color components as separate values `r, g, b, a`"""
|
|
545
|
-
return self.r, self.g, self.b, self.a
|
|
579
|
+
return self.r, self.g, self.b, None if self.a is None else (round(self.a, 2) if round_alpha else self.a)
|
|
546
580
|
|
|
547
581
|
def to_rgba(self, round_alpha: bool = True) -> "rgba":
|
|
548
582
|
"""Returns the color as a `rgba()` color"""
|
|
@@ -590,16 +624,22 @@ class hexa:
|
|
|
590
624
|
def invert(self, invert_alpha: bool = False) -> "hexa":
|
|
591
625
|
"""Inverts the color by rotating hue by 180 degrees and inverting lightness"""
|
|
592
626
|
self.r, self.g, self.b, self.a = self.to_rgba(False).invert().values()
|
|
593
|
-
if invert_alpha:
|
|
627
|
+
if invert_alpha and self.a is not None:
|
|
594
628
|
self.a = 1 - self.a
|
|
595
629
|
return hexa("", self.r, self.g, self.b, self.a)
|
|
596
630
|
|
|
597
|
-
def grayscale(self) -> "hexa":
|
|
598
|
-
"""Converts the color to grayscale using the luminance formula
|
|
599
|
-
|
|
631
|
+
def grayscale(self, method: str = "wcag2") -> "hexa":
|
|
632
|
+
"""Converts the color to grayscale using the luminance formula.\n
|
|
633
|
+
------------------------------------------------------------------
|
|
634
|
+
The `method` is the luminance calculation method to use:
|
|
635
|
+
- `"wcag2"` WCAG 2.0 standard (default and most accurate for perception)
|
|
636
|
+
- `"wcag3"` Draft WCAG 3.0 standard with improved coefficients
|
|
637
|
+
- `"simple"` Simple arithmetic mean (less accurate)
|
|
638
|
+
- `"bt601"` ITU-R BT.601 standard (older TV standard)"""
|
|
639
|
+
self.r = self.g = self.b = int(Color.luminance(self.r, self.g, self.b, method=method))
|
|
600
640
|
return hexa("", self.r, self.g, self.b, self.a)
|
|
601
641
|
|
|
602
|
-
def blend(self, other:
|
|
642
|
+
def blend(self, other: Hexa, ratio: float = 0.5, additive_alpha: bool = False) -> "hexa":
|
|
603
643
|
"""Blends the current color with another color using the specified ratio (`0.0`-`1.0`):
|
|
604
644
|
- if `ratio` is `0.0` it means 100% of the current color and 0% of the `other` color (2:0 mixture)
|
|
605
645
|
- if `ratio` is `0.5` it means 50% of both colors (1:1 mixture)
|
|
@@ -637,7 +677,7 @@ class hexa:
|
|
|
637
677
|
class Color:
|
|
638
678
|
|
|
639
679
|
@staticmethod
|
|
640
|
-
def is_valid_rgba(color:
|
|
680
|
+
def is_valid_rgba(color: Rgba, allow_alpha: bool = True) -> bool:
|
|
641
681
|
try:
|
|
642
682
|
if isinstance(color, rgba):
|
|
643
683
|
return True
|
|
@@ -645,16 +685,22 @@ class Color:
|
|
|
645
685
|
if allow_alpha and Color.has_alpha(color):
|
|
646
686
|
return (
|
|
647
687
|
0 <= color[0] <= 255 and 0 <= color[1] <= 255 and 0 <= color[2] <= 255
|
|
648
|
-
and (0 <= color[3] <= 1 or color[3] is None)
|
|
688
|
+
and (0 <= color[3] <= 1 or color[3] is None) # type: ignore[index]
|
|
649
689
|
)
|
|
650
|
-
|
|
690
|
+
elif len(color) == 3:
|
|
691
|
+
return 0 <= color[0] <= 255 and 0 <= color[1] <= 255 and 0 <= color[2] <= 255
|
|
692
|
+
else:
|
|
693
|
+
return False
|
|
651
694
|
elif isinstance(color, dict):
|
|
652
695
|
if allow_alpha and Color.has_alpha(color):
|
|
653
696
|
return (
|
|
654
697
|
0 <= color["r"] <= 255 and 0 <= color["g"] <= 255 and 0 <= color["b"] <= 255
|
|
655
698
|
and (0 <= color["a"] <= 1 or color["a"] is None)
|
|
656
699
|
)
|
|
657
|
-
|
|
700
|
+
elif len(color) == 3:
|
|
701
|
+
return 0 <= color["r"] <= 255 and 0 <= color["g"] <= 255 and 0 <= color["b"] <= 255
|
|
702
|
+
else:
|
|
703
|
+
return False
|
|
658
704
|
elif isinstance(color, str):
|
|
659
705
|
return bool(_re.fullmatch(Regex.rgba_str(allow_alpha=allow_alpha), color))
|
|
660
706
|
return False
|
|
@@ -662,7 +708,7 @@ class Color:
|
|
|
662
708
|
return False
|
|
663
709
|
|
|
664
710
|
@staticmethod
|
|
665
|
-
def is_valid_hsla(color:
|
|
711
|
+
def is_valid_hsla(color: Hsla, allow_alpha: bool = True) -> bool:
|
|
666
712
|
try:
|
|
667
713
|
if isinstance(color, hsla):
|
|
668
714
|
return True
|
|
@@ -670,30 +716,38 @@ class Color:
|
|
|
670
716
|
if allow_alpha and Color.has_alpha(color):
|
|
671
717
|
return (
|
|
672
718
|
0 <= color[0] <= 360 and 0 <= color[1] <= 100 and 0 <= color[2] <= 100
|
|
673
|
-
and (0 <= color[3] <= 1 or color[3] is None)
|
|
719
|
+
and (0 <= color[3] <= 1 or color[3] is None) # type: ignore[index]
|
|
674
720
|
)
|
|
675
|
-
|
|
721
|
+
elif len(color) == 3:
|
|
676
722
|
return 0 <= color[0] <= 360 and 0 <= color[1] <= 100 and 0 <= color[2] <= 100
|
|
723
|
+
else:
|
|
724
|
+
return False
|
|
677
725
|
elif isinstance(color, dict):
|
|
678
726
|
if allow_alpha and Color.has_alpha(color):
|
|
679
727
|
return (
|
|
680
728
|
0 <= color["h"] <= 360 and 0 <= color["s"] <= 100 and 0 <= color["l"] <= 100
|
|
681
729
|
and (0 <= color["a"] <= 1 or color["a"] is None)
|
|
682
730
|
)
|
|
683
|
-
|
|
731
|
+
elif len(color) == 3:
|
|
684
732
|
return 0 <= color["h"] <= 360 and 0 <= color["s"] <= 100 and 0 <= color["l"] <= 100
|
|
733
|
+
else:
|
|
734
|
+
return False
|
|
685
735
|
elif isinstance(color, str):
|
|
686
736
|
return bool(_re.fullmatch(Regex.hsla_str(allow_alpha=allow_alpha), color))
|
|
687
737
|
except Exception:
|
|
688
738
|
return False
|
|
689
739
|
|
|
690
740
|
@staticmethod
|
|
691
|
-
def is_valid_hexa(
|
|
741
|
+
def is_valid_hexa(
|
|
742
|
+
color: Hexa,
|
|
743
|
+
allow_alpha: bool = True,
|
|
744
|
+
get_prefix: bool = False,
|
|
745
|
+
) -> bool | tuple[bool, Optional[Literal['#', '0x']]]:
|
|
692
746
|
try:
|
|
693
747
|
if isinstance(color, hexa):
|
|
694
|
-
return (True, "#")
|
|
748
|
+
return (True, "#") if get_prefix else True
|
|
695
749
|
elif isinstance(color, int):
|
|
696
|
-
is_valid =
|
|
750
|
+
is_valid = 0x000000 <= color <= (0xFFFFFFFF if allow_alpha else 0xFFFFFF)
|
|
697
751
|
return (is_valid, "0x") if get_prefix else is_valid
|
|
698
752
|
elif isinstance(color, str):
|
|
699
753
|
color, prefix = ((color[1:], "#") if color.startswith("#") else
|
|
@@ -704,21 +758,21 @@ class Color:
|
|
|
704
758
|
return (False, None) if get_prefix else False
|
|
705
759
|
|
|
706
760
|
@staticmethod
|
|
707
|
-
def is_valid(color:
|
|
708
|
-
return (
|
|
709
|
-
Color.is_valid_rgba(color, allow_alpha) or Color.is_valid_hsla(color, allow_alpha)
|
|
710
|
-
or Color.is_valid_hexa(color, allow_alpha)
|
|
761
|
+
def is_valid(color: Rgba | Hsla | Hexa, allow_alpha: bool = True) -> bool:
|
|
762
|
+
return bool(
|
|
763
|
+
Color.is_valid_rgba(color, allow_alpha) or Color.is_valid_hsla(color, allow_alpha) # type: ignore[assignment]
|
|
764
|
+
or Color.is_valid_hexa(color, allow_alpha) # type: ignore[assignment]
|
|
711
765
|
)
|
|
712
766
|
|
|
713
767
|
@staticmethod
|
|
714
|
-
def has_alpha(color:
|
|
768
|
+
def has_alpha(color: Rgba | Hsla | Hexa) -> bool:
|
|
715
769
|
"""Check if the given color has an alpha channel.\n
|
|
716
770
|
---------------------------------------------------------------------------
|
|
717
771
|
Input a RGBA, HSLA or HEXA color as `color`.
|
|
718
772
|
Returns `True` if the color has an alpha channel and `False` otherwise."""
|
|
719
773
|
if isinstance(color, (rgba, hsla, hexa)):
|
|
720
774
|
return color.has_alpha()
|
|
721
|
-
if Color.is_valid_hexa(color):
|
|
775
|
+
if Color.is_valid_hexa(color): # type: ignore[assignment]
|
|
722
776
|
if isinstance(color, str):
|
|
723
777
|
if color.startswith("#"):
|
|
724
778
|
color = color[1:]
|
|
@@ -733,42 +787,42 @@ class Color:
|
|
|
733
787
|
return False
|
|
734
788
|
|
|
735
789
|
@staticmethod
|
|
736
|
-
def to_rgba(color:
|
|
790
|
+
def to_rgba(color: Rgba | Hsla | Hexa) -> rgba:
|
|
737
791
|
"""Will try to convert any color type to a color of type RGBA."""
|
|
738
792
|
if isinstance(color, (hsla, hexa)):
|
|
739
793
|
return color.to_rgba()
|
|
740
|
-
elif Color.is_valid_hsla(color):
|
|
741
|
-
return hsla(*color, _validate=False).to_rgba()
|
|
742
|
-
elif Color.is_valid_hexa(color):
|
|
743
|
-
return hexa(color).to_rgba()
|
|
744
|
-
elif Color.is_valid_rgba(color):
|
|
745
|
-
return color if isinstance(color, rgba) else (rgba(*color, _validate=False))
|
|
794
|
+
elif Color.is_valid_hsla(color): # type: ignore[assignment]
|
|
795
|
+
return hsla(*color, _validate=False).to_rgba() # type: ignore[not-iterable]
|
|
796
|
+
elif Color.is_valid_hexa(color): # type: ignore[assignment]
|
|
797
|
+
return hexa(color).to_rgba() # type: ignore[assignment]
|
|
798
|
+
elif Color.is_valid_rgba(color): # type: ignore[assignment]
|
|
799
|
+
return color if isinstance(color, rgba) else (rgba(*color, _validate=False)) # type: ignore[not-iterable]
|
|
746
800
|
raise ValueError(f"Invalid color format '{color}'")
|
|
747
801
|
|
|
748
802
|
@staticmethod
|
|
749
|
-
def to_hsla(color:
|
|
803
|
+
def to_hsla(color: Rgba | Hsla | Hexa) -> hsla:
|
|
750
804
|
"""Will try to convert any color type to a color of type HSLA."""
|
|
751
805
|
if isinstance(color, (rgba, hexa)):
|
|
752
806
|
return color.to_hsla()
|
|
753
|
-
elif Color.is_valid_rgba(color):
|
|
754
|
-
return rgba(*color, _validate=False).to_hsla()
|
|
755
|
-
elif Color.is_valid_hexa(color):
|
|
756
|
-
return hexa(color).to_hsla()
|
|
757
|
-
elif Color.is_valid_hsla(color):
|
|
758
|
-
return color if isinstance(color, hsla) else (hsla(*color, _validate=False))
|
|
807
|
+
elif Color.is_valid_rgba(color): # type: ignore[assignment]
|
|
808
|
+
return rgba(*color, _validate=False).to_hsla() # type: ignore[not-iterable]
|
|
809
|
+
elif Color.is_valid_hexa(color): # type: ignore[assignment]
|
|
810
|
+
return hexa(color).to_hsla() # type: ignore[assignment]
|
|
811
|
+
elif Color.is_valid_hsla(color): # type: ignore[assignment]
|
|
812
|
+
return color if isinstance(color, hsla) else (hsla(*color, _validate=False)) # type: ignore[not-iterable]
|
|
759
813
|
raise ValueError(f"Invalid color format '{color}'")
|
|
760
814
|
|
|
761
815
|
@staticmethod
|
|
762
|
-
def to_hexa(color:
|
|
816
|
+
def to_hexa(color: Rgba | Hsla | Hexa) -> hexa:
|
|
763
817
|
"""Will try to convert any color type to a color of type HEXA."""
|
|
764
818
|
if isinstance(color, (rgba, hsla)):
|
|
765
819
|
return color.to_hexa()
|
|
766
|
-
elif Color.is_valid_rgba(color):
|
|
767
|
-
return rgba(*color, _validate=False).to_hexa()
|
|
768
|
-
elif Color.is_valid_hsla(color):
|
|
769
|
-
return hsla(*color, _validate=False).to_hexa()
|
|
770
|
-
elif Color.is_valid_hexa(color):
|
|
771
|
-
return color if isinstance(color, hexa) else hexa(color)
|
|
820
|
+
elif Color.is_valid_rgba(color): # type: ignore[assignment]
|
|
821
|
+
return rgba(*color, _validate=False).to_hexa() # type: ignore[not-iterable]
|
|
822
|
+
elif Color.is_valid_hsla(color): # type: ignore[assignment]
|
|
823
|
+
return hsla(*color, _validate=False).to_hexa() # type: ignore[not-iterable]
|
|
824
|
+
elif Color.is_valid_hexa(color): # type: ignore[assignment]
|
|
825
|
+
return color if isinstance(color, hexa) else hexa(color) # type: ignore[assignment]
|
|
772
826
|
raise ValueError(f"Invalid color format '{color}'")
|
|
773
827
|
|
|
774
828
|
@staticmethod
|
|
@@ -777,7 +831,7 @@ class Color:
|
|
|
777
831
|
--------------------------------------------------------------------------------------------------
|
|
778
832
|
If `only_first` is `True` only the first found color will be returned (not as a list)."""
|
|
779
833
|
if only_first:
|
|
780
|
-
match = _re.search(Regex.
|
|
834
|
+
match = _re.search(Regex.rgba_str(allow_alpha=True), string)
|
|
781
835
|
if not match:
|
|
782
836
|
return None
|
|
783
837
|
m = match.groups()
|
|
@@ -789,7 +843,7 @@ class Color:
|
|
|
789
843
|
_validate=False,
|
|
790
844
|
)
|
|
791
845
|
else:
|
|
792
|
-
matches = _re.findall(Regex.
|
|
846
|
+
matches = _re.findall(Regex.rgba_str(allow_alpha=True), string)
|
|
793
847
|
if not matches:
|
|
794
848
|
return None
|
|
795
849
|
return [
|
|
@@ -807,7 +861,7 @@ class Color:
|
|
|
807
861
|
r: int,
|
|
808
862
|
g: int,
|
|
809
863
|
b: int,
|
|
810
|
-
a: float = None,
|
|
864
|
+
a: Optional[float] = None,
|
|
811
865
|
preserve_original: bool = False,
|
|
812
866
|
) -> int:
|
|
813
867
|
"""Convert RGBA channels to a HEXA integer (alpha is optional).\n
|
|
@@ -865,71 +919,81 @@ class Color:
|
|
|
865
919
|
raise ValueError(f"Invalid HEX integer '0x{hex_str}': expected in range [0x000000, 0xFFFFFF]")
|
|
866
920
|
|
|
867
921
|
@staticmethod
|
|
868
|
-
def luminance(r: int, g: int, b: int, output_type: type = None) -> int | float:
|
|
869
|
-
"""
|
|
870
|
-
|
|
871
|
-
The
|
|
872
|
-
- `int`
|
|
873
|
-
- `float`
|
|
874
|
-
- `None`
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
922
|
+
def luminance(r: int, g: int, b: int, output_type: Optional[type] = None, method: str = "wcag2") -> int | float:
|
|
923
|
+
"""Calculates the relative luminance of a color according to various standards.\n
|
|
924
|
+
----------------------------------------------------------------------------------
|
|
925
|
+
The `output_type` controls the range of the returned luminance value:
|
|
926
|
+
- `int` returns integer in [0, 100]
|
|
927
|
+
- `float` returns float in [0.0, 1.0]
|
|
928
|
+
- `None` returns integer in [0, 255]\n
|
|
929
|
+
The `method` is the luminance calculation method to use:
|
|
930
|
+
- `"wcag2"` WCAG 2.0 standard (default and most accurate for perception)
|
|
931
|
+
- `"wcag3"` Draft WCAG 3.0 standard with improved coefficients
|
|
932
|
+
- `"simple"` Simple arithmetic mean (less accurate)
|
|
933
|
+
- `"bt601"` ITU-R BT.601 standard (older TV standard)"""
|
|
934
|
+
_r, _g, _b = r / 255.0, g / 255.0, b / 255.0
|
|
935
|
+
if method == "simple":
|
|
936
|
+
luminance = (_r + _g + _b) / 3
|
|
937
|
+
elif method == "bt601":
|
|
938
|
+
luminance = 0.299 * _r + 0.587 * _g + 0.114 * _b
|
|
939
|
+
elif method == "wcag3":
|
|
940
|
+
_r = Color._linearize_srgb(_r)
|
|
941
|
+
_g = Color._linearize_srgb(_g)
|
|
942
|
+
_b = Color._linearize_srgb(_b)
|
|
943
|
+
luminance = 0.2126729 * _r + 0.7151522 * _g + 0.0721750 * _b
|
|
878
944
|
else:
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
945
|
+
_r = Color._linearize_srgb(_r)
|
|
946
|
+
_g = Color._linearize_srgb(_g)
|
|
947
|
+
_b = Color._linearize_srgb(_b)
|
|
948
|
+
luminance = 0.2126 * _r + 0.7152 * _g + 0.0722 * _b
|
|
949
|
+
if output_type == int:
|
|
950
|
+
return round(luminance * 100)
|
|
951
|
+
elif output_type == float:
|
|
952
|
+
return luminance
|
|
882
953
|
else:
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
954
|
+
return round(luminance * 255)
|
|
955
|
+
|
|
956
|
+
@staticmethod
|
|
957
|
+
def _linearize_srgb(c: float) -> float:
|
|
958
|
+
"""Helper method to linearize sRGB component following the WCAG standard."""
|
|
959
|
+
if c <= 0.03928:
|
|
960
|
+
return c / 12.92
|
|
886
961
|
else:
|
|
887
|
-
|
|
888
|
-
l = 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
889
|
-
return round(l * 100) if isinstance(output_type, int) else round(l * 255) if output_type is None else l
|
|
962
|
+
return ((c + 0.055) / 1.055)**2.4
|
|
890
963
|
|
|
891
964
|
@staticmethod
|
|
892
|
-
def text_color_for_on_bg(text_bg_color:
|
|
893
|
-
was_hexa, was_int = Color.is_valid_hexa(text_bg_color), isinstance(text_bg_color, int)
|
|
965
|
+
def text_color_for_on_bg(text_bg_color: Rgba | Hexa) -> rgba | hexa | int:
|
|
966
|
+
was_hexa, was_int = Color.is_valid_hexa(text_bg_color), isinstance(text_bg_color, int) # type: ignore[assignment]
|
|
894
967
|
text_bg_color = Color.to_rgba(text_bg_color)
|
|
895
968
|
brightness = 0.2126 * text_bg_color[0] + 0.7152 * text_bg_color[1] + 0.0722 * text_bg_color[2]
|
|
896
|
-
return ((hexa("", 255, 255, 255) if was_hexa else rgba(255, 255, 255, _validate=False))
|
|
969
|
+
return (((0xFFFFFF if was_int else hexa("", 255, 255, 255)) if was_hexa else rgba(255, 255, 255, _validate=False))
|
|
970
|
+
if brightness < 128 else
|
|
897
971
|
((0x000 if was_int else hexa("", 0, 0, 0)) if was_hexa else rgba(0, 0, 0, _validate=False)))
|
|
898
972
|
|
|
899
973
|
@staticmethod
|
|
900
|
-
def adjust_lightness(color:
|
|
974
|
+
def adjust_lightness(color: Rgba | Hexa, lightness_change: float) -> rgba | hexa:
|
|
901
975
|
"""In- or decrease the lightness of the input color.\n
|
|
902
976
|
-----------------------------------------------------------------------------------------------------
|
|
903
977
|
- color (rgba|hexa): HEX or RGBA color
|
|
904
978
|
- lightness_change (float): float between -1.0 (darken by `100%`) and 1.0 (lighten by `100%`)\n
|
|
905
979
|
-----------------------------------------------------------------------------------------------------
|
|
906
980
|
returns (rgba|hexa): the adjusted color in the format of the input color"""
|
|
907
|
-
was_hexa = Color.is_valid_hexa(color)
|
|
908
|
-
|
|
909
|
-
h, s, l, a = (
|
|
910
|
-
color[0],
|
|
911
|
-
color[1],
|
|
912
|
-
color[2],
|
|
913
|
-
color[3] if Color.has_alpha(color) else None,
|
|
914
|
-
)
|
|
981
|
+
was_hexa = Color.is_valid_hexa(color) # type: ignore[assignment]
|
|
982
|
+
_color: hsla = Color.to_hsla(color) # type: ignore[assignment]
|
|
983
|
+
h, s, l, a = (int(_color[0]), int(_color[1]), int(_color[2]), _color[3] if Color.has_alpha(_color) else None)
|
|
915
984
|
l = int(max(0, min(100, l + lightness_change * 100)))
|
|
916
985
|
return hsla(h, s, l, a, _validate=False).to_hexa() if was_hexa else hsla(h, s, l, a, _validate=False).to_rgba()
|
|
917
986
|
|
|
918
987
|
@staticmethod
|
|
919
|
-
def adjust_saturation(color:
|
|
988
|
+
def adjust_saturation(color: Rgba | Hsla | Hexa, saturation_change: float) -> rgba | hexa:
|
|
920
989
|
"""In- or decrease the saturation of the input color.\n
|
|
921
990
|
-----------------------------------------------------------------------------------------------------------
|
|
922
991
|
- color (rgba|hexa): HEX or RGBA color
|
|
923
992
|
- saturation_change (float): float between -1.0 (saturate by `100%`) and 1.0 (desaturate by `100%`)\n
|
|
924
993
|
-----------------------------------------------------------------------------------------------------------
|
|
925
994
|
returns (rgba|hexa): the adjusted color in the format of the input color"""
|
|
926
|
-
was_hexa = Color.is_valid_hexa(color)
|
|
927
|
-
|
|
928
|
-
h, s, l, a = (
|
|
929
|
-
color[0],
|
|
930
|
-
color[1],
|
|
931
|
-
color[2],
|
|
932
|
-
color[3] if Color.has_alpha(color) else None,
|
|
933
|
-
)
|
|
995
|
+
was_hexa = Color.is_valid_hexa(color) # type: ignore[assignment]
|
|
996
|
+
_color: hsla = Color.to_hsla(color) # type: ignore[assignment]
|
|
997
|
+
h, s, l, a = (int(_color[0]), int(_color[1]), int(_color[2]), _color[3] if Color.has_alpha(_color) else None)
|
|
934
998
|
s = int(max(0, min(100, s + saturation_change * 100)))
|
|
935
999
|
return hsla(h, s, l, a, _validate=False).to_hexa() if was_hexa else hsla(h, s, l, a, _validate=False).to_rgba()
|