xulbux 1.6.0__py3-none-any.whl → 1.6.2__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
@@ -8,7 +8,7 @@
8
8
  `hexa`:
9
9
  A HEX color: is a string in the format `RGB`, `RGBA`, `RRGGBB` or `RRGGBBAA` (where `R` `G` `B` `A` are hexadecimal digits).
10
10
 
11
- ---------------------------------------------------------------------------------------------------------------------------------------
11
+ -------------------------------------------------------------------------------------------------------------------------------------
12
12
  The `Color` class, which contains all sorts of different color-related methods:
13
13
  - validate colors:
14
14
  - is valid rgba
@@ -37,9 +37,9 @@ import re as _re
37
37
 
38
38
 
39
39
  class rgba:
40
- """An RGB/RGBA color: is a tuple of 3 integers, representing the red (`0`-`255`), green (`0`-`255`), and blue (`0`-`255`).<br>
40
+ """An RGB/RGBA color: is a tuple of 3 integers, representing the red (`0`-`255`), green (`0`-`255`), and blue (`0`-`255`).\n
41
41
  Also includes an optional 4th param, which is a float, that represents the alpha channel (`0.0`-`1.0`).\n
42
- -------------------------------------------------------------------------------------------------------------------------------
42
+ -----------------------------------------------------------------------------------------------------------------------------
43
43
  Includes methods:
44
44
  - `to_hsla()` to convert to HSL color
45
45
  - `to_hexa()` to convert to HEX color
@@ -59,7 +59,10 @@ class rgba:
59
59
  - `with_alpha(alpha)` to create a new color with different alpha
