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/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) -> iter:
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 __dict__(self) -> dict:
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
- self.r = self.g = self.b = Color.luminance(self.r, self.g, self.b)
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: "rgba", ratio: float = 0.5, additive_alpha: bool = False) -> "rgba":
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 = rgba(*other, _validate=False)
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
- r, g, b = r / 255.0, g / 255.0, b / 255.0
220
- max_c, min_c = max(r, g, b), min(r, g, b)
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 == r:
228
- h = ((g - b) / delta) % 6
229
- elif max_c == g:
230
- h = ((b - r) / delta) + 2
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 = ((r - g) / delta) + 4
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) -> iter:
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 __dict__(self) -> dict:
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}, {self.s}, {self.l}{"" if self.a is None else f", {self.a}"})'
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}, {self.s}, {self.l}{"" if self.a is None else f", {self.a}"})'
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
- l = Color.luminance(*self._hsl_to_rgb(self.h, self.s, self.l))
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: "hsla", ratio: float = 0.5, additive_alpha: bool = False) -> "rgba":
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
- h, s, l = h / 360, s / 100, l / 100
408
- if s == 0:
409
- r = g = b = int(l * 255)
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 = l * (1 + s) if l < 0.5 else l + s - l * s
426
- p = 2 * l - q
427
- r = int(round(hue_to_rgb(p, q, h + 1 / 3) * 255))
428
- g = int(round(hue_to_rgb(p, q, h) * 255))
429
- b = int(round(hue_to_rgb(p, q, h - 1 / 3) * 255))
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__(self, color: str | int, _r: int = None, _g: int = None, _b: int = None, _a: float = None):
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) -> iter:
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 __dict__(self) -> dict:
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
- self.r = self.g = self.b = Color.luminance(self.r, self.g, self.b)
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: "hexa", ratio: float = 0.5, additive_alpha: bool = False) -> "rgba":
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: str | list | tuple | dict, allow_alpha: bool = True) -> bool:
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
- return 0 <= color[0] <= 255 and 0 <= color[1] <= 255 and 0 <= color[2] <= 255
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
- return 0 <= color["r"] <= 255 and 0 <= color["g"] <= 255 and 0 <= color["b"] <= 255
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: str | list | tuple | dict, allow_alpha: bool = True) -> bool:
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
- else:
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
- else:
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(color: str | int, allow_alpha: bool = True, get_prefix: bool = False) -> bool | tuple[bool, str]:
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 = 0 <= color <= (0xFFFFFFFF if allow_alpha else 0xFFFFFF)
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: str | list | tuple | dict, allow_alpha: bool = True) -> bool:
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: rgba | hsla | hexa) -> bool:
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: hsla | hexa) -> rgba:
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: rgba | hexa) -> hsla:
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: rgba | hsla) -> hexa:
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.rgb_str(allow_alpha=True), string)
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.rgb_str(allow_alpha=True), string)
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
- """Gets the colors luminance using the luminance formula.\n
870
- ------------------------------------------------------------
871
- The param `output_type` can be set to:
872
- - `int` =⠀integer in [0, 100]
873
- - `float` =⠀float in [0.0, 1.0]
874
- - `None` =⠀integer in [0, 255]"""
875
- r, g, b = r / 255.0, g / 255.0, b / 255.0
876
- if r < 0.03928:
877
- r = r / 12.92
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
- r = ((r + 0.055) / 1.055)**2.4
880
- if g < 0.03928:
881
- g = g / 12.92
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
- g = ((g + 0.055) / 1.055)**2.4
884
- if b < 0.03928:
885
- b = b / 12.92
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
- b = ((b + 0.055) / 1.055)**2.4
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: rgba | hexa) -> rgba | hexa:
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)) if brightness < 128 else
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: rgba | hexa, lightness_change: float) -> rgba | hexa:
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
- color = Color.to_hsla(color)
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: rgba | hexa, saturation_change: float) -> rgba | hexa:
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
- color = Color.to_hsla(color)
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()