stdl 0.6.7__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.
- stdl/__init__.py +2 -0
- stdl/color.py +1505 -0
- stdl/decorators.py +96 -0
- stdl/dt.py +355 -0
- stdl/fs.py +1711 -0
- stdl/import_lazy.py +123 -0
- stdl/log.py +142 -0
- stdl/lst.py +166 -0
- stdl/net.py +80 -0
- stdl/st.py +422 -0
- stdl-0.6.7.dist-info/METADATA +141 -0
- stdl-0.6.7.dist-info/RECORD +14 -0
- stdl-0.6.7.dist-info/WHEEL +4 -0
- stdl-0.6.7.dist-info/licenses/LICENSE +21 -0
stdl/color.py
ADDED
|
@@ -0,0 +1,1505 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import builtins
|
|
4
|
+
import re
|
|
5
|
+
import typing as T
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
|
+
from collections.abc import Sequence as SequenceType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ColorValueError(Exception):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Color(ABC):
|
|
16
|
+
"""Base class for all color representations"""
|
|
17
|
+
|
|
18
|
+
__slots__ = ("_frozen",)
|
|
19
|
+
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
object.__setattr__(self, "_frozen", False)
|
|
22
|
+
|
|
23
|
+
def _freeze(self) -> None:
|
|
24
|
+
object.__setattr__(self, "_frozen", True)
|
|
25
|
+
|
|
26
|
+
def to_str(self) -> builtins.str:
|
|
27
|
+
return self.__repr__()
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def validate(self) -> None:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def get_value_keys(self) -> list[builtins.str]:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def to_rgb(self) -> RGB:
|
|
39
|
+
"""Convert to RGB color space"""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def to_hex(self) -> HEX:
|
|
44
|
+
"""Convert to HEX color space"""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def to_hsv(self) -> HSV:
|
|
49
|
+
"""Convert to HSV color space"""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def to_hsl(self) -> HSL:
|
|
54
|
+
"""Convert to HSL color space"""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def to_cmyk(self) -> CMYK:
|
|
59
|
+
"""Convert to CMYK color space"""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def to_assa(self) -> ASSA:
|
|
64
|
+
"""Convert to ASS color format"""
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
def copy(self) -> Color:
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def dict(self) -> dict[builtins.str, int | float | builtins.str]:
|
|
71
|
+
return {k: getattr(self, k) for k in self.get_value_keys()}
|
|
72
|
+
|
|
73
|
+
def __setattr__(self, name: builtins.str, value: T.Any) -> None:
|
|
74
|
+
if getattr(self, "_frozen", False):
|
|
75
|
+
raise AttributeError(f"{self.__class__.__name__} is immutable")
|
|
76
|
+
object.__setattr__(self, name, value)
|
|
77
|
+
|
|
78
|
+
def __delattr__(self, name: builtins.str) -> None:
|
|
79
|
+
raise AttributeError(f"{self.__class__.__name__} is immutable")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class RGB(Color):
|
|
83
|
+
__slots__ = ("red", "green", "blue")
|
|
84
|
+
red: int
|
|
85
|
+
green: int
|
|
86
|
+
blue: int
|
|
87
|
+
|
|
88
|
+
def __init__(self, red: int, green: int, blue: int):
|
|
89
|
+
super().__init__()
|
|
90
|
+
self.red = int(red)
|
|
91
|
+
self.green = int(green)
|
|
92
|
+
self.blue = int(blue)
|
|
93
|
+
self.validate()
|
|
94
|
+
self._freeze()
|
|
95
|
+
|
|
96
|
+
def validate(self) -> None:
|
|
97
|
+
if not all(isinstance(x, int) and 0 <= x <= 255 for x in self.tuple()):
|
|
98
|
+
raise ColorValueError("RGB values must be between 0 and 255")
|
|
99
|
+
|
|
100
|
+
def get_value_keys(self) -> list[str]:
|
|
101
|
+
return ["red", "green", "blue"]
|
|
102
|
+
|
|
103
|
+
def __repr__(self) -> str:
|
|
104
|
+
return f"rgb({self.red}, {self.green}, {self.blue})"
|
|
105
|
+
|
|
106
|
+
def __eq__(self, other: object) -> bool:
|
|
107
|
+
if not isinstance(other, RGB):
|
|
108
|
+
return NotImplemented
|
|
109
|
+
return (self.red, self.green, self.blue) == (other.red, other.green, other.blue)
|
|
110
|
+
|
|
111
|
+
def tuple(self) -> tuple[int, int, int]:
|
|
112
|
+
return (self.red, self.green, self.blue)
|
|
113
|
+
|
|
114
|
+
def to_rgb(self) -> RGB:
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
def to_hsv(self) -> HSV:
|
|
118
|
+
r, g, b = self.red / 255, self.green / 255, self.blue / 255
|
|
119
|
+
cmax = max(r, g, b)
|
|
120
|
+
cmin = min(r, g, b)
|
|
121
|
+
diff = cmax - cmin
|
|
122
|
+
|
|
123
|
+
if cmax == cmin:
|
|
124
|
+
h = 0.0
|
|
125
|
+
elif cmax == r:
|
|
126
|
+
h = (60 * ((g - b) / diff) + 360) % 360
|
|
127
|
+
elif cmax == g:
|
|
128
|
+
h = (60 * ((b - r) / diff) + 120) % 360
|
|
129
|
+
else:
|
|
130
|
+
h = (60 * ((r - g) / diff) + 240) % 360
|
|
131
|
+
|
|
132
|
+
s = 0 if cmax == 0 else diff / cmax
|
|
133
|
+
v = cmax
|
|
134
|
+
|
|
135
|
+
return HSV(h, s * 100, v * 100)
|
|
136
|
+
|
|
137
|
+
def to_hsl(self) -> HSL:
|
|
138
|
+
r, g, b = self.red / 255, self.green / 255, self.blue / 255
|
|
139
|
+
cmax = max(r, g, b)
|
|
140
|
+
cmin = min(r, g, b)
|
|
141
|
+
diff = cmax - cmin
|
|
142
|
+
|
|
143
|
+
l = (cmax + cmin) / 2
|
|
144
|
+
|
|
145
|
+
if diff == 0:
|
|
146
|
+
h = 0.0
|
|
147
|
+
s = 0.0
|
|
148
|
+
else:
|
|
149
|
+
s = diff / (2 - cmax - cmin) if l > 0.5 else diff / (cmax + cmin)
|
|
150
|
+
|
|
151
|
+
if cmax == r:
|
|
152
|
+
h = (60 * ((g - b) / diff) + 360) % 360
|
|
153
|
+
elif cmax == g:
|
|
154
|
+
h = (60 * ((b - r) / diff) + 120) % 360
|
|
155
|
+
else:
|
|
156
|
+
h = (60 * ((r - g) / diff) + 240) % 360
|
|
157
|
+
|
|
158
|
+
return HSL(h, s * 100, l * 100)
|
|
159
|
+
|
|
160
|
+
def to_cmyk(self) -> CMYK:
|
|
161
|
+
r, g, b = self.red / 255, self.green / 255, self.blue / 255
|
|
162
|
+
|
|
163
|
+
k = 1 - max(r, g, b)
|
|
164
|
+
if k == 1:
|
|
165
|
+
c = m = y = 0.0
|
|
166
|
+
else:
|
|
167
|
+
c = (1 - r - k) / (1 - k)
|
|
168
|
+
m = (1 - g - k) / (1 - k)
|
|
169
|
+
y = (1 - b - k) / (1 - k)
|
|
170
|
+
|
|
171
|
+
return CMYK(c * 100, m * 100, y * 100, k * 100)
|
|
172
|
+
|
|
173
|
+
def to_hex(self) -> HEX:
|
|
174
|
+
return HEX(f"#{self.red:02x}{self.green:02x}{self.blue:02x}")
|
|
175
|
+
|
|
176
|
+
def to_rgba(self, alpha: float = 1.0) -> RGBA:
|
|
177
|
+
return RGBA(self.red, self.green, self.blue, alpha)
|
|
178
|
+
|
|
179
|
+
def to_assa(self) -> ASSA:
|
|
180
|
+
return ASSA.from_rgb(self)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class RGBA(Color):
|
|
184
|
+
__slots__ = ("red", "green", "blue", "alpha")
|
|
185
|
+
red: int
|
|
186
|
+
green: int
|
|
187
|
+
blue: int
|
|
188
|
+
alpha: float
|
|
189
|
+
|
|
190
|
+
def __init__(self, red: int, green: int, blue: int, alpha: float = 1.0):
|
|
191
|
+
super().__init__()
|
|
192
|
+
self.red = int(red)
|
|
193
|
+
self.green = int(green)
|
|
194
|
+
self.blue = int(blue)
|
|
195
|
+
self.alpha = float(alpha)
|
|
196
|
+
self.validate()
|
|
197
|
+
self._freeze()
|
|
198
|
+
|
|
199
|
+
def __repr__(self) -> str:
|
|
200
|
+
return f"rgba({self.red}, {self.green}, {self.blue}, {self.alpha})"
|
|
201
|
+
|
|
202
|
+
def __eq__(self, other: object) -> bool:
|
|
203
|
+
if not isinstance(other, RGBA):
|
|
204
|
+
return NotImplemented
|
|
205
|
+
return (self.red, self.green, self.blue, self.alpha) == (
|
|
206
|
+
other.red,
|
|
207
|
+
other.green,
|
|
208
|
+
other.blue,
|
|
209
|
+
other.alpha,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def validate(self) -> None:
|
|
213
|
+
if not all(
|
|
214
|
+
isinstance(x, (int, float)) and 0 <= x <= 255 for x in (self.red, self.green, self.blue)
|
|
215
|
+
):
|
|
216
|
+
raise ColorValueError("RGB values must be 0-255")
|
|
217
|
+
if not isinstance(self.alpha, (int, float)) or not 0 <= self.alpha <= 1:
|
|
218
|
+
raise ColorValueError("Alpha value must be 0-1")
|
|
219
|
+
|
|
220
|
+
def get_value_keys(self) -> list[str]:
|
|
221
|
+
return ["red", "green", "blue", "alpha"]
|
|
222
|
+
|
|
223
|
+
def tuple(self) -> tuple[int, int, int, float]:
|
|
224
|
+
return (self.red, self.green, self.blue, self.alpha)
|
|
225
|
+
|
|
226
|
+
def to_rgb(self) -> RGB:
|
|
227
|
+
return RGB(self.red, self.green, self.blue)
|
|
228
|
+
|
|
229
|
+
def to_rgba(self) -> RGBA:
|
|
230
|
+
return self
|
|
231
|
+
|
|
232
|
+
def to_hsv(self) -> HSV:
|
|
233
|
+
return self.to_rgb().to_hsv()
|
|
234
|
+
|
|
235
|
+
def to_hsl(self) -> HSL:
|
|
236
|
+
return self.to_rgb().to_hsl()
|
|
237
|
+
|
|
238
|
+
def to_cmyk(self) -> CMYK:
|
|
239
|
+
return self.to_rgb().to_cmyk()
|
|
240
|
+
|
|
241
|
+
def to_hex(self) -> HEX:
|
|
242
|
+
return self.to_rgb().to_hex()
|
|
243
|
+
|
|
244
|
+
def to_assa(self) -> ASSA:
|
|
245
|
+
return ASSA.from_rgba(self)
|
|
246
|
+
|
|
247
|
+
@classmethod
|
|
248
|
+
def from_assa(cls, assa: ASSA) -> RGBA:
|
|
249
|
+
rgb = assa.to_rgb()
|
|
250
|
+
alpha = assa.alpha
|
|
251
|
+
if alpha is None:
|
|
252
|
+
return cls(rgb.red, rgb.green, rgb.blue, 1.0)
|
|
253
|
+
# ASSA alpha (FF=transparent) -> RGBA alpha (1=opaque)
|
|
254
|
+
return cls(rgb.red, rgb.green, rgb.blue, 1 - (alpha / 255))
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class HEX(Color):
|
|
258
|
+
__slots__ = ("value",)
|
|
259
|
+
value: str
|
|
260
|
+
|
|
261
|
+
def __init__(self, value: str):
|
|
262
|
+
super().__init__()
|
|
263
|
+
if value.startswith("#"):
|
|
264
|
+
value = value[1:]
|
|
265
|
+
self.value = f"#{value.lower()}"
|
|
266
|
+
self.validate()
|
|
267
|
+
self._freeze()
|
|
268
|
+
|
|
269
|
+
def validate(self) -> None:
|
|
270
|
+
if not isinstance(self.value, str):
|
|
271
|
+
raise ColorValueError("HEX value must be a string")
|
|
272
|
+
hex_value = self.value.lstrip("#")
|
|
273
|
+
if len(hex_value) != 6 or not all(c in "0123456789ABCDEFabcdef" for c in hex_value):
|
|
274
|
+
raise ColorValueError(f"Invalid HEX color format ({hex_value})")
|
|
275
|
+
|
|
276
|
+
def get_value_keys(self) -> list[str]:
|
|
277
|
+
return ["value"]
|
|
278
|
+
|
|
279
|
+
def __repr__(self) -> str:
|
|
280
|
+
return f"hex({self.value})"
|
|
281
|
+
|
|
282
|
+
def __eq__(self, other: object) -> bool:
|
|
283
|
+
if not isinstance(other, HEX):
|
|
284
|
+
return NotImplemented
|
|
285
|
+
return self.value.lower() == other.value.lower()
|
|
286
|
+
|
|
287
|
+
def to_str(self) -> builtins.str:
|
|
288
|
+
return self.value
|
|
289
|
+
|
|
290
|
+
def to_rgb(self) -> RGB:
|
|
291
|
+
hex_value = self.value.lstrip("#")
|
|
292
|
+
return RGB(int(hex_value[0:2], 16), int(hex_value[2:4], 16), int(hex_value[4:6], 16))
|
|
293
|
+
|
|
294
|
+
def to_hsv(self) -> HSV:
|
|
295
|
+
return self.to_rgb().to_hsv()
|
|
296
|
+
|
|
297
|
+
def to_hsl(self) -> HSL:
|
|
298
|
+
return self.to_rgb().to_hsl()
|
|
299
|
+
|
|
300
|
+
def to_cmyk(self) -> CMYK:
|
|
301
|
+
return self.to_rgb().to_cmyk()
|
|
302
|
+
|
|
303
|
+
def to_hex(self) -> HEX:
|
|
304
|
+
return self
|
|
305
|
+
|
|
306
|
+
def to_assa(self) -> ASSA:
|
|
307
|
+
return self.to_rgb().to_assa()
|
|
308
|
+
|
|
309
|
+
def to_webcolor(self) -> webcolor:
|
|
310
|
+
return webcolor.from_hex(self.value)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class HSV(Color):
|
|
314
|
+
__slots__ = ("hue", "saturation", "value")
|
|
315
|
+
hue: float
|
|
316
|
+
saturation: float
|
|
317
|
+
value: float
|
|
318
|
+
|
|
319
|
+
def __init__(self, hue: float, saturation: float, value: float):
|
|
320
|
+
super().__init__()
|
|
321
|
+
self.hue = float(hue)
|
|
322
|
+
self.saturation = float(saturation)
|
|
323
|
+
self.value = float(value)
|
|
324
|
+
self.validate()
|
|
325
|
+
self._freeze()
|
|
326
|
+
|
|
327
|
+
def validate(self) -> None:
|
|
328
|
+
if not (0 <= self.hue <= 360 and 0 <= self.saturation <= 100 and 0 <= self.value <= 100):
|
|
329
|
+
raise ColorValueError("HSV values must be: H[0-360], S[0-100], V[0-100]")
|
|
330
|
+
|
|
331
|
+
def get_value_keys(self) -> list[str]:
|
|
332
|
+
return ["hue", "saturation", "value"]
|
|
333
|
+
|
|
334
|
+
def __repr__(self) -> str:
|
|
335
|
+
return f"hsv({self.hue}, {self.saturation}, {self.value})"
|
|
336
|
+
|
|
337
|
+
def to_str(self) -> builtins.str:
|
|
338
|
+
return f"hsv({self.hue}°, {self.saturation}%, {self.value}%)"
|
|
339
|
+
|
|
340
|
+
def __eq__(self, other: object) -> bool:
|
|
341
|
+
if not isinstance(other, HSV):
|
|
342
|
+
return NotImplemented
|
|
343
|
+
return (self.hue, self.saturation, self.value) == (
|
|
344
|
+
other.hue,
|
|
345
|
+
other.saturation,
|
|
346
|
+
other.value,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def tuple(self) -> tuple[float, float, float]:
|
|
350
|
+
return (self.hue, self.saturation, self.value)
|
|
351
|
+
|
|
352
|
+
def to_rgb(self) -> RGB:
|
|
353
|
+
h, s, v = self.hue, self.saturation / 100, self.value / 100
|
|
354
|
+
c = v * s
|
|
355
|
+
x = c * (1 - abs((h / 60) % 2 - 1))
|
|
356
|
+
m = v - c
|
|
357
|
+
|
|
358
|
+
if 0 <= h < 60:
|
|
359
|
+
r, g, b = c, x, 0.0
|
|
360
|
+
elif 60 <= h < 120:
|
|
361
|
+
r, g, b = x, c, 0.0
|
|
362
|
+
elif 120 <= h < 180:
|
|
363
|
+
r, g, b = 0.0, c, x
|
|
364
|
+
elif 180 <= h < 240:
|
|
365
|
+
r, g, b = 0.0, x, c
|
|
366
|
+
elif 240 <= h < 300:
|
|
367
|
+
r, g, b = x, 0.0, c
|
|
368
|
+
else:
|
|
369
|
+
r, g, b = c, 0.0, x
|
|
370
|
+
|
|
371
|
+
return RGB(round((r + m) * 255), round((g + m) * 255), round((b + m) * 255))
|
|
372
|
+
|
|
373
|
+
def to_hsv(self) -> HSV:
|
|
374
|
+
return self
|
|
375
|
+
|
|
376
|
+
def to_hsl(self) -> HSL:
|
|
377
|
+
return self.to_rgb().to_hsl()
|
|
378
|
+
|
|
379
|
+
def to_cmyk(self) -> CMYK:
|
|
380
|
+
return self.to_rgb().to_cmyk()
|
|
381
|
+
|
|
382
|
+
def to_hex(self) -> HEX:
|
|
383
|
+
return self.to_rgb().to_hex()
|
|
384
|
+
|
|
385
|
+
def to_assa(self) -> ASSA:
|
|
386
|
+
return self.to_rgb().to_assa()
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class HSL(Color):
|
|
390
|
+
__slots__ = ("hue", "saturation", "lightness")
|
|
391
|
+
hue: float
|
|
392
|
+
saturation: float
|
|
393
|
+
lightness: float
|
|
394
|
+
|
|
395
|
+
def __init__(self, hue: float, saturation: float, lightness: float):
|
|
396
|
+
super().__init__()
|
|
397
|
+
self.hue = float(hue)
|
|
398
|
+
self.saturation = float(saturation)
|
|
399
|
+
self.lightness = float(lightness)
|
|
400
|
+
self.validate()
|
|
401
|
+
self._freeze()
|
|
402
|
+
|
|
403
|
+
def validate(self) -> None:
|
|
404
|
+
if not (
|
|
405
|
+
0 <= self.hue <= 360 and 0 <= self.saturation <= 100 and 0 <= self.lightness <= 100
|
|
406
|
+
):
|
|
407
|
+
raise ColorValueError("HSL values must be: H[0-360], S[0-100], L[0-100]")
|
|
408
|
+
|
|
409
|
+
def get_value_keys(self) -> list[str]:
|
|
410
|
+
return ["hue", "saturation", "lightness"]
|
|
411
|
+
|
|
412
|
+
def __repr__(self) -> str:
|
|
413
|
+
return f"hsl({self.hue}, {self.saturation}, {self.lightness})"
|
|
414
|
+
|
|
415
|
+
def to_str(self) -> builtins.str:
|
|
416
|
+
return f"hsl({self.hue}°, {self.saturation}%, {self.lightness}%)"
|
|
417
|
+
|
|
418
|
+
def __eq__(self, other: object) -> bool:
|
|
419
|
+
if not isinstance(other, HSL):
|
|
420
|
+
return NotImplemented
|
|
421
|
+
return (self.hue, self.saturation, self.lightness) == (
|
|
422
|
+
other.hue,
|
|
423
|
+
other.saturation,
|
|
424
|
+
other.lightness,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
def tuple(self) -> tuple[float, float, float]:
|
|
428
|
+
return (self.hue, self.saturation, self.lightness)
|
|
429
|
+
|
|
430
|
+
def to_rgb(self) -> RGB:
|
|
431
|
+
hue, saturation, lightness = self.hue / 360, self.saturation / 100, self.lightness / 100
|
|
432
|
+
|
|
433
|
+
if saturation == 0:
|
|
434
|
+
r = g = b = lightness
|
|
435
|
+
else:
|
|
436
|
+
|
|
437
|
+
def hue_to_rgb(p: float, q: float, t: float) -> float:
|
|
438
|
+
if t < 0:
|
|
439
|
+
t += 1
|
|
440
|
+
if t > 1:
|
|
441
|
+
t -= 1
|
|
442
|
+
if t < 1 / 6:
|
|
443
|
+
return p + (q - p) * 6 * t
|
|
444
|
+
if t < 1 / 2:
|
|
445
|
+
return q
|
|
446
|
+
if t < 2 / 3:
|
|
447
|
+
return p + (q - p) * (2 / 3 - t) * 6
|
|
448
|
+
return p
|
|
449
|
+
|
|
450
|
+
q = (
|
|
451
|
+
lightness * (1 + saturation)
|
|
452
|
+
if lightness < 0.5
|
|
453
|
+
else lightness + saturation - lightness * saturation
|
|
454
|
+
)
|
|
455
|
+
p = 2 * lightness - q
|
|
456
|
+
|
|
457
|
+
r = hue_to_rgb(p, q, hue + 1 / 3)
|
|
458
|
+
g = hue_to_rgb(p, q, hue)
|
|
459
|
+
b = hue_to_rgb(p, q, hue - 1 / 3)
|
|
460
|
+
|
|
461
|
+
return RGB(round(r * 255), round(g * 255), round(b * 255))
|
|
462
|
+
|
|
463
|
+
def to_hsv(self) -> HSV:
|
|
464
|
+
return self.to_rgb().to_hsv()
|
|
465
|
+
|
|
466
|
+
def to_hsl(self) -> HSL:
|
|
467
|
+
return self
|
|
468
|
+
|
|
469
|
+
def to_cmyk(self) -> CMYK:
|
|
470
|
+
return self.to_rgb().to_cmyk()
|
|
471
|
+
|
|
472
|
+
def to_hex(self) -> HEX:
|
|
473
|
+
return self.to_rgb().to_hex()
|
|
474
|
+
|
|
475
|
+
def to_assa(self) -> ASSA:
|
|
476
|
+
return self.to_rgb().to_assa()
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
class CMYK(Color):
|
|
480
|
+
__slots__ = ("cyan", "magenta", "yellow", "key")
|
|
481
|
+
cyan: float
|
|
482
|
+
magenta: float
|
|
483
|
+
yellow: float
|
|
484
|
+
key: float
|
|
485
|
+
|
|
486
|
+
def __init__(self, cyan: float, magenta: float, yellow: float, key: float):
|
|
487
|
+
super().__init__()
|
|
488
|
+
self.cyan = float(cyan)
|
|
489
|
+
self.magenta = float(magenta)
|
|
490
|
+
self.yellow = float(yellow)
|
|
491
|
+
self.key = float(key)
|
|
492
|
+
self.validate()
|
|
493
|
+
self._freeze()
|
|
494
|
+
|
|
495
|
+
def validate(self) -> None:
|
|
496
|
+
if not all(0 <= x <= 100 for x in (self.cyan, self.magenta, self.yellow, self.key)):
|
|
497
|
+
raise ColorValueError("CMYK values must be between 0 and 100")
|
|
498
|
+
|
|
499
|
+
def get_value_keys(self) -> list[str]:
|
|
500
|
+
return ["cyan", "magenta", "yellow", "key"]
|
|
501
|
+
|
|
502
|
+
def __repr__(self) -> str:
|
|
503
|
+
return f"cmyk({self.cyan}, {self.magenta}, {self.yellow}, {self.key})"
|
|
504
|
+
|
|
505
|
+
def to_str(self) -> builtins.str:
|
|
506
|
+
return f"cmyk({self.cyan}%, {self.magenta}%, {self.yellow}%, {self.key}%)"
|
|
507
|
+
|
|
508
|
+
def __eq__(self, other: object) -> bool:
|
|
509
|
+
if not isinstance(other, CMYK):
|
|
510
|
+
return NotImplemented
|
|
511
|
+
return (self.cyan, self.magenta, self.yellow, self.key) == (
|
|
512
|
+
other.cyan,
|
|
513
|
+
other.magenta,
|
|
514
|
+
other.yellow,
|
|
515
|
+
other.key,
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
def tuple(self) -> tuple[float, float, float, float]:
|
|
519
|
+
return (self.cyan, self.magenta, self.yellow, self.key)
|
|
520
|
+
|
|
521
|
+
def to_rgb(self) -> RGB:
|
|
522
|
+
c, m, y, k = self.cyan / 100, self.magenta / 100, self.yellow / 100, self.key / 100
|
|
523
|
+
|
|
524
|
+
r = 255 * (1 - c) * (1 - k)
|
|
525
|
+
g = 255 * (1 - m) * (1 - k)
|
|
526
|
+
b = 255 * (1 - y) * (1 - k)
|
|
527
|
+
|
|
528
|
+
return RGB(round(r), round(g), round(b))
|
|
529
|
+
|
|
530
|
+
def to_hsv(self) -> HSV:
|
|
531
|
+
return self.to_rgb().to_hsv()
|
|
532
|
+
|
|
533
|
+
def to_hsl(self) -> HSL:
|
|
534
|
+
return self.to_rgb().to_hsl()
|
|
535
|
+
|
|
536
|
+
def to_cmyk(self) -> CMYK:
|
|
537
|
+
return self
|
|
538
|
+
|
|
539
|
+
def to_hex(self) -> HEX:
|
|
540
|
+
return self.to_rgb().to_hex()
|
|
541
|
+
|
|
542
|
+
def to_assa(self) -> ASSA:
|
|
543
|
+
return self.to_rgb().to_assa()
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
class ASSA(Color):
|
|
547
|
+
__slots__ = ("value",)
|
|
548
|
+
value: str
|
|
549
|
+
|
|
550
|
+
def __init__(self, value: str):
|
|
551
|
+
"""
|
|
552
|
+
Initialize ASSA color with just hex numbers:
|
|
553
|
+
- 'BBGGRR' for color
|
|
554
|
+
- 'AABBGGRR' for color with alpha
|
|
555
|
+
"""
|
|
556
|
+
super().__init__()
|
|
557
|
+
clean_value = "".join(c for c in value.lower() if c in "0123456789abcdef")
|
|
558
|
+
self.value = f"&H{clean_value}&"
|
|
559
|
+
self.validate()
|
|
560
|
+
self._freeze()
|
|
561
|
+
|
|
562
|
+
def get_value_keys(self) -> list[str]:
|
|
563
|
+
return ["value"]
|
|
564
|
+
|
|
565
|
+
@property
|
|
566
|
+
def clean_value(self) -> str:
|
|
567
|
+
value = self.value
|
|
568
|
+
if (value.startswith("&H") or value.startswith("&h")) and value.endswith("&"):
|
|
569
|
+
return value[2:-1].lower()
|
|
570
|
+
return value.lower()
|
|
571
|
+
|
|
572
|
+
def __repr__(self) -> str:
|
|
573
|
+
return f"assa({self.clean_value})"
|
|
574
|
+
|
|
575
|
+
def to_str(self) -> builtins.str:
|
|
576
|
+
return self.value
|
|
577
|
+
|
|
578
|
+
def __eq__(self, other: object) -> bool:
|
|
579
|
+
if not isinstance(other, ASSA):
|
|
580
|
+
return NotImplemented
|
|
581
|
+
return self.value == other.value
|
|
582
|
+
|
|
583
|
+
def validate(self) -> None:
|
|
584
|
+
if not self.is_valid_format(self.value):
|
|
585
|
+
raise ColorValueError(
|
|
586
|
+
"Color value must be either 6 (BBGGRR) or 8 (AABBGGRR) hexadecimal digits"
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
def is_BBGGRR_format(self) -> bool:
|
|
590
|
+
return len(self.clean_value) == 6
|
|
591
|
+
|
|
592
|
+
def is_AABBGGRR_format(self) -> bool:
|
|
593
|
+
return len(self.clean_value) == 8
|
|
594
|
+
|
|
595
|
+
def to_rgb(self) -> RGB:
|
|
596
|
+
clean = self.clean_value
|
|
597
|
+
if len(clean) == 6:
|
|
598
|
+
blue_hex = clean[0:2]
|
|
599
|
+
green_hex = clean[2:4]
|
|
600
|
+
red_hex = clean[4:6]
|
|
601
|
+
else:
|
|
602
|
+
blue_hex = clean[2:4]
|
|
603
|
+
green_hex = clean[4:6]
|
|
604
|
+
red_hex = clean[6:8]
|
|
605
|
+
return RGB(int(red_hex, 16), int(green_hex, 16), int(blue_hex, 16))
|
|
606
|
+
|
|
607
|
+
@property
|
|
608
|
+
def blue(self) -> int:
|
|
609
|
+
clean = self.clean_value
|
|
610
|
+
if len(clean) == 6:
|
|
611
|
+
return int(clean[0:2], 16)
|
|
612
|
+
else:
|
|
613
|
+
return int(clean[2:4], 16)
|
|
614
|
+
|
|
615
|
+
@property
|
|
616
|
+
def green(self) -> int:
|
|
617
|
+
clean = self.clean_value
|
|
618
|
+
if len(clean) == 6:
|
|
619
|
+
return int(clean[2:4], 16)
|
|
620
|
+
else:
|
|
621
|
+
return int(clean[4:6], 16)
|
|
622
|
+
|
|
623
|
+
@property
|
|
624
|
+
def red(self) -> int:
|
|
625
|
+
clean = self.clean_value
|
|
626
|
+
if len(clean) == 6:
|
|
627
|
+
return int(clean[4:6], 16)
|
|
628
|
+
else:
|
|
629
|
+
return int(clean[6:8], 16)
|
|
630
|
+
|
|
631
|
+
@property
|
|
632
|
+
def alpha(self) -> int | None:
|
|
633
|
+
clean = self.clean_value
|
|
634
|
+
if len(clean) == 6:
|
|
635
|
+
return None
|
|
636
|
+
else:
|
|
637
|
+
return int(clean[0:2], 16)
|
|
638
|
+
|
|
639
|
+
def to_hsv(self) -> HSV:
|
|
640
|
+
return self.to_rgb().to_hsv()
|
|
641
|
+
|
|
642
|
+
def to_hsl(self) -> HSL:
|
|
643
|
+
return self.to_rgb().to_hsl()
|
|
644
|
+
|
|
645
|
+
def to_cmyk(self) -> CMYK:
|
|
646
|
+
return self.to_rgb().to_cmyk()
|
|
647
|
+
|
|
648
|
+
def to_hex(self) -> HEX:
|
|
649
|
+
return self.to_rgb().to_hex()
|
|
650
|
+
|
|
651
|
+
def to_assa(self) -> ASSA:
|
|
652
|
+
return self
|
|
653
|
+
|
|
654
|
+
def embed_text(self, text: str) -> str:
|
|
655
|
+
"""Embed text with this color in ASS format"""
|
|
656
|
+
return f"{{\\c{self.value}}}{text}{{\\c}}"
|
|
657
|
+
|
|
658
|
+
@staticmethod
|
|
659
|
+
def is_valid_format(value: str) -> bool:
|
|
660
|
+
clean_value = ASSA._clean_hex(value)
|
|
661
|
+
return len(clean_value) in (6, 8)
|
|
662
|
+
|
|
663
|
+
@classmethod
|
|
664
|
+
def from_alpha(cls, alpha: int) -> ASSA:
|
|
665
|
+
if not 0 <= alpha <= 255:
|
|
666
|
+
raise ColorValueError("Alpha value must be 0-255")
|
|
667
|
+
return cls(f"{alpha:02X}000000")
|
|
668
|
+
|
|
669
|
+
@classmethod
|
|
670
|
+
def from_rgb(cls, rgb: RGB, alpha: int | None = None) -> ASSA:
|
|
671
|
+
if alpha is not None and not 0 <= alpha <= 255:
|
|
672
|
+
raise ColorValueError("Alpha value must be 0-255")
|
|
673
|
+
if alpha is None:
|
|
674
|
+
return cls(f"{rgb.blue:02X}{rgb.green:02X}{rgb.red:02X}")
|
|
675
|
+
else:
|
|
676
|
+
return cls(f"{alpha:02X}{rgb.blue:02X}{rgb.green:02X}{rgb.red:02X}")
|
|
677
|
+
|
|
678
|
+
@classmethod
|
|
679
|
+
def from_rgba(cls, rgba: RGBA) -> ASSA:
|
|
680
|
+
return cls.from_rgb(rgba.to_rgb(), alpha=round((1 - rgba.alpha) * 255))
|
|
681
|
+
|
|
682
|
+
@classmethod
|
|
683
|
+
def from_clean_value(cls, value: str) -> ASSA:
|
|
684
|
+
clean = cls._clean_hex(value)
|
|
685
|
+
if len(clean) not in (6, 8):
|
|
686
|
+
raise ColorValueError(
|
|
687
|
+
"Color value must be either 6 (BBGGRR) or 8 (AABBGGRR) hex digits"
|
|
688
|
+
)
|
|
689
|
+
return cls(clean)
|
|
690
|
+
|
|
691
|
+
@classmethod
|
|
692
|
+
def from_value(cls, value: str) -> ASSA:
|
|
693
|
+
"""Create an ASSA color from a string (with or without &H markers)."""
|
|
694
|
+
clean_value = "".join(c for c in value.lower() if c in "0123456789abcdef")
|
|
695
|
+
return cls(clean_value)
|
|
696
|
+
|
|
697
|
+
@staticmethod
|
|
698
|
+
def _clean_hex(value: str) -> str:
|
|
699
|
+
return "".join(c for c in value.upper() if c in "0123456789ABCDEF")
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
CssColorName = T.Literal[
|
|
703
|
+
"aliceblue",
|
|
704
|
+
"antiquewhite",
|
|
705
|
+
"aqua",
|
|
706
|
+
"aquamarine",
|
|
707
|
+
"azure",
|
|
708
|
+
"beige",
|
|
709
|
+
"bisque",
|
|
710
|
+
"black",
|
|
711
|
+
"blanchedalmond",
|
|
712
|
+
"blue",
|
|
713
|
+
"blueviolet",
|
|
714
|
+
"brown",
|
|
715
|
+
"burlywood",
|
|
716
|
+
"cadetblue",
|
|
717
|
+
"chartreuse",
|
|
718
|
+
"chocolate",
|
|
719
|
+
"coral",
|
|
720
|
+
"cornflowerblue",
|
|
721
|
+
"cornsilk",
|
|
722
|
+
"crimson",
|
|
723
|
+
"cyan",
|
|
724
|
+
"darkblue",
|
|
725
|
+
"darkcyan",
|
|
726
|
+
"darkgoldenrod",
|
|
727
|
+
"darkgray",
|
|
728
|
+
"darkgrey",
|
|
729
|
+
"darkgreen",
|
|
730
|
+
"darkkhaki",
|
|
731
|
+
"darkmagenta",
|
|
732
|
+
"darkolivegreen",
|
|
733
|
+
"darkorange",
|
|
734
|
+
"darkorchid",
|
|
735
|
+
"darkred",
|
|
736
|
+
"darksalmon",
|
|
737
|
+
"darkseagreen",
|
|
738
|
+
"darkslateblue",
|
|
739
|
+
"darkslategray",
|
|
740
|
+
"darkslategrey",
|
|
741
|
+
"darkturquoise",
|
|
742
|
+
"darkviolet",
|
|
743
|
+
"deeppink",
|
|
744
|
+
"deepskyblue",
|
|
745
|
+
"dimgray",
|
|
746
|
+
"dimgrey",
|
|
747
|
+
"dodgerblue",
|
|
748
|
+
"firebrick",
|
|
749
|
+
"floralwhite",
|
|
750
|
+
"forestgreen",
|
|
751
|
+
"fuchsia",
|
|
752
|
+
"gainsboro",
|
|
753
|
+
"ghostwhite",
|
|
754
|
+
"gold",
|
|
755
|
+
"goldenrod",
|
|
756
|
+
"gray",
|
|
757
|
+
"grey",
|
|
758
|
+
"green",
|
|
759
|
+
"greenyellow",
|
|
760
|
+
"honeydew",
|
|
761
|
+
"hotpink",
|
|
762
|
+
"indianred",
|
|
763
|
+
"indigo",
|
|
764
|
+
"ivory",
|
|
765
|
+
"khaki",
|
|
766
|
+
"lavender",
|
|
767
|
+
"lavenderblush",
|
|
768
|
+
"lawngreen",
|
|
769
|
+
"lemonchiffon",
|
|
770
|
+
"lightblue",
|
|
771
|
+
"lightcoral",
|
|
772
|
+
"lightcyan",
|
|
773
|
+
"lightgoldenrodyellow",
|
|
774
|
+
"lightgray",
|
|
775
|
+
"lightgrey",
|
|
776
|
+
"lightgreen",
|
|
777
|
+
"lightpink",
|
|
778
|
+
"lightsalmon",
|
|
779
|
+
"lightseagreen",
|
|
780
|
+
"lightskyblue",
|
|
781
|
+
"lightslategray",
|
|
782
|
+
"lightslategrey",
|
|
783
|
+
"lightsteelblue",
|
|
784
|
+
"lightyellow",
|
|
785
|
+
"lime",
|
|
786
|
+
"limegreen",
|
|
787
|
+
"linen",
|
|
788
|
+
"magenta",
|
|
789
|
+
"maroon",
|
|
790
|
+
"mediumaquamarine",
|
|
791
|
+
"mediumblue",
|
|
792
|
+
"mediumorchid",
|
|
793
|
+
"mediumpurple",
|
|
794
|
+
"mediumseagreen",
|
|
795
|
+
"mediumslateblue",
|
|
796
|
+
"mediumspringgreen",
|
|
797
|
+
"mediumturquoise",
|
|
798
|
+
"mediumvioletred",
|
|
799
|
+
"midnightblue",
|
|
800
|
+
"mintcream",
|
|
801
|
+
"mistyrose",
|
|
802
|
+
"moccasin",
|
|
803
|
+
"navajowhite",
|
|
804
|
+
"navy",
|
|
805
|
+
"oldlace",
|
|
806
|
+
"olive",
|
|
807
|
+
"olivedrab",
|
|
808
|
+
"orange",
|
|
809
|
+
"orangered",
|
|
810
|
+
"orchid",
|
|
811
|
+
"palegoldenrod",
|
|
812
|
+
"palegreen",
|
|
813
|
+
"paleturquoise",
|
|
814
|
+
"palevioletred",
|
|
815
|
+
"papayawhip",
|
|
816
|
+
"peachpuff",
|
|
817
|
+
"peru",
|
|
818
|
+
"pink",
|
|
819
|
+
"plum",
|
|
820
|
+
"powderblue",
|
|
821
|
+
"purple",
|
|
822
|
+
"rebeccapurple",
|
|
823
|
+
"red",
|
|
824
|
+
"rosybrown",
|
|
825
|
+
"royalblue",
|
|
826
|
+
"saddlebrown",
|
|
827
|
+
"salmon",
|
|
828
|
+
"sandybrown",
|
|
829
|
+
"seagreen",
|
|
830
|
+
"seashell",
|
|
831
|
+
"sienna",
|
|
832
|
+
"silver",
|
|
833
|
+
"skyblue",
|
|
834
|
+
"slateblue",
|
|
835
|
+
"slategray",
|
|
836
|
+
"slategrey",
|
|
837
|
+
"snow",
|
|
838
|
+
"springgreen",
|
|
839
|
+
"steelblue",
|
|
840
|
+
"tan",
|
|
841
|
+
"teal",
|
|
842
|
+
"thistle",
|
|
843
|
+
"tomato",
|
|
844
|
+
"turquoise",
|
|
845
|
+
"violet",
|
|
846
|
+
"wheat",
|
|
847
|
+
"white",
|
|
848
|
+
"whitesmoke",
|
|
849
|
+
"yellow",
|
|
850
|
+
"yellowgreen",
|
|
851
|
+
]
|
|
852
|
+
|
|
853
|
+
CSS_COLOR_TO_HEX = {
|
|
854
|
+
"aliceblue": "#F0F8FF",
|
|
855
|
+
"antiquewhite": "#FAEBD7",
|
|
856
|
+
"aqua": "#00FFFF",
|
|
857
|
+
"aquamarine": "#7FFFD4",
|
|
858
|
+
"azure": "#F0FFFF",
|
|
859
|
+
"beige": "#F5F5DC",
|
|
860
|
+
"bisque": "#FFE4C4",
|
|
861
|
+
"black": "#000000",
|
|
862
|
+
"blanchedalmond": "#FFEBCD",
|
|
863
|
+
"blue": "#0000FF",
|
|
864
|
+
"blueviolet": "#8A2BE2",
|
|
865
|
+
"brown": "#A52A2A",
|
|
866
|
+
"burlywood": "#DEB887",
|
|
867
|
+
"cadetblue": "#5F9EA0",
|
|
868
|
+
"chartreuse": "#7FFF00",
|
|
869
|
+
"chocolate": "#D2691E",
|
|
870
|
+
"coral": "#FF7F50",
|
|
871
|
+
"cornflowerblue": "#6495ED",
|
|
872
|
+
"cornsilk": "#FFF8DC",
|
|
873
|
+
"crimson": "#DC143C",
|
|
874
|
+
"cyan": "#00FFFF",
|
|
875
|
+
"darkblue": "#00008B",
|
|
876
|
+
"darkcyan": "#008B8B",
|
|
877
|
+
"darkgoldenrod": "#B8860B",
|
|
878
|
+
"darkgray": "#A9A9A9",
|
|
879
|
+
"darkgrey": "#A9A9A9",
|
|
880
|
+
"darkgreen": "#006400",
|
|
881
|
+
"darkkhaki": "#BDB76B",
|
|
882
|
+
"darkmagenta": "#8B008B",
|
|
883
|
+
"darkolivegreen": "#556B2F",
|
|
884
|
+
"darkorange": "#FF8C00",
|
|
885
|
+
"darkorchid": "#9932CC",
|
|
886
|
+
"darkred": "#8B0000",
|
|
887
|
+
"darksalmon": "#E9967A",
|
|
888
|
+
"darkseagreen": "#8FBC8F",
|
|
889
|
+
"darkslateblue": "#483D8B",
|
|
890
|
+
"darkslategray": "#2F4F4F",
|
|
891
|
+
"darkslategrey": "#2F4F4F",
|
|
892
|
+
"darkturquoise": "#00CED1",
|
|
893
|
+
"darkviolet": "#9400D3",
|
|
894
|
+
"deeppink": "#FF1493",
|
|
895
|
+
"deepskyblue": "#00BFFF",
|
|
896
|
+
"dimgray": "#696969",
|
|
897
|
+
"dimgrey": "#696969",
|
|
898
|
+
"dodgerblue": "#1E90FF",
|
|
899
|
+
"firebrick": "#B22222",
|
|
900
|
+
"floralwhite": "#FFFAF0",
|
|
901
|
+
"forestgreen": "#228B22",
|
|
902
|
+
"fuchsia": "#FF00FF",
|
|
903
|
+
"gainsboro": "#DCDCDC",
|
|
904
|
+
"ghostwhite": "#F8F8FF",
|
|
905
|
+
"gold": "#FFD700",
|
|
906
|
+
"goldenrod": "#DAA520",
|
|
907
|
+
"gray": "#808080",
|
|
908
|
+
"grey": "#808080",
|
|
909
|
+
"green": "#008000",
|
|
910
|
+
"greenyellow": "#ADFF2F",
|
|
911
|
+
"honeydew": "#F0FFF0",
|
|
912
|
+
"hotpink": "#FF69B4",
|
|
913
|
+
"indianred": "#CD5C5C",
|
|
914
|
+
"indigo": "#4B0082",
|
|
915
|
+
"ivory": "#FFFFF0",
|
|
916
|
+
"khaki": "#F0E68C",
|
|
917
|
+
"lavender": "#E6E6FA",
|
|
918
|
+
"lavenderblush": "#FFF0F5",
|
|
919
|
+
"lawngreen": "#7CFC00",
|
|
920
|
+
"lemonchiffon": "#FFFACD",
|
|
921
|
+
"lightblue": "#ADD8E6",
|
|
922
|
+
"lightcoral": "#F08080",
|
|
923
|
+
"lightcyan": "#E0FFFF",
|
|
924
|
+
"lightgoldenrodyellow": "#FAFAD2",
|
|
925
|
+
"lightgray": "#D3D3D3",
|
|
926
|
+
"lightgrey": "#D3D3D3",
|
|
927
|
+
"lightgreen": "#90EE90",
|
|
928
|
+
"lightpink": "#FFB6C1",
|
|
929
|
+
"lightsalmon": "#FFA07A",
|
|
930
|
+
"lightseagreen": "#20B2AA",
|
|
931
|
+
"lightskyblue": "#87CEFA",
|
|
932
|
+
"lightslategray": "#778899",
|
|
933
|
+
"lightslategrey": "#778899",
|
|
934
|
+
"lightsteelblue": "#B0C4DE",
|
|
935
|
+
"lightyellow": "#FFFFE0",
|
|
936
|
+
"lime": "#00FF00",
|
|
937
|
+
"limegreen": "#32CD32",
|
|
938
|
+
"linen": "#FAF0E6",
|
|
939
|
+
"magenta": "#FF00FF",
|
|
940
|
+
"maroon": "#800000",
|
|
941
|
+
"mediumaquamarine": "#66CDAA",
|
|
942
|
+
"mediumblue": "#0000CD",
|
|
943
|
+
"mediumorchid": "#BA55D3",
|
|
944
|
+
"mediumpurple": "#9370DB",
|
|
945
|
+
"mediumseagreen": "#3CB371",
|
|
946
|
+
"mediumslateblue": "#7B68EE",
|
|
947
|
+
"mediumspringgreen": "#00FA9A",
|
|
948
|
+
"mediumturquoise": "#48D1CC",
|
|
949
|
+
"mediumvioletred": "#C71585",
|
|
950
|
+
"midnightblue": "#191970",
|
|
951
|
+
"mintcream": "#F5FFFA",
|
|
952
|
+
"mistyrose": "#FFE4E1",
|
|
953
|
+
"moccasin": "#FFE4B5",
|
|
954
|
+
"navajowhite": "#FFDEAD",
|
|
955
|
+
"navy": "#000080",
|
|
956
|
+
"oldlace": "#FDF5E6",
|
|
957
|
+
"olive": "#808000",
|
|
958
|
+
"olivedrab": "#6B8E23",
|
|
959
|
+
"orange": "#FFA500",
|
|
960
|
+
"orangered": "#FF4500",
|
|
961
|
+
"orchid": "#DA70D6",
|
|
962
|
+
"palegoldenrod": "#EEE8AA",
|
|
963
|
+
"palegreen": "#98FB98",
|
|
964
|
+
"paleturquoise": "#AFEEEE",
|
|
965
|
+
"palevioletred": "#DB7093",
|
|
966
|
+
"papayawhip": "#FFEFD5",
|
|
967
|
+
"peachpuff": "#FFDAB9",
|
|
968
|
+
"peru": "#CD853F",
|
|
969
|
+
"pink": "#FFC0CB",
|
|
970
|
+
"plum": "#DDA0DD",
|
|
971
|
+
"powderblue": "#B0E0E6",
|
|
972
|
+
"purple": "#800080",
|
|
973
|
+
"rebeccapurple": "#663399",
|
|
974
|
+
"red": "#FF0000",
|
|
975
|
+
"rosybrown": "#BC8F8F",
|
|
976
|
+
"royalblue": "#4169E1",
|
|
977
|
+
"saddlebrown": "#8B4513",
|
|
978
|
+
"salmon": "#FA8072",
|
|
979
|
+
"sandybrown": "#F4A460",
|
|
980
|
+
"seagreen": "#2E8B57",
|
|
981
|
+
"seashell": "#FFF5EE",
|
|
982
|
+
"sienna": "#A0522D",
|
|
983
|
+
"silver": "#C0C0C0",
|
|
984
|
+
"skyblue": "#87CEEB",
|
|
985
|
+
"slateblue": "#6A5ACD",
|
|
986
|
+
"slategray": "#708090",
|
|
987
|
+
"slategrey": "#708090",
|
|
988
|
+
"snow": "#FFFAFA",
|
|
989
|
+
"springgreen": "#00FF7F",
|
|
990
|
+
"steelblue": "#4682B4",
|
|
991
|
+
"tan": "#D2B48C",
|
|
992
|
+
"teal": "#008080",
|
|
993
|
+
"thistle": "#D8BFD8",
|
|
994
|
+
"tomato": "#FF6347",
|
|
995
|
+
"turquoise": "#40E0D0",
|
|
996
|
+
"violet": "#EE82EE",
|
|
997
|
+
"wheat": "#F5DEB3",
|
|
998
|
+
"white": "#FFFFFF",
|
|
999
|
+
"whitesmoke": "#F5F5F5",
|
|
1000
|
+
"yellow": "#FFFF00",
|
|
1001
|
+
"yellowgreen": "#9ACD32",
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
class webcolor(Color):
|
|
1006
|
+
__slots__ = ("name", "hex_value")
|
|
1007
|
+
name: str
|
|
1008
|
+
hex_value: HEX
|
|
1009
|
+
COLORS = CSS_COLOR_TO_HEX
|
|
1010
|
+
|
|
1011
|
+
def __init__(self, name: CssColorName):
|
|
1012
|
+
super().__init__()
|
|
1013
|
+
name = name.lower() # type:ignore
|
|
1014
|
+
self.name = name
|
|
1015
|
+
self.validate()
|
|
1016
|
+
self.hex_value = HEX(self.COLORS[name])
|
|
1017
|
+
self._freeze()
|
|
1018
|
+
|
|
1019
|
+
def validate(self) -> None:
|
|
1020
|
+
if self.name not in self.COLORS:
|
|
1021
|
+
raise ColorValueError(f"Unknown web color name '{self.name}'")
|
|
1022
|
+
|
|
1023
|
+
def get_value_keys(self) -> list[str]:
|
|
1024
|
+
return ["name"]
|
|
1025
|
+
|
|
1026
|
+
def __repr__(self) -> str:
|
|
1027
|
+
return f"webcolor('{self.name}')"
|
|
1028
|
+
|
|
1029
|
+
def __eq__(self, other: object) -> bool:
|
|
1030
|
+
if not isinstance(other, webcolor):
|
|
1031
|
+
return NotImplemented
|
|
1032
|
+
return self.name == other.name
|
|
1033
|
+
|
|
1034
|
+
def to_str(self) -> builtins.str:
|
|
1035
|
+
return self.name
|
|
1036
|
+
|
|
1037
|
+
def to_rgb(self) -> RGB:
|
|
1038
|
+
return self.hex_value.to_rgb()
|
|
1039
|
+
|
|
1040
|
+
def to_hsv(self) -> HSV:
|
|
1041
|
+
return self.hex_value.to_hsv()
|
|
1042
|
+
|
|
1043
|
+
def to_hsl(self) -> HSL:
|
|
1044
|
+
return self.hex_value.to_hsl()
|
|
1045
|
+
|
|
1046
|
+
def to_cmyk(self) -> CMYK:
|
|
1047
|
+
return self.hex_value.to_cmyk()
|
|
1048
|
+
|
|
1049
|
+
def to_hex(self) -> HEX:
|
|
1050
|
+
return self.hex_value
|
|
1051
|
+
|
|
1052
|
+
def to_webcolor(self) -> webcolor:
|
|
1053
|
+
return self
|
|
1054
|
+
|
|
1055
|
+
def to_assa(self) -> ASSA:
|
|
1056
|
+
return self.hex_value.to_assa()
|
|
1057
|
+
|
|
1058
|
+
@classmethod
|
|
1059
|
+
def from_hex(cls, value: str) -> webcolor:
|
|
1060
|
+
value = value.lower()
|
|
1061
|
+
for name, color in cls.COLORS.items():
|
|
1062
|
+
if color.lower() == value:
|
|
1063
|
+
return cls(name)
|
|
1064
|
+
raise ColorValueError(f"No web color name matches hex value {value}")
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
def hex_to_rgb(color: str) -> tuple[int, int, int]:
|
|
1068
|
+
c = color.lstrip("#")
|
|
1069
|
+
if len(c) != 6:
|
|
1070
|
+
raise ValueError(color)
|
|
1071
|
+
r = int(c[0:2], 16)
|
|
1072
|
+
g = int(c[2:4], 16)
|
|
1073
|
+
b = int(c[4:6], 16)
|
|
1074
|
+
return (r, g, b)
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
def normalize_color(
|
|
1078
|
+
color: (
|
|
1079
|
+
str
|
|
1080
|
+
| CssColorName
|
|
1081
|
+
| tuple[int, int, int]
|
|
1082
|
+
| tuple[int, int, int, float]
|
|
1083
|
+
| Sequence[int]
|
|
1084
|
+
| Sequence[float]
|
|
1085
|
+
),
|
|
1086
|
+
) -> RGB | RGBA | HEX | ASSA | webcolor:
|
|
1087
|
+
"""Convert various color formats to a Color object."""
|
|
1088
|
+
if isinstance(color, SequenceType) and all(isinstance(x, (int, float)) for x in color):
|
|
1089
|
+
if len(color) == 3:
|
|
1090
|
+
return RGB(*color)
|
|
1091
|
+
elif len(color) == 4:
|
|
1092
|
+
return RGBA(*color)
|
|
1093
|
+
raise ColorValueError(f"Invalid sequence length: {len(color)}")
|
|
1094
|
+
elif isinstance(color, str):
|
|
1095
|
+
if color.startswith("#"):
|
|
1096
|
+
return HEX(color)
|
|
1097
|
+
elif color.startswith("&h") or color.startswith("&H"):
|
|
1098
|
+
return ASSA.from_value(color)
|
|
1099
|
+
else:
|
|
1100
|
+
return webcolor(color)
|
|
1101
|
+
raise ColorValueError(f"Unsupported color format: {color}")
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
try:
|
|
1105
|
+
from pydantic import GetCoreSchemaHandler as _GetCoreSchemaHandler
|
|
1106
|
+
from pydantic import GetJsonSchemaHandler as _GetJsonSchemaHandler
|
|
1107
|
+
from pydantic.json_schema import JsonSchemaValue as _JsonSchemaValue
|
|
1108
|
+
from pydantic_core import core_schema as _core_schema
|
|
1109
|
+
except Exception: # pragma: no cover
|
|
1110
|
+
pass # Pydantic not installed
|
|
1111
|
+
else:
|
|
1112
|
+
|
|
1113
|
+
def parse_rgb(value: str | Mapping[str, T.Any]) -> RGB:
|
|
1114
|
+
if isinstance(value, Mapping):
|
|
1115
|
+
data = dict(value)
|
|
1116
|
+
if {"red", "green", "blue"}.issubset(data):
|
|
1117
|
+
return RGB(data["red"], data["green"], data["blue"])
|
|
1118
|
+
if {"r", "g", "b"}.issubset(data):
|
|
1119
|
+
return RGB(data["r"], data["g"], data["b"])
|
|
1120
|
+
raise ValueError(f"Invalid RGB mapping: {value!r}")
|
|
1121
|
+
|
|
1122
|
+
match = re.fullmatch(
|
|
1123
|
+
r"\s*rgb\(\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*\)\s*", value
|
|
1124
|
+
)
|
|
1125
|
+
if not match:
|
|
1126
|
+
raise ValueError(f"Invalid RGB repr: {value!r}")
|
|
1127
|
+
|
|
1128
|
+
r, g, b = (int(match.group(1)), int(match.group(2)), int(match.group(3)))
|
|
1129
|
+
return RGB(r, g, b)
|
|
1130
|
+
|
|
1131
|
+
def parse_rgba(value: str | Mapping[str, T.Any]) -> RGBA:
|
|
1132
|
+
if isinstance(value, Mapping):
|
|
1133
|
+
data = dict(value)
|
|
1134
|
+
if {"red", "green", "blue", "alpha"}.issubset(data):
|
|
1135
|
+
return RGBA(data["red"], data["green"], data["blue"], data["alpha"])
|
|
1136
|
+
if {"r", "g", "b", "a"}.issubset(data):
|
|
1137
|
+
return RGBA(data["r"], data["g"], data["b"], data["a"])
|
|
1138
|
+
raise ValueError(f"Invalid RGBA mapping: {value!r}")
|
|
1139
|
+
|
|
1140
|
+
match = re.fullmatch(
|
|
1141
|
+
r"\s*rgba\(\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*,\s*([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*\)\s*",
|
|
1142
|
+
value,
|
|
1143
|
+
)
|
|
1144
|
+
if not match:
|
|
1145
|
+
raise ValueError(f"Invalid RGBA repr: {value!r}")
|
|
1146
|
+
|
|
1147
|
+
r, g, b = int(match.group(1)), int(match.group(2)), int(match.group(3))
|
|
1148
|
+
a = float(match.group(4))
|
|
1149
|
+
return RGBA(r, g, b, a)
|
|
1150
|
+
|
|
1151
|
+
def parse_hex(value: str | Mapping[str, T.Any]) -> HEX:
|
|
1152
|
+
if isinstance(value, Mapping):
|
|
1153
|
+
data = dict(value)
|
|
1154
|
+
if "value" in data:
|
|
1155
|
+
return HEX(data["value"])
|
|
1156
|
+
if "hex" in data:
|
|
1157
|
+
return HEX(data["hex"])
|
|
1158
|
+
raise ValueError(f"Invalid HEX mapping: {value!r}")
|
|
1159
|
+
|
|
1160
|
+
match = re.fullmatch(r"\s*hex\(\s*(#?[0-9A-Fa-f]{6})\s*\)\s*", value)
|
|
1161
|
+
if not match:
|
|
1162
|
+
raise ValueError(f"Invalid HEX repr: {value!r}")
|
|
1163
|
+
v = match.group(1)
|
|
1164
|
+
if not v.startswith("#"):
|
|
1165
|
+
v = f"#{v}"
|
|
1166
|
+
|
|
1167
|
+
return HEX(v)
|
|
1168
|
+
|
|
1169
|
+
def parse_hsv(value: str | Mapping[str, T.Any]) -> HSV:
|
|
1170
|
+
if isinstance(value, Mapping):
|
|
1171
|
+
data = dict(value)
|
|
1172
|
+
if {"hue", "saturation", "value"}.issubset(data):
|
|
1173
|
+
return HSV(data["hue"], data["saturation"], data["value"])
|
|
1174
|
+
if {"h", "s", "v"}.issubset(data):
|
|
1175
|
+
return HSV(data["h"], data["s"], data["v"])
|
|
1176
|
+
raise ValueError(f"Invalid HSV mapping: {value!r}")
|
|
1177
|
+
|
|
1178
|
+
match = re.fullmatch(
|
|
1179
|
+
r"\s*hsv\(\s*([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*,\s*([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*,\s*([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*\)\s*",
|
|
1180
|
+
value,
|
|
1181
|
+
)
|
|
1182
|
+
if not match:
|
|
1183
|
+
raise ValueError(f"Invalid HSV repr: {value!r}")
|
|
1184
|
+
|
|
1185
|
+
h, s, v = float(match.group(1)), float(match.group(2)), float(match.group(3))
|
|
1186
|
+
return HSV(h, s, v)
|
|
1187
|
+
|
|
1188
|
+
def parse_hsl(value: str | Mapping[str, T.Any]) -> HSL:
|
|
1189
|
+
if isinstance(value, Mapping):
|
|
1190
|
+
data = dict(value)
|
|
1191
|
+
if {"hue", "saturation", "lightness"}.issubset(data):
|
|
1192
|
+
return HSL(data["hue"], data["saturation"], data["lightness"])
|
|
1193
|
+
if {"h", "s", "l"}.issubset(data):
|
|
1194
|
+
return HSL(data["h"], data["s"], data["l"])
|
|
1195
|
+
raise ValueError(f"Invalid HSL mapping: {value!r}")
|
|
1196
|
+
|
|
1197
|
+
match = re.fullmatch(
|
|
1198
|
+
r"\s*hsl\(\s*([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*,\s*([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*,\s*([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*\)\s*",
|
|
1199
|
+
value,
|
|
1200
|
+
)
|
|
1201
|
+
if not match:
|
|
1202
|
+
raise ValueError(f"Invalid HSL repr: {value!r}")
|
|
1203
|
+
|
|
1204
|
+
h, s, l = float(match.group(1)), float(match.group(2)), float(match.group(3))
|
|
1205
|
+
return HSL(h, s, l)
|
|
1206
|
+
|
|
1207
|
+
def parse_cmyk(value: str | Mapping[str, T.Any]) -> CMYK:
|
|
1208
|
+
if isinstance(value, Mapping):
|
|
1209
|
+
data = dict(value)
|
|
1210
|
+
if {"cyan", "magenta", "yellow", "key"}.issubset(data):
|
|
1211
|
+
return CMYK(data["cyan"], data["magenta"], data["yellow"], data["key"])
|
|
1212
|
+
if {"c", "m", "y", "k"}.issubset(data):
|
|
1213
|
+
return CMYK(data["c"], data["m"], data["y"], data["k"])
|
|
1214
|
+
raise ValueError(f"Invalid CMYK mapping: {value!r}")
|
|
1215
|
+
|
|
1216
|
+
match = re.fullmatch(
|
|
1217
|
+
r"\s*cmyk\(\s*([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*,\s*([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*,\s*([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*,\s*([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*\)\s*",
|
|
1218
|
+
value,
|
|
1219
|
+
)
|
|
1220
|
+
if not match:
|
|
1221
|
+
raise ValueError(f"Invalid CMYK repr: {value!r}")
|
|
1222
|
+
|
|
1223
|
+
c, m, y, k = (
|
|
1224
|
+
float(match.group(1)),
|
|
1225
|
+
float(match.group(2)),
|
|
1226
|
+
float(match.group(3)),
|
|
1227
|
+
float(match.group(4)),
|
|
1228
|
+
)
|
|
1229
|
+
return CMYK(c, m, y, k)
|
|
1230
|
+
|
|
1231
|
+
def parse_assa(value: str | Mapping[str, T.Any]) -> ASSA:
|
|
1232
|
+
if isinstance(value, Mapping):
|
|
1233
|
+
data = dict(value)
|
|
1234
|
+
if "value" in data:
|
|
1235
|
+
return ASSA(data["value"])
|
|
1236
|
+
if "clean_value" in data:
|
|
1237
|
+
return ASSA(data["clean_value"])
|
|
1238
|
+
raise ValueError(f"Invalid ASSA mapping: {value!r}")
|
|
1239
|
+
|
|
1240
|
+
match = re.fullmatch(r"\s*assa\(\s*([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})\s*\)\s*", value)
|
|
1241
|
+
if not match:
|
|
1242
|
+
raise ValueError(f"Invalid ASSA repr: {value!r}")
|
|
1243
|
+
|
|
1244
|
+
return ASSA(match.group(1))
|
|
1245
|
+
|
|
1246
|
+
def parse_webcolor(value: str | Mapping[str, T.Any]) -> webcolor:
|
|
1247
|
+
if isinstance(value, Mapping):
|
|
1248
|
+
data = dict(value)
|
|
1249
|
+
if "name" in data:
|
|
1250
|
+
return webcolor(data["name"])
|
|
1251
|
+
raise ValueError(f"Invalid webcolor mapping: {value!r}")
|
|
1252
|
+
|
|
1253
|
+
match = re.fullmatch(r"\s*webcolor\(\s*(['\"])\s*([A-Za-z]+)\s*\1\s*\)\s*", value)
|
|
1254
|
+
if not match:
|
|
1255
|
+
raise ValueError(f"Invalid webcolor repr: {value!r}")
|
|
1256
|
+
|
|
1257
|
+
return webcolor(match.group(2))
|
|
1258
|
+
|
|
1259
|
+
def _string_schema(
|
|
1260
|
+
*,
|
|
1261
|
+
title: str,
|
|
1262
|
+
description: str | None = None,
|
|
1263
|
+
pattern: str | None = None,
|
|
1264
|
+
examples: list[str] | None = None,
|
|
1265
|
+
) -> _JsonSchemaValue:
|
|
1266
|
+
schema: dict[str, T.Any] = {"type": "string", "title": title}
|
|
1267
|
+
if description:
|
|
1268
|
+
schema["description"] = description
|
|
1269
|
+
if pattern:
|
|
1270
|
+
schema["pattern"] = pattern
|
|
1271
|
+
if examples:
|
|
1272
|
+
schema["examples"] = examples
|
|
1273
|
+
return schema
|
|
1274
|
+
|
|
1275
|
+
def _object_schema(
|
|
1276
|
+
*,
|
|
1277
|
+
title: str,
|
|
1278
|
+
description: str | None,
|
|
1279
|
+
properties: dict[str, T.Any],
|
|
1280
|
+
required: list[str],
|
|
1281
|
+
) -> _JsonSchemaValue:
|
|
1282
|
+
return {
|
|
1283
|
+
"type": "object",
|
|
1284
|
+
"title": title,
|
|
1285
|
+
"description": description,
|
|
1286
|
+
"properties": properties,
|
|
1287
|
+
"required": required,
|
|
1288
|
+
"additionalProperties": False,
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
def _register_color(
|
|
1292
|
+
cls: type[Color],
|
|
1293
|
+
parser: T.Callable[[str | Mapping[str, T.Any]], Color],
|
|
1294
|
+
*,
|
|
1295
|
+
title: str,
|
|
1296
|
+
string_description: str,
|
|
1297
|
+
string_pattern: str,
|
|
1298
|
+
string_examples: list[str],
|
|
1299
|
+
object_description: str,
|
|
1300
|
+
object_properties: dict[str, T.Any],
|
|
1301
|
+
object_required: list[str],
|
|
1302
|
+
) -> None:
|
|
1303
|
+
string_schema = _string_schema(
|
|
1304
|
+
title=title,
|
|
1305
|
+
description=string_description,
|
|
1306
|
+
pattern=string_pattern,
|
|
1307
|
+
examples=string_examples,
|
|
1308
|
+
)
|
|
1309
|
+
object_schema = _object_schema(
|
|
1310
|
+
title=title,
|
|
1311
|
+
description=object_description,
|
|
1312
|
+
properties=object_properties,
|
|
1313
|
+
required=object_required,
|
|
1314
|
+
)
|
|
1315
|
+
|
|
1316
|
+
def _validator(v: T.Any) -> Color:
|
|
1317
|
+
if isinstance(v, cls):
|
|
1318
|
+
return v
|
|
1319
|
+
if isinstance(v, str):
|
|
1320
|
+
return parser(v)
|
|
1321
|
+
if isinstance(v, Mapping):
|
|
1322
|
+
return parser(v)
|
|
1323
|
+
raise ValueError(
|
|
1324
|
+
f"Expected {cls.__name__} instance, repr string, or mapping, got {type(v).__name__}"
|
|
1325
|
+
)
|
|
1326
|
+
|
|
1327
|
+
def _serializer(v: T.Any, info: _core_schema.SerializationInfo) -> T.Any:
|
|
1328
|
+
if info.mode == "json":
|
|
1329
|
+
return repr(v)
|
|
1330
|
+
return v
|
|
1331
|
+
|
|
1332
|
+
core_schema = _core_schema.no_info_plain_validator_function(
|
|
1333
|
+
_validator,
|
|
1334
|
+
serialization=_core_schema.plain_serializer_function_ser_schema(
|
|
1335
|
+
_serializer, info_arg=True
|
|
1336
|
+
),
|
|
1337
|
+
)
|
|
1338
|
+
|
|
1339
|
+
def _core_schema_factory(
|
|
1340
|
+
_cls: type[Color], _source: T.Any, _handler: _GetCoreSchemaHandler
|
|
1341
|
+
) -> _core_schema.CoreSchema:
|
|
1342
|
+
return core_schema
|
|
1343
|
+
|
|
1344
|
+
def _json_schema_factory(
|
|
1345
|
+
_cls: type[Color],
|
|
1346
|
+
_core_schema_obj: _core_schema.CoreSchema,
|
|
1347
|
+
handler: _GetJsonSchemaHandler,
|
|
1348
|
+
) -> _JsonSchemaValue:
|
|
1349
|
+
return {
|
|
1350
|
+
"title": title,
|
|
1351
|
+
"anyOf": [
|
|
1352
|
+
handler.resolve_ref_schema(string_schema),
|
|
1353
|
+
handler.resolve_ref_schema(object_schema),
|
|
1354
|
+
],
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
cls.__get_pydantic_core_schema__ = classmethod(_core_schema_factory) # type: ignore[attr-defined]
|
|
1358
|
+
cls.__get_pydantic_json_schema__ = classmethod(_json_schema_factory) # type: ignore[attr-defined]
|
|
1359
|
+
|
|
1360
|
+
_register_color(
|
|
1361
|
+
RGB,
|
|
1362
|
+
parse_rgb,
|
|
1363
|
+
title="RGB",
|
|
1364
|
+
string_description="String representation rgb(r, g, b) with 0-255 integer channels",
|
|
1365
|
+
string_pattern=r"^rgb\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\)$",
|
|
1366
|
+
string_examples=["rgb(255, 0, 0)", "rgb(0, 0, 0)"],
|
|
1367
|
+
object_description="Object with integer channels 0-255",
|
|
1368
|
+
object_properties={
|
|
1369
|
+
"red": {"type": "integer", "minimum": 0, "maximum": 255},
|
|
1370
|
+
"green": {"type": "integer", "minimum": 0, "maximum": 255},
|
|
1371
|
+
"blue": {"type": "integer", "minimum": 0, "maximum": 255},
|
|
1372
|
+
},
|
|
1373
|
+
object_required=["red", "green", "blue"],
|
|
1374
|
+
)
|
|
1375
|
+
|
|
1376
|
+
_register_color(
|
|
1377
|
+
RGBA,
|
|
1378
|
+
parse_rgba,
|
|
1379
|
+
title="RGBA",
|
|
1380
|
+
string_description="String representation rgba(r, g, b, a) with 0-255 integer channels and alpha 0-1",
|
|
1381
|
+
string_pattern=r"^rgba\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*(?:0|1|0?\.\d+)\s*\)$",
|
|
1382
|
+
string_examples=["rgba(255, 0, 0, 1)", "rgba(0, 128, 255, 0.5)"],
|
|
1383
|
+
object_description="Object with integer channels 0-255 and alpha 0-1",
|
|
1384
|
+
object_properties={
|
|
1385
|
+
"red": {"type": "integer", "minimum": 0, "maximum": 255},
|
|
1386
|
+
"green": {"type": "integer", "minimum": 0, "maximum": 255},
|
|
1387
|
+
"blue": {"type": "integer", "minimum": 0, "maximum": 255},
|
|
1388
|
+
"alpha": {"type": "number", "minimum": 0, "maximum": 1},
|
|
1389
|
+
},
|
|
1390
|
+
object_required=["red", "green", "blue", "alpha"],
|
|
1391
|
+
)
|
|
1392
|
+
|
|
1393
|
+
_register_color(
|
|
1394
|
+
HEX,
|
|
1395
|
+
parse_hex,
|
|
1396
|
+
title="HEX",
|
|
1397
|
+
string_description="String representation hex(#RRGGBB)",
|
|
1398
|
+
string_pattern=r"^hex\(\s*#?[0-9A-Fa-f]{6}\s*\)$",
|
|
1399
|
+
string_examples=["hex(#ffffff)", "hex(#0a1b2c)"],
|
|
1400
|
+
object_description="Object with hex string value #RRGGBB",
|
|
1401
|
+
object_properties={
|
|
1402
|
+
"value": {
|
|
1403
|
+
"type": "string",
|
|
1404
|
+
"pattern": r"^#?[0-9A-Fa-f]{6}$",
|
|
1405
|
+
"examples": ["#ffffff", "0A1B2C"],
|
|
1406
|
+
}
|
|
1407
|
+
},
|
|
1408
|
+
object_required=["value"],
|
|
1409
|
+
)
|
|
1410
|
+
|
|
1411
|
+
_register_color(
|
|
1412
|
+
HSV,
|
|
1413
|
+
parse_hsv,
|
|
1414
|
+
title="HSV",
|
|
1415
|
+
string_description="String representation hsv(h, s, v)",
|
|
1416
|
+
string_pattern=r"^hsv\(\s*[+-]?(?:\d+(?:\.\d+)?|\.\d+)\s*,\s*[+-]?(?:\d+(?:\.\d+)?|\.\d+)\s*,\s*[+-]?(?:\d+(?:\.\d+)?|\.\d+)\s*\)$",
|
|
1417
|
+
string_examples=["hsv(120.0, 100.0, 100.0)"],
|
|
1418
|
+
object_description="Object with hue 0-360, saturation/value 0-100",
|
|
1419
|
+
object_properties={
|
|
1420
|
+
"hue": {"type": "number", "minimum": 0, "maximum": 360},
|
|
1421
|
+
"saturation": {"type": "number", "minimum": 0, "maximum": 100},
|
|
1422
|
+
"value": {"type": "number", "minimum": 0, "maximum": 100},
|
|
1423
|
+
},
|
|
1424
|
+
object_required=["hue", "saturation", "value"],
|
|
1425
|
+
)
|
|
1426
|
+
|
|
1427
|
+
_register_color(
|
|
1428
|
+
HSL,
|
|
1429
|
+
parse_hsl,
|
|
1430
|
+
title="HSL",
|
|
1431
|
+
string_description="String representation hsl(h, s, l)",
|
|
1432
|
+
string_pattern=r"^hsl\(\s*[+-]?(?:\d+(?:\.\d+)?|\.\d+)\s*,\s*[+-]?(?:\d+(?:\.\d+)?|\.\d+)\s*,\s*[+-]?(?:\d+(?:\.\d+)?|\.\d+)\s*\)$",
|
|
1433
|
+
string_examples=["hsl(240.0, 100.0, 50.0)"],
|
|
1434
|
+
object_description="Object with hue 0-360, saturation/lightness 0-100",
|
|
1435
|
+
object_properties={
|
|
1436
|
+
"hue": {"type": "number", "minimum": 0, "maximum": 360},
|
|
1437
|
+
"saturation": {"type": "number", "minimum": 0, "maximum": 100},
|
|
1438
|
+
"lightness": {"type": "number", "minimum": 0, "maximum": 100},
|
|
1439
|
+
},
|
|
1440
|
+
object_required=["hue", "saturation", "lightness"],
|
|
1441
|
+
)
|
|
1442
|
+
|
|
1443
|
+
_register_color(
|
|
1444
|
+
CMYK,
|
|
1445
|
+
parse_cmyk,
|
|
1446
|
+
title="CMYK",
|
|
1447
|
+
string_description="String representation cmyk(c, m, y, k)",
|
|
1448
|
+
string_pattern=r"^cmyk\(\s*[+-]?(?:\d+(?:\.\d+)?|\.\d+)\s*,\s*[+-]?(?:\d+(?:\.\d+)?|\.\d+)\s*,\s*[+-]?(?:\d+(?:\.\d+)?|\.\d+)\s*,\s*[+-]?(?:\d+(?:\.\d+)?|\.\d+)\s*\)$",
|
|
1449
|
+
string_examples=["cmyk(0, 100, 100, 0)", "cmyk(10.5, 0, 25, 5)"],
|
|
1450
|
+
object_description="Object with CMYK percentages 0-100",
|
|
1451
|
+
object_properties={
|
|
1452
|
+
"cyan": {"type": "number", "minimum": 0, "maximum": 100},
|
|
1453
|
+
"magenta": {"type": "number", "minimum": 0, "maximum": 100},
|
|
1454
|
+
"yellow": {"type": "number", "minimum": 0, "maximum": 100},
|
|
1455
|
+
"key": {"type": "number", "minimum": 0, "maximum": 100},
|
|
1456
|
+
},
|
|
1457
|
+
object_required=["cyan", "magenta", "yellow", "key"],
|
|
1458
|
+
)
|
|
1459
|
+
|
|
1460
|
+
_register_color(
|
|
1461
|
+
ASSA,
|
|
1462
|
+
parse_assa,
|
|
1463
|
+
title="ASSA",
|
|
1464
|
+
string_description="String representation assa(BBGGRR) or assa(AABBGGRR)",
|
|
1465
|
+
string_pattern=r"^assa\(\s*[0-9A-Fa-f]{6}(?:[0-9A-Fa-f]{2})?\s*\)$",
|
|
1466
|
+
string_examples=["assa(00ff00)", "assa(ff00ff00)"],
|
|
1467
|
+
object_description="Object with ASSA hex value (BBGGRR or AABBGGRR)",
|
|
1468
|
+
object_properties={
|
|
1469
|
+
"value": {
|
|
1470
|
+
"type": "string",
|
|
1471
|
+
"pattern": r"^[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$",
|
|
1472
|
+
"examples": ["00ff00", "ff00ff00"],
|
|
1473
|
+
}
|
|
1474
|
+
},
|
|
1475
|
+
object_required=["value"],
|
|
1476
|
+
)
|
|
1477
|
+
|
|
1478
|
+
_register_color(
|
|
1479
|
+
webcolor,
|
|
1480
|
+
parse_webcolor,
|
|
1481
|
+
title="Web Color",
|
|
1482
|
+
string_description="Named CSS color via webcolor('name')",
|
|
1483
|
+
string_pattern=r"^webcolor\(\s*['\"]?[A-Za-z]+['\"]?\s*\)$",
|
|
1484
|
+
string_examples=["webcolor('red')", 'webcolor("aliceblue")'],
|
|
1485
|
+
object_description="Object with CSS color name",
|
|
1486
|
+
object_properties={
|
|
1487
|
+
"name": {"type": "string", "enum": list(CSS_COLOR_TO_HEX.keys())},
|
|
1488
|
+
},
|
|
1489
|
+
object_required=["name"],
|
|
1490
|
+
)
|
|
1491
|
+
__all__ = [
|
|
1492
|
+
"Color",
|
|
1493
|
+
"RGB",
|
|
1494
|
+
"RGBA",
|
|
1495
|
+
"HEX",
|
|
1496
|
+
"HSV",
|
|
1497
|
+
"HSL",
|
|
1498
|
+
"CMYK",
|
|
1499
|
+
"ASSA",
|
|
1500
|
+
"webcolor",
|
|
1501
|
+
"CSS_COLOR_TO_HEX",
|
|
1502
|
+
"CssColorName",
|
|
1503
|
+
"ColorValueError",
|
|
1504
|
+
"normalize_color",
|
|
1505
|
+
]
|