60
60
  - `complementary()` to get the complementary color"""
61
61
 
62
- def __init__(self, r: int, g: int, b: int, a: float = None):
62
+ def __init__(self, r: int, g: int, b: int, a: float = None, _validate: bool = True):
63
+ if not _validate:
64
+ self.r, self.g, self.b, self.a = r, g, b, a
65
+ return
63
66
  if any(isinstance(x, rgba) for x in (r, g, b)):
64
67
  raise ValueError("Color is already a rgba() color")
65
68
  elif not all(isinstance(x, int) and 0 <= x <= 255 for x in (r, g, b)):
@@ -68,27 +71,27 @@ class rgba:
68
71
  (r, g, b),
69
72
  )
70
73
  elif a is not None and not (isinstance(a, (int, float)) and 0 <= a <= 1):
71
- raise ValueError("Alpha channel must be a float/int in [0.0, 1.0]: got", a)
74
+ raise ValueError(f"Alpha channel must be a float/int in [0.0, 1.0]: got '{a}'")
72
75
  self.r, self.g, self.b = r, g, b
73
- self.a = (1.0 if a > 1.0 else float(a)) if a else None
76
+ self.a = None if a is None else (1.0 if a > 1.0 else float(a))
74
77
 
75
78
  def __len__(self):
76
- return 4 if self.a else 3
79
+ return 3 if self.a is None else 4
77
80
 
78
81
  def __iter__(self):
79
- return iter((self.r, self.g, self.b) + ((self.a,) if self.a else ()))
82
+ return iter((self.r, self.g, self.b) + (() if self.a is None else (self.a,)))
80
83
 
81
84
  def __dict__(self):
82
85
  return self.dict()
83
86
 
84
87
  def __getitem__(self, index):
85
- return ((self.r, self.g, self.b) + ((self.a,) if self.a else ()))[index]
88
+ return ((self.r, self.g, self.b) + (() if self.a is None else (self.a,)))[index]
86
89
 
87
90
  def __repr__(self):
88
- return f'rgba({self.r}, {self.g}, {self.b}{f", {self.a}" if self.a else ""})'
91
+ return f'rgba({self.r}, {self.g}, {self.b}{"" if self.a is None else f", {self.a}"})'
89
92
 
90
93
  def __str__(self):
91
- return f'({self.r}, {self.g}, {self.b}{f", {self.a}" if self.a else ""})'
94
+ return f'({self.r}, {self.g}, {self.b}{"" if self.a is None else f", {self.a}"})'
92
95
 
93
96
  def __eq__(self, other):
94
97
  if not isinstance(other, rgba):
@@ -102,7 +105,7 @@ class rgba:
102
105
 
103
106
  def dict(self) -> dict:
104
107
  """Returns the color components as a dictionary with keys `'r'`, `'g'`, `'b'` and optionally `'a'`"""
105
- return dict(r=self.r, g=self.g, b=self.b, a=self.a) if self.a else dict(r=self.r, g=self.g, b=self.b)
108
+ return dict(r=self.r, g=self.g, b=self.b) if self.a is None else dict(r=self.r, g=self.g, b=self.b, a=self.a)
106
109
 
107
110
  def values(self) -> tuple:
108
111
  """Returns the color components as separate values `r, g, b, a`"""
@@ -110,11 +113,11 @@ class rgba:
110
113
 
111
114
  def to_hsla(self) -> "hsla":
112
115
  """Returns the color as a `hsla()` color"""
113
- return hsla(*self._rgb_to_hsl(self.r, self.g, self.b), self.a)
116
+ return hsla(*self._rgb_to_hsl(self.r, self.g, self.b), self.a, _validate=False)
114
117
 
115
118
  def to_hexa(self) -> "hexa":
116
119
  """Returns the color as a `hexa()` color"""
117
- return hexa(f'#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(self.a * 255):02X}" if self.a else ""}')
120
+ return hexa("", self.r, self.g, self.b, self.a)
118
121
 
119
122
  def has_alpha(self) -> bool:
120
123
  """Returns `True` if the color has an alpha channel and `False` otherwise"""
@@ -123,50 +126,50 @@ class rgba:
123
126
  def lighten(self, amount: float) -> "rgba":
124
127
  """Increases the colors lightness by the specified amount (`0.0`-`1.0`)"""
125
128
  self.r, self.g, self.b, self.a = self.to_hsla().lighten(amount).to_rgba().values()
126
- return rgba(self.r, self.g, self.b, self.a)
129
+ return rgba(self.r, self.g, self.b, self.a, _validate=False)
127
130
 
128
131
  def darken(self, amount: float) -> "rgba":
129
132
  """Decreases the colors lightness by the specified amount (`0.0`-`1.0`)"""
130
133
  self.r, self.g, self.b, self.a = self.to_hsla().darken(amount).to_rgba().values()
131
- return rgba(self.r, self.g, self.b, self.a)
134
+ return rgba(self.r, self.g, self.b, self.a, _validate=False)
132
135
 
133
136
  def saturate(self, amount: float) -> "rgba":
134
137
  """Increases the colors saturation by the specified amount (`0.0`-`1.0`)"""
135
138
  self.r, self.g, self.b, self.a = self.to_hsla().saturate(amount).to_rgba().values()
136
- return rgba(self.r, self.g, self.b, self.a)
139
+ return rgba(self.r, self.g, self.b, self.a, _validate=False)
137
140
 
138
141
  def desaturate(self, amount: float) -> "rgba":
139
142
  """Decreases the colors saturation by the specified amount (`0.0`-`1.0`)"""
140
143
  self.r, self.g, self.b, self.a = self.to_hsla().desaturate(amount).to_rgba().values()
141
- return rgba(self.r, self.g, self.b, self.a)
144
+ return rgba(self.r, self.g, self.b, self.a, _validate=False)
142
145
 
143
146
  def rotate(self, degrees: int) -> "rgba":
144
147
  """Rotates the colors hue by the specified number of degrees"""
145
148
  self.r, self.g, self.b, self.a = self.to_hsla().rotate(degrees).to_rgba().values()
146
- return rgba(self.r, self.g, self.b, self.a)
149
+ return rgba(self.r, self.g, self.b, self.a, _validate=False)
147
150
 
148
151
  def invert(self, invert_alpha: bool = False) -> "rgba":
149
152
  """Inverts the color by rotating hue by 180 degrees and inverting lightness"""
150
153
  self.r, self.g, self.b = 255 - self.r, 255 - self.g, 255 - self.b
151
154
  if invert_alpha:
152
155
  self.a = 1 - self.a
153
- return rgba(self.r, self.g, self.b, self.a)
156
+ return rgba(self.r, self.g, self.b, self.a, _validate=False)
154
157
 
155
158
  def grayscale(self) -> "rgba":
156
159
  """Converts the color to grayscale using the luminance formula"""
157
160
  self.r = self.g = self.b = Color.luminance(self.r, self.g, self.b)
158
- return rgba(self.r, self.g, self.b, self.a)
161
+ return rgba(self.r, self.g, self.b, self.a, _validate=False)
159
162
 
160
163
  def blend(self, other: "rgba", ratio: float = 0.5, additive_alpha: bool = False) -> "rgba":
161
- """Blends the current color with another color using the specified ratio (`0.0`-`1.0`):<br>
162
- If `ratio` is `0.0` it means 100% of the current color and 0% of the `other` color (2:0 mixture)<br>
163
- If `ratio` is `0.5` it means 50% of both colors (1:1 mixture)<br>
164
- If `ratio` is `1.0` it means 0% of the current color and 100% of the `other` color (0:2 mixture)"""
164
+ """Blends the current color with another color using the specified ratio (`0.0`-`1.0`):
165
+ - if `ratio` is `0.0` it means 100% of the current color and 0% of the `other` color (2:0 mixture)
166
+ - if `ratio` is `0.5` it means 50% of both colors (1:1 mixture)
167
+ - if `ratio` is `1.0` it means 0% of the current color and 100% of the `other` color (0:2 mixture)"""
165
168
  if not (isinstance(ratio, (int, float)) and 0 <= ratio <= 1):
166
169
  raise ValueError("'ratio' must be a float/int in [0.0, 1.0]")
167
170
  elif not isinstance(other, rgba):
168
171
  if Color.is_valid_rgba(other):
169
- other = rgba(*other)
172
+ other = rgba(*other, _validate=False)
170
173
  else:
171
174
  raise TypeError("'other' must be a valid RGBA color")
172
175
  ratio *= 2
@@ -175,7 +178,7 @@ class rgba:
175
178
  self.b = max(0, min(255, int(round((self.b * (2 - ratio)) + (other.b * ratio)))))
176
179
  none_alpha = self.a is None and (len(other) <= 3 or other[3] is None)
177
180
  if not none_alpha:
178
- self_a = self.a if self.a is not None else 1
181
+ self_a = 1 if self.a is None else self.a
179
182
  other_a = (other[3] if other[3] is not None else 1) if len(other) > 3 else 1
180
183
  if additive_alpha:
181
184
  self.a = max(0, min(1, (self_a * (2 - ratio)) + (other_a * ratio)))
@@ -189,11 +192,11 @@ class rgba:
189
192
  )
190
193
  else:
191
194
  self.a = None
192
- return rgba(self.r, self.g, self.b, None if none_alpha else self.a)
195
+ return rgba(self.r, self.g, self.b, None if none_alpha else self.a, _validate=False)
193
196
 
194
197
  def is_dark(self) -> bool:
195
198
  """Returns `True` if the color is considered dark (`lightness < 50%`)"""
196
- return (0.299 * self.r + 0.587 * self.g + 0.114 * self.b) < 128
199
+ return self.to_hsla().is_dark()
197
200
 
198
201
  def is_light(self) -> bool:
199
202
  """Returns `True` if the color is considered light (`lightness >= 50%`)"""
@@ -211,7 +214,7 @@ class rgba:
211
214
  """Returns a new color with the specified alpha value"""
212
215
  if not (isinstance(alpha, (int, float)) and 0 <= alpha <= 1):
213
216
  raise ValueError("'alpha' must be a float/int in [0.0, 1.0]")
214
- return rgba(self.r, self.g, self.b, alpha)
217
+ return rgba(self.r, self.g, self.b, alpha, _validate=False)
215
218
 
216
219
  def complementary(self) -> "rgba":
217
220
  """Returns the complementary color (180 degrees on the color wheel)"""
@@ -237,9 +240,9 @@ class rgba:
237
240
 
238
241
 
239
242
  class hsla:
240
- """A HSL/HSLA color: is a tuple of 3 integers, representing hue (`0`-`360`), saturation (`0`-`100`), and lightness (`0`-`100`).<br>
243
+ """A HSL/HSLA color: is a tuple of 3 integers, representing hue (`0`-`360`), saturation (`0`-`100`), and lightness (`0`-`100`).\n
241
244
  Also includes an optional 4th param, which is a float, that represents the alpha channel (`0.0`-`1.0`).\n
242
- ------------------------------------------------------------------------------------------------------------------------------------
245
+ ----------------------------------------------------------------------------------------------------------------------------------
243
246
  Includes methods:
244
247
  - `to_rgba()` to convert to RGB color
245
248
  - `to_hexa()` to convert to HEX color
@@ -259,7 +262,10 @@ class hsla:
259
262
  - `with_alpha(alpha)` to create a new color with different alpha
260
263
  - `complementary()` to get the complementary color"""
261
264
 
262
- def __init__(self, h: int, s: int, l: int, a: float = None):
265
+ def __init__(self, h: int, s: int, l: int, a: float = None, _validate: bool = True):
266
+ if not _validate:
267
+ self.h, self.s, self.l, self.a = h, s, l, a
268
+ return
263
269
  if any(isinstance(x, hsla) for x in (h, s, l)):
264
270
  raise ValueError("Color is already a hsla() color")
265
271
  elif not (isinstance(h, int) and (0 <= h <= 360) and all(isinstance(x, int) and (0 <= x <= 100) for x in (s, l))):
@@ -268,27 +274,27 @@ class hsla:
268
274
  (h, s, l),
269
275
  )
270
276
  elif a is not None and (not isinstance(a, (int, float)) or not 0 <= a <= 1):
271
- raise ValueError("Alpha channel must be a float/int in [0.0, 1.0]: got", a)
277
+ raise ValueError(f"Alpha channel must be a float/int in [0.0, 1.0]: got '{a}'")
272
278
  self.h, self.s, self.l = h, s, l
273
- self.a = (1.0 if a > 1.0 else float(a)) if a else None
279
+ self.a = None if a is None else (1.0 if a > 1.0 else float(a))
274
280
 
275
281
  def __len__(self):
276
- return 4 if self.a else 3
282
+ return 3 if self.a is None else 4
277
283
 
278
284
  def __iter__(self):
279
- return iter((self.h, self.s, self.l) + ((self.a,) if self.a else ()))
285
+ return iter((self.h, self.s, self.l) + (() if self.a is None else (self.a,)))
280
286
 
281
287
  def __dict__(self):
282
288
  return self.dict()
283
289
 
284
290
  def __getitem__(self, index):
285
- return ((self.h, self.s, self.l) + ((self.a,) if self.a else ()))[index]
291
+ return ((self.h, self.s, self.l) + (() if self.a is None else (self.a,)))[index]
286
292
 
287
293
  def __repr__(self):
288
- return f'hsla({self.h}, {self.s}, {self.l}{f", {self.a}" if self.a else ""})'
294
+ return f'hsla({self.h}, {self.s}, {self.l}{"" if self.a is None else f", {self.a}"})'
289
295
 
290
296
  def __str__(self):
291
- return f'({self.h}, {self.s}, {self.l}{f", {self.a}" if self.a else ""})'
297
+ return f'({self.h}, {self.s}, {self.l}{"" if self.a is None else f", {self.a}"})'
292
298
 
293
299
  def __eq__(self, other):
294
300
  if not isinstance(other, hsla):
@@ -302,7 +308,7 @@ class hsla:
302
308
 
303
309
  def dict(self) -> dict:
304
310
  """Returns the color components as a dictionary with keys `'h'`, `'s'`, `'l'` and optionally `'a'`"""
305
- return dict(h=self.h, s=self.s, l=self.l, a=self.a) if self.a else dict(h=self.h, s=self.s, l=self.l)
311
+ return dict(h=self.h, s=self.s, l=self.l) if self.a is None else dict(h=self.h, s=self.s, l=self.l, a=self.a)
306
312
 
307
313
  def values(self) -> tuple:
308
314
  """Returns the color components as separate values `h, s, l, a`"""
@@ -310,12 +316,12 @@ class hsla:
310
316
 
311
317
  def to_rgba(self) -> "rgba":
312
318
  """Returns the color as a `rgba()` color"""
313
- return rgba(*self._hsl_to_rgb(self.h, self.s, self.l), self.a)
319
+ return rgba(*self._hsl_to_rgb(self.h, self.s, self.l), self.a, _validate=False)
314
320
 
315
321
  def to_hexa(self) -> "hexa":
316
322
  """Returns the color as a `hexa()` color"""
317
323
  r, g, b = self._hsl_to_rgb(self.h, self.s, self.l)
318
- return hexa(f'#{r:02X}{g:02X}{b:02X}{f"{int(self.a * 255):02X}" if self.a else ""}')
324
+ return hexa("", r, g, b, self.a)
319
325
 
320
326
  def has_alpha(self) -> bool:
321
327
  """Returns `True` if the color has an alpha channel and `False` otherwise"""
@@ -326,33 +332,33 @@ class hsla:
326
332
  if not (isinstance(amount, (int, float)) and 0 <= amount <= 1):
327
333
  raise ValueError("'amount' must be a float/int in [0.0, 1.0]")
328
334
  self.l = int(min(100, self.l + (100 - self.l) * amount))
329
- return hsla(self.h, self.s, self.l, self.a)
335
+ return hsla(self.h, self.s, self.l, self.a, _validate=False)
330
336
 
331
337
  def darken(self, amount: float) -> "hsla":
332
338
  """Decreases the colors lightness by the specified amount (`0.0`-`1.0`)"""
333
339
  if not (isinstance(amount, (int, float)) and 0 <= amount <= 1):
334
340
  raise ValueError("'amount' must be a float/int in [0.0, 1.0]")
335
341
  self.l = int(max(0, self.l * (1 - amount)))
336
- return hsla(self.h, self.s, self.l, self.a)
342
+ return hsla(self.h, self.s, self.l, self.a, _validate=False)
337
343
 
338
344
  def saturate(self, amount: float) -> "hsla":
339
345
  """Increases the colors saturation by the specified amount (`0.0`-`1.0`)"""
340
346
  if not (isinstance(amount, (int, float)) and 0 <= amount <= 1):
341
347
  raise ValueError("'amount' must be a float/int in [0.0, 1.0]")
342
348
  self.s = int(min(100, self.s + (100 - self.s) * amount))
343
- return hsla(self.h, self.s, self.l, self.a)
349
+ return hsla(self.h, self.s, self.l, self.a, _validate=False)
344
350
 
345
351
  def desaturate(self, amount: float) -> "hsla":
346
352
  """Decreases the colors saturation by the specified amount (`0.0`-`1.0`)"""
347
353
  if not (isinstance(amount, (int, float)) and 0 <= amount <= 1):
348
354
  raise ValueError("'amount' must be a float/int in [0.0, 1.0]")
349
355
  self.s = int(max(0, self.s * (1 - amount)))
350
- return hsla(self.h, self.s, self.l, self.a)
356
+ return hsla(self.h, self.s, self.l, self.a, _validate=False)
351
357
 
352
358
  def rotate(self, degrees: int) -> "hsla":
353
359
  """Rotates the colors hue by the specified number of degrees"""
354
360
  self.h = (self.h + degrees) % 360
355
- return hsla(self.h, self.s, self.l, self.a)
361
+ return hsla(self.h, self.s, self.l, self.a, _validate=False)
356
362
 
357
363
  def invert(self, invert_alpha: bool = False) -> "hsla":
358
364
  """Inverts the color by rotating hue by 180 degrees and inverting lightness"""
@@ -360,21 +366,21 @@ class hsla:
360
366
  self.l = 100 - self.l
361
367
  if invert_alpha:
362
368
  self.a = 1 - self.a
363
- return hsla(self.h, self.s, self.l, self.a)
369
+ return hsla(self.h, self.s, self.l, self.a, _validate=False)
364
370
 
365
371
  def grayscale(self) -> "hsla":
366
372
  """Converts the color to grayscale using the luminance formula"""
367
373
  l = Color.luminance(*self._hsl_to_rgb(self.h, self.s, self.l))
368
- self.h, self.s, self.l, _ = rgba(l, l, l).to_hsla().values()
369
- return hsla(self.h, self.s, self.l, self.a)
374
+ self.h, self.s, self.l, _ = rgba(l, l, l, _validate=False).to_hsla().values()
375
+ return hsla(self.h, self.s, self.l, self.a, _validate=False)
370
376
 
371
377
  def blend(self, other: "hsla", ratio: float = 0.5, additive_alpha: bool = False) -> "rgba":
372
- """Blends the current color with another color using the specified ratio (`0.0`-`1.0`):<br>
373
- If `ratio` is `0.0` it means 100% of the current color and 0% of the `other` color (2:0 mixture)<br>
374
- If `ratio` is `0.5` it means 50% of both colors (1:1 mixture)<br>
375
- If `ratio` is `1.0` it means 0% of the current color and 100% of the `other` color (0:2 mixture)"""
378
+ """Blends the current color with another color using the specified ratio (`0.0`-`1.0`):
379
+ - if `ratio` is `0.0` it means 100% of the current color and 0% of the `other` color (2:0 mixture)
380
+ - if `ratio` is `0.5` it means 50% of both colors (1:1 mixture)
381
+ - if `ratio` is `1.0` it means 0% of the current color and 100% of the `other` color (0:2 mixture)"""
376
382
  self.h, self.s, self.l, self.a = self.to_rgba().blend(Color.to_rgba(other), ratio, additive_alpha).to_hsla().values()
377
- return hsla(self.h, self.s, self.l, self.a)
383
+ return hsla(self.h, self.s, self.l, self.a, _validate=False)
378
384
 
379
385
  def is_dark(self) -> bool:
380
386
  """Returns `True` if the color is considered dark (`lightness < 50%`)"""
@@ -396,11 +402,11 @@ class hsla:
396
402
  """Returns a new color with the specified alpha value"""
397
403
  if not (isinstance(alpha, (int, float)) and 0 <= alpha <= 1):
398
404
  raise ValueError("'alpha' must be a float/int in [0.0, 1.0]")
399
- return hsla(self.h, self.s, self.l, alpha)
405
+ return hsla(self.h, self.s, self.l, alpha, _validate=False)
400
406
 
401
407
  def complementary(self) -> "hsla":
402
408
  """Returns the complementary color (180 degrees on the color wheel)"""
403
- return hsla((self.h + 180) % 360, self.s, self.l, self.a)
409
+ return hsla((self.h + 180) % 360, self.s, self.l, self.a, _validate=False)
404
410
 
405
411
  def _hsl_to_rgb(self, h: int, s: int, l: int) -> tuple:
406
412
  h, s, l = h / 360, s / 100, l / 100
@@ -432,7 +438,7 @@ class hsla:
432
438
  class hexa:
433
439
  """A HEX color: is a string representing a hexadecimal color code with optional alpha channel.\n
434
440
  -------------------------------------------------------------------------------------------------
435
- Supports formats: RGB, RGBA, RRGGBB, RRGGBBAA (with or without prefix)<br>
441
+ Supports formats: RGB, RGBA, RRGGBB, RRGGBBAA (with or without prefix)
436
442
  Includes methods:
437
443
  - `to_rgba()` to convert to RGB color
438
444
  - `to_hsla()` to convert to HSL color
@@ -452,7 +458,10 @@ class hexa:
452
458
  - `with_alpha(alpha)` to create a new color with different alpha
453
459
  - `complementary()` to get the complementary color"""
454
460
 
455
- def __init__(self, color: str | int):
461
+ def __init__(self, color: str | int, _r: int = None, _g: int = None, _b: int = None, _a: float = None):
462
+ if all(x is not None for x in (_r, _g, _b)):
463
+ self.r, self.g, self.b, self.a = _r, _g, _b, _a
464
+ return
456
465
  if isinstance(color, hexa):
457
466
  raise ValueError("Color is already a hexa() color")
458
467
  if isinstance(color, str):
@@ -491,27 +500,31 @@ class hexa:
491
500
  else:
492
501
  raise ValueError(f"Invalid HEX format '{color}'")
493
502
  elif isinstance(color, int):
494
- self.r, self.g, self.b, self.a = Color.hex_int_to_rgba(color)
503
+ self.r, self.g, self.b, self.a = Color.hex_int_to_rgba(color).values()
495
504
  else:
496
- raise TypeError("HEX color must be of type 'str' or 'int': got", type(color))
505
+ raise TypeError(f"HEX color must be of type 'str' or 'int': got '{type(color)}'")
497
506
 
498
507
  def __len__(self):
499
- return 4 if self.a else 3
508
+ return 3 if self.a is None else 4
500
509
 
501
510
  def __iter__(self):
502
- return iter((f"{self.r:02X}", f"{self.g:02X}", f"{self.b:02X}") + ((f"{int(self.a * 255):02X}",) if self.a else ()))
511
+ return iter(
512
+ (f"{self.r:02X}", f"{self.g:02X}", f"{self.b:02X}") + (() if self.a is None else (f"{int(self.a * 255):02X}",))
513
+ )
503
514
 
504
515
  def __dict__(self):
505
516
  return self.dict()
506
517
 
507
518
  def __getitem__(self, index):
508
- return ((f"{self.r:02X}", f"{self.g:02X}", f"{self.b:02X}") + ((f"{int(self.a * 255):02X}",) if self.a else ()))[index]
519
+ return (
520
+ (f"{self.r:02X}", f"{self.g:02X}", f"{self.b:02X}") + (() if self.a is None else (f"{int(self.a * 255):02X}",))
521
+ )[index]
509
522
 
510
523
  def __repr__(self):
511
- return f'hexa(#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(self.a * 255):02X}" if self.a else ""})'
524
+ return f'hexa(#{self.r:02X}{self.g:02X}{self.b:02X}{"" if self.a is None else f"{int(self.a * 255):02X}"})'
512
525
 
513
526
  def __str__(self):
514
- return f'#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(self.a * 255):02X}" if self.a else ""}'
527
+ return f'#{self.r:02X}{self.g:02X}{self.b:02X}{"" if self.a is None else f"{int(self.a * 255):02X}"}'
515
528
 
516
529
  def __eq__(self, other):
517
530
  if not isinstance(other, hexa):
@@ -526,14 +539,14 @@ class hexa:
526
539
  def dict(self) -> dict:
527
540
  """Returns the color components as a dictionary with hex string values for keys `'r'`, `'g'`, `'b'` and optionally `'a'`"""
528
541
  return (
529
- dict(
542
+ dict(r=f"{self.r:02X}", g=f"{self.g:02X}", b=f"{self.b:02X}")
543
+ if self.a is None
544
+ else dict(
530
545
  r=f"{self.r:02X}",
531
546
  g=f"{self.g:02X}",
532
547
  b=f"{self.b:02X}",
533
548
  a=f"{int(self.a * 255):02X}",
534
549
  )
535
- if self.a
536
- else dict(r=f"{self.r:02X}", g=f"{self.g:02X}", b=f"{self.b:02X}")
537
550
  )
538
551
 
539
552
  def values(self) -> tuple:
@@ -546,7 +559,8 @@ class hexa:
546
559
  self.r,
547
560
  self.g,
548
561
  self.b,
549
- (round(self.a, 2) if round_alpha else self.a) if self.a else None,
562
+ None if self.a is None else (round(self.a, 2) if round_alpha else self.a),
563
+ _validate=False,
550
564
  )
551
565
 
552
566
  def to_hsla(self, round_alpha: bool = True) -> "hsla":
@@ -560,47 +574,47 @@ class hexa:
560
574
  def lighten(self, amount: float) -> "hexa":
561
575
  """Increases the colors lightness by the specified amount (`0.0`-`1.0`)"""
562
576
  self.r, self.g, self.b, self.a = self.to_rgba(False).lighten(amount).values()
563
- return hexa(f'#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(self.a * 255):02X}" if self.a else ""}')
577
+ return hexa("", self.r, self.g, self.b, self.a)
564
578
 
565
579
  def darken(self, amount: float) -> "hexa":
566
580
  """Decreases the colors lightness by the specified amount (`0.0`-`1.0`)"""
567
581
  self.r, self.g, self.b, self.a = self.to_rgba(False).darken(amount).values()
568
- return hexa(f'#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(self.a * 255):02X}" if self.a else ""}')
582
+ return hexa("", self.r, self.g, self.b, self.a)
569
583
 
570
584
  def saturate(self, amount: float) -> "hexa":
571
585
  """Increases the colors saturation by the specified amount (`0.0`-`1.0`)"""
572
586
  self.r, self.g, self.b, self.a = self.to_rgba(False).saturate(amount).values()
573
- return hexa(f'#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(self.a * 255):02X}" if self.a else ""}')
587
+ return hexa("", self.r, self.g, self.b, self.a)
574
588
 
575
589
  def desaturate(self, amount: float) -> "hexa":
576
590
  """Decreases the colors saturation by the specified amount (`0.0`-`1.0`)"""
577
591
  self.r, self.g, self.b, self.a = self.to_rgba(False).desaturate(amount).values()
578
- return hexa(f'#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(self.a * 255):02X}" if self.a else ""}')
592
+ return hexa("", self.r, self.g, self.b, self.a)
579
593
 
580
594
  def rotate(self, degrees: int) -> "hexa":
581
595
  """Rotates the colors hue by the specified number of degrees"""
582
596
  self.r, self.g, self.b, self.a = self.to_rgba(False).rotate(degrees).values()
583
- return hexa(f'#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(self.a * 255):02X}" if self.a else ""}')
597
+ return hexa("", self.r, self.g, self.b, self.a)
584
598
 
585
599
  def invert(self, invert_alpha: bool = False) -> "hexa":
586
600
  """Inverts the color by rotating hue by 180 degrees and inverting lightness"""
587
601
  self.r, self.g, self.b, self.a = self.to_rgba(False).invert().values()
588
602
  if invert_alpha:
589
603
  self.a = 1 - self.a
590
- return hexa(f'#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(self.a * 255):02X}" if self.a else ""}')
604
+ return hexa("", self.r, self.g, self.b, self.a)
591
605
 
592
606
  def grayscale(self) -> "hexa":
593
607
  """Converts the color to grayscale using the luminance formula"""
594
608
  self.r = self.g = self.b = Color.luminance(self.r, self.g, self.b)
595
- return hexa(f'#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(self.a * 255):02X}" if self.a else ""}')
609
+ return hexa("", self.r, self.g, self.b, self.a)
596
610
 
597
611
  def blend(self, other: "hexa", ratio: float = 0.5, additive_alpha: bool = False) -> "rgba":
598
- """Blends the current color with another color using the specified ratio (`0.0`-`1.0`):<br>
599
- If `ratio` is `0.0` it means 100% of the current color and 0% of the `other` color (2:0 mixture)<br>
600
- If `ratio` is `0.5` it means 50% of both colors (1:1 mixture)<br>
601
- If `ratio` is `1.0` it means 0% of the current color and 100% of the `other` color (0:2 mixture)"""
612
+ """Blends the current color with another color using the specified ratio (`0.0`-`1.0`):
613
+ - if `ratio` is `0.0` it means 100% of the current color and 0% of the `other` color (2:0 mixture)
614
+ - if `ratio` is `0.5` it means 50% of both colors (1:1 mixture)
615
+ - if `ratio` is `1.0` it means 0% of the current color and 100% of the `other` color (0:2 mixture)"""
602
616
  self.r, self.g, self.b, self.a = self.to_rgba(False).blend(Color.to_rgba(other), ratio, additive_alpha).values()
603
- return hexa(f'#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(self.a * 255):02X}" if self.a else ""}')
617
+ return hexa("", self.r, self.g, self.b, self.a)
604
618
 
605
619
  def is_dark(self) -> bool:
606
620
  """Returns `True` if the color is considered dark (`lightness < 50%`)"""
@@ -608,7 +622,7 @@ class hexa:
608
622
 
609
623
  def is_light(self) -> bool:
610
624
  """Returns `True` if the color is considered light (`lightness >= 50%`)"""
611
- return self.to_hsla(False).is_light()
625
+ return not self.is_dark()
612
626
 
613
627
  def is_grayscale(self) -> bool:
614
628
  """Returns `True` if the color is grayscale (`saturation == 0`)"""
@@ -616,13 +630,13 @@ class hexa:
616
630
 
617
631
  def is_opaque(self) -> bool:
618
632
  """Returns `True` if the color has no transparency (`alpha == 1.0`)"""
619
- return self.to_hsla(False).is_opaque()
633
+ return self.a == 1 or self.a is None
620
634
 
621
635
  def with_alpha(self, alpha: float) -> "hexa":
622
636
  """Returns a new color with the specified alpha value"""
623
637
  if not (isinstance(alpha, (int, float)) and 0 <= alpha <= 1):
624
638
  raise ValueError("'alpha' must be in [0.0, 1.0]")
625
- return hexa(f'#{self.r:02X}{self.g:02X}{self.b:02X}{f"{int(alpha * 255):02X}" if alpha else ""}')
639
+ return hexa("", self.r, self.g, self.b, alpha)
626
640
 
627
641
  def complementary(self) -> "hexa":
628
642
  """Returns the complementary color (180 degrees on the color wheel)"""
@@ -723,10 +737,9 @@ class Color:
723
737
  @staticmethod
724
738
  def has_alpha(color: rgba | hsla | hexa) -> bool:
725
739
  """Check if the given color has an alpha channel.\n
726
- --------------------------------------------------------------------------------
727
- Input a RGBA, HSLA or HEXA color as `color`.<br>
728
- Returns `True` if the color has an alpha channel and `False` otherwise.
729
- """
740
+ ---------------------------------------------------------------------------
741
+ Input a RGBA, HSLA or HEXA color as `color`.
742
+ Returns `True` if the color has an alpha channel and `False` otherwise."""
730
743
  if isinstance(color, (rgba, hsla, hexa)):
731
744
  return color.has_alpha()
732
745
  if Color.is_valid_hexa(color):
@@ -749,15 +762,11 @@ class Color:
749
762
  if isinstance(color, (hsla, hexa)):
750
763
  return color.to_rgba()
751
764
  elif Color.is_valid_hsla(color):
752
- return hsla(*color).to_rgba() if Color.has_alpha(color) else hsla(color[0], color[1], color[2]).to_rgba()
765
+ return hsla(*color, _validate=False).to_rgba()
753
766
  elif Color.is_valid_hexa(color):
754
767
  return hexa(color).to_rgba()
755
768
  elif Color.is_valid_rgba(color):
756
- return (
757
- color
758
- if isinstance(color, rgba)
759
- else (rgba(*color) if Color.has_alpha(color) else rgba(color[0], color[1], color[2]))
760
- )
769
+ return color if isinstance(color, rgba) else (rgba(*color, _validate=False))
761
770
  raise ValueError(f"Invalid color format '{color}'")
762
771
 
763
772
  @staticmethod
@@ -766,15 +775,11 @@ class Color:
766
775
  if isinstance(color, (rgba, hexa)):
767
776
  return color.to_hsla()
768
777
  elif Color.is_valid_rgba(color):
769
- return rgba(*color).to_hsla() if Color.has_alpha(color) else rgba(color[0], color[1], color[2]).to_hsla()
778
+ return rgba(*color, _validate=False).to_hsla()
770
779
  elif Color.is_valid_hexa(color):
771
780
  return hexa(color).to_hsla()
772
781
  elif Color.is_valid_hsla(color):
773
- return (
774
- color
775
- if isinstance(color, hsla)
776
- else (hsla(*color) if Color.has_alpha(color) else hsla(color[0], color[1], color[2]))
777
- )
782
+ return color if isinstance(color, hsla) else (hsla(*color, _validate=False))
778
783
  raise ValueError(f"Invalid color format '{color}'")
779
784
 
780
785
  @staticmethod
@@ -783,18 +788,18 @@ class Color:
783
788
  if isinstance(color, (rgba, hsla)):
784
789
  return color.to_hexa()
785
790
  elif Color.is_valid_rgba(color):
786
- return rgba(*color).to_hexa() if Color.has_alpha(color) else rgba(color[0], color[1], color[2]).to_hexa()
791
+ return rgba(*color, _validate=False).to_hexa()
787
792
  elif Color.is_valid_hsla(color):
788
- return hsla(*color).to_hexa() if Color.has_alpha(color) else hsla(color[0], color[1], color[2]).to_hexa()
793
+ return hsla(*color, _validate=False).to_hexa()
789
794
  elif Color.is_valid_hexa(color):
790
- return color if isinstance(color, hexa) else hexa(f"#{color}")
795
+ return color if isinstance(color, hexa) else hexa(color)
791
796
  raise ValueError(f"Invalid color format '{color}'")
792
797
 
793
798
  @staticmethod
794
799
  def str_to_rgba(string: str, only_first: bool = False) -> rgba | list[rgba] | None:
795
- """Will try to recognize RGBA colors inside a string and output the found ones as RGBA objects.<br>
796
- If `only_first` is `True` only the first found color will be returned (not as a list).
797
- """
800
+ """Will try to recognize RGBA colors inside a string and output the found ones as RGBA objects.\n
801
+ --------------------------------------------------------------------------------------------------
802
+ If `only_first` is `True` only the first found color will be returned (not as a list)."""
798
803
  if only_first:
799
804
  match = _re.search(Regex.rgb_str(allow_alpha=True), string)
800
805
  if not match:
@@ -805,6 +810,7 @@ class Color:
805
810
  int(m[1]),
806
811
  int(m[2]),
807
812
  ((int(m[3]) if "." not in m[3] else float(m[3])) if m[3] else None),
813
+ _validate=False,
808
814
  )
809
815
  else:
810
816
  matches = _re.findall(Regex.rgb_str(allow_alpha=True), string)
@@ -816,6 +822,7 @@ class Color:
816
822
  int(m[1]),
817
823
  int(m[2]),
818
824
  ((int(m[3]) if "." not in m[3] else float(m[3])) if m[3] else None),
825
+ _validate=False,
819
826
  )
820
827
  for m in matches
821
828
  ]
@@ -829,51 +836,55 @@ class Color:
829
836
  preserve_original: bool = False,
830
837
  ) -> int:
831
838
  """Convert RGBA channels to a HEXA integer (alpha is optional).\n
832
- -------------------------------------------------------------------------------------------------------------------------
833
- To preserve leading zeros, the function will add a `1` at the beginning, if the HEX integer would start with a `0`.<br>
834
- This could affect the color a little bit, but will make sure, that it won't be interpreted as a completely different<br>
835
- color, when initializing it as a `hexa()` color or changing it back to RGBA using `Color.hex_int_to_rgba()`.\n
836
- ⇾ **You can disable this behavior by setting `preserve_original` to `True`**"""
839
+ --------------------------------------------------------------------------------------------
840
+ To preserve leading zeros, the function will add a `1` at the beginning, if the HEX integer
841
+ would start with a `0`.
842
+ This could affect the color a little bit, but will make sure, that it won't be interpreted
843
+ as a completely different color, when initializing it as a `hexa()` color or changing it
844
+ back to RGBA using `Color.hex_int_to_rgba()`.\n
845
+ ⇾ You can disable this behavior by setting `preserve_original` to `True`"""
837
846
  r = max(0, min(255, int(r)))
838
847
  g = max(0, min(255, int(g)))
839
848
  b = max(0, min(255, int(b)))
840
- if a is not None:
849
+ if a is None:
850
+ hex_int = (r << 16) | (g << 8) | b
851
+ if not preserve_original and (hex_int & 0xF00000) == 0:
852
+ hex_int |= 0x010000
853
+ else:
841
854
  a = max(0, min(255, int(a * 255)))
842
855
  hex_int = (r << 24) | (g << 16) | (b << 8) | a
843
856
  if not preserve_original and r == 0:
844
857
  hex_int |= 0x01000000
845
- else:
846
- hex_int = (r << 16) | (g << 8) | b
847
- if not preserve_original and (hex_int & 0xF00000) == 0:
848
- hex_int |= 0x010000
849
858
  return hex_int
850
859
 
851
860
  @staticmethod
852
- def hex_int_to_rgba(hex_int: int, preserve_original: bool = False) -> tuple[int, int, int, float | None]:
861
+ def hex_int_to_rgba(hex_int: int, preserve_original: bool = False) -> rgba:
853
862
  """Convert a HEX integer to RGBA channels.\n
854
- -----------------------------------------------------------------------------------------------
855
- If the red channel is `1` after conversion, it will be set to `0`, because when converting<br>
856
- from RGBA to a HEX integer, the first `0` will be set to `1` to preserve leading zeros.<br>
863
+ -------------------------------------------------------------------------------------------
864
+ If the red channel is `1` after conversion, it will be set to `0`, because when converting
865
+ from RGBA to a HEX integer, the first `0` will be set to `1` to preserve leading zeros.
857
866
  This is the correction, so the color doesn't even look slightly different.\n
858
- ⇾ **You can disable this behavior by setting `preserve_original` to `True`**"""
867
+ ⇾ You can disable this behavior by setting `preserve_original` to `True`"""
859
868
  if not isinstance(hex_int, int):
860
869
  raise ValueError("Input must be an integer")
861
870
  hex_str = f"{hex_int:x}"
862
871
  if len(hex_str) <= 6:
863
872
  hex_str = hex_str.zfill(6)
864
- return (
873
+ return rgba(
865
874
  r if (r := int(hex_str[0:2], 16)) != 1 or preserve_original else 0,
866
875
  int(hex_str[2:4], 16),
867
876
  int(hex_str[4:6], 16),
868
877
  None,
878
+ _validate=False,
869
879
  )
870
880
  elif len(hex_str) <= 8:
871
881
  hex_str = hex_str.zfill(8)
872
- return (
882
+ return rgba(
873
883
  r if (r := int(hex_str[0:2], 16)) != 1 or preserve_original else 0,
874
884
  int(hex_str[2:4], 16),
875
885
  int(hex_str[4:6], 16),
876
886
  int(hex_str[6:8], 16) / 255.0,
887
+ _validate=False,
877
888
  )
878
889
  else:
879
890
  raise ValueError(f"Invalid HEX integer '0x{hex_str}': expected in range [0x000000, 0xFFFFFF]")
@@ -882,10 +893,10 @@ class Color:
882
893
  def luminance(r: int, g: int, b: int, output_type: type = None) -> int | float:
883
894
  """Gets the colors luminance using the luminance formula.\n
884
895
  ------------------------------------------------------------
885
- The param `output_type` can be set to:<br>
886
- *`int`* =⠀integer in [0, 100]<br>
887
- *`float`* =⠀float in [0.0, 1.0]<br>
888
- `None` =⠀integer in [0, 255]"""
896
+ The param `output_type` can be set to:
897
+ - `int` =⠀integer in [0, 100]
898
+ - `float` =⠀float in [0.0, 1.0]
899
+ - `None` =⠀integer in [0, 255]"""
889
900
  r, g, b = r / 255.0, g / 255.0, b / 255.0
890
901
  if r < 0.03928:
891
902
  r = r / 12.92
@@ -904,25 +915,25 @@ class Color:
904
915
 
905
916
  @staticmethod
906
917
  def text_color_for_on_bg(
907
- text_bg_color: rgba | hexa = 0xFFF,
918
+ text_bg_color: rgba | hexa = "#FFF",
908
919
  ) -> rgba | hexa:
909
920
  was_hexa, was_int = Color.is_valid_hexa(text_bg_color), isinstance(text_bg_color, int)
910
921
  text_bg_color = Color.to_rgba(text_bg_color)
911
922
  brightness = 0.2126 * text_bg_color[0] + 0.7152 * text_bg_color[1] + 0.0722 * text_bg_color[2]
912
923
  return (
913
- (hexa("#FFF") if was_hexa else rgba(255, 255, 255))
924
+ (hexa("", 255, 255, 255) if was_hexa else rgba(255, 255, 255, _validate=False))
914
925
  if brightness < 128
915
- else ((0x000 if was_int else hexa("#000")) if was_hexa else rgba(0, 0, 0))
926
+ else ((0x000 if was_int else hexa("", 0, 0, 0)) if was_hexa else rgba(0, 0, 0, _validate=False))
916
927
  )
917
928
 
918
929
  @staticmethod
919
930
  def adjust_lightness(color: rgba | hexa, lightness_change: float) -> rgba | hexa:
920
931
  """In- or decrease the lightness of the input color.\n
921
- ----------------------------------------------------------------------------------------------------
922
- **color** (rgba|hexa): HEX or RGBA color<br>
923
- **lightness_change** (float): float between -1.0 (darken by `100%`) and 1.0 (lighten by `100%`)\n
924
- ----------------------------------------------------------------------------------------------------
925
- **returns** (rgba|hexa): the adjusted color in the format of the input color"""
932
+ -----------------------------------------------------------------------------------------------------
933
+ - color (rgba|hexa): HEX or RGBA color
934
+ - lightness_change (float): float between -1.0 (darken by `100%`) and 1.0 (lighten by `100%`)\n
935
+ -----------------------------------------------------------------------------------------------------
936
+ returns (rgba|hexa): the adjusted color in the format of the input color"""
926
937
  was_hexa = Color.is_valid_hexa(color)
927
938
  color = Color.to_hsla(color)
928
939
  h, s, l, a = (
@@ -932,16 +943,16 @@ class Color:
932
943
  color[3] if Color.has_alpha(color) else None,
933
944
  )
934
945
  l = int(max(0, min(100, l + lightness_change * 100)))
935
- return Color.to_hexa((h, s, l, a)) if was_hexa else Color.to_rgba((h, s, l, a))
946
+ return hsla(h, s, l, a, _validate=False).to_hexa() if was_hexa else hsla(h, s, l, a, _validate=False).to_rgba()
936
947
 
937
948
  @staticmethod
938
949
  def adjust_saturation(color: rgba | hexa, saturation_change: float) -> rgba | hexa:
939
950
  """In- or decrease the saturation of the input color.\n
940
- ---------------------------------------------------------------------------------------------------------
941
- **color** (rgba|hexa): HEX or RGBA color<br>
942
- **saturation_change** (float): float between -1.0 (saturate by `100%`) and 1.0 (desaturate by `100%`)\n
943
- ---------------------------------------------------------------------------------------------------------
944
- **returns** (rgba|hexa): the adjusted color in the format of the input color"""
951
+ -----------------------------------------------------------------------------------------------------------
952
+ - color (rgba|hexa): HEX or RGBA color
953
+ - saturation_change (float): float between -1.0 (saturate by `100%`) and 1.0 (desaturate by `100%`)\n
954
+ -----------------------------------------------------------------------------------------------------------
955
+ returns (rgba|hexa): the adjusted color in the format of the input color"""
945
956
  was_hexa = Color.is_valid_hexa(color)
946
957
  color = Color.to_hsla(color)
947
958
  h, s, l, a = (
@@ -951,4 +962,4 @@ class Color:
951
962
  color[3] if Color.has_alpha(color) else None,
952
963
  )
953
964
  s = int(max(0, min(100, s + saturation_change * 100)))
954
- return Color.to_hexa((h, s, l, a)) if was_hexa else Color.to_rgba((h, s, l, a))
965
+ return hsla(h, s, l, a, _validate=False).to_hexa() if was_hexa else hsla(h, s, l, a, _validate=False).to_rgba()