
| Current Path : /var/www/bavspeed/venvxxx/lib64/python3.12/site-packages/tinycss2/ |
Linux ift1.ift-informatik.de 5.4.0-216-generic #236-Ubuntu SMP Fri Apr 11 19:53:21 UTC 2025 x86_64 |
| Current File : /var/www/bavspeed/venvxxx/lib64/python3.12/site-packages/tinycss2/color4.py |
from colorsys import hls_to_rgb
from math import cos, degrees, radians, sin
from .color3 import _BASIC_COLOR_KEYWORDS, _EXTENDED_COLOR_KEYWORDS, _HASH_REGEXPS
from .parser import parse_one_component_value
D50 = (0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585)
D65 = (0.3127 / 0.3290, 1, (1 - 0.3127 - 0.3290) / 0.3290)
_FUNCTION_SPACES = {
'srgb', 'srgb-linear',
'display-p3', 'a98-rgb', 'prophoto-rgb', 'rec2020',
'xyz', 'xyz-d50', 'xyz-d65'
}
COLOR_SPACES = _FUNCTION_SPACES | {'hsl', 'hwb', 'lab', 'lch', 'oklab', 'oklch'}
class Color:
"""A specified color in a defined color space.
The color space is one of ``COLOR_SPACES``.
Coordinates are floats with undefined ranges, but alpha channel is clipped
to [0, 1]. Coordinates can also be set to ``None`` when undefined.
"""
def __init__(self, space, coordinates, alpha):
assert space in COLOR_SPACES, f"{space} is not a supported color space"
self.space = space
self.coordinates = tuple(
None if coordinate is None else float(coordinate)
for coordinate in coordinates)
self.alpha = max(0., min(1., float(alpha)))
def __repr__(self):
coordinates = ' '.join(str(coordinate) for coordinate in self.coordinates)
return f'color({self.space} {coordinates} / {self.alpha})'
def __iter__(self):
yield from self.coordinates
yield self.alpha
def __getitem__(self, key):
return (*self.coordinates, self.alpha)[key]
def __hash__(self):
return hash(str(self))
def __eq__(self, other):
if isinstance(other, str):
return False
elif isinstance(other, tuple):
return tuple(self) == other
elif isinstance(other, Color):
return self.space == other.space and self.coordinates == other.coordinates
return super().__eq__(other)
def to(self, space):
"""Return new instance with coordinates transformed to given ``space``.
The destination color space is one of ``SPACES``.
``None`` coordinates are always transformed into ``0`` values.
Here are the supported combinations:
- from hsl and hwb to srgb;
- from lab and lch to xyz-d50;
- from oklab and oklch to xyz-d65;
- from xyz-d50, xyz-d65, lch, oklab and oklch to lab.
"""
coordinates = tuple(coordinate or 0 for coordinate in self.coordinates)
if space == 'xyz':
space = 'xyz-d65'
if space == self.space:
return Color(space, coordinates, self.alpha)
elif space == 'srgb':
if self.space == 'hsl':
rgb = hls_to_rgb(
coordinates[0] / 360,
coordinates[2] / 100,
coordinates[1] / 100,
)
return Color(space, rgb, self.alpha)
elif self.space == 'hwb':
white, black = coordinates[1:]
if white + black >= 100:
rgb = (white / (white + black),) * 3
else:
rgb = (
((channel * (100 - white - black)) + white) / 100
for channel in hls_to_rgb(coordinates[0] / 360, 0.5, 1))
return Color(space, rgb, self.alpha)
elif space == 'xyz-d50':
if self.space == 'lab':
xyz = _lab_to_xyz(*coordinates, D50)
return Color(space, xyz, self.alpha)
elif self.space == 'lch':
a = coordinates[1] * cos(radians(coordinates[2]))
b = coordinates[1] * sin(radians(coordinates[2]))
xyz = _lab_to_xyz(coordinates[0], a, b, D50)
return Color(space, xyz, self.alpha)
elif space == 'xyz-d65':
if self.space == 'oklab':
xyz = _oklab_to_xyz(*coordinates)
return Color(space, xyz, self.alpha)
elif self.space == 'oklch':
a = coordinates[1] * cos(radians(coordinates[2]))
b = coordinates[1] * sin(radians(coordinates[2]))
xyz = _oklab_to_xyz(coordinates[0], a, b)
return Color(space, xyz, self.alpha)
elif space == 'lab':
if self.space == 'xyz-d50':
lab = _xyz_to_lab(*coordinates, D50)
return Color(space, lab, self.alpha)
elif self.space == 'xyz-d65':
lab = _xyz_to_lab(*coordinates, D65)
return Color(space, lab, self.alpha)
elif self.space == 'lch':
a = coordinates[1] * cos(radians(coordinates[2]))
b = coordinates[1] * sin(radians(coordinates[2]))
return Color(space, (coordinates[0], a, b), self.alpha)
elif self.space == 'oklab':
xyz = _oklab_to_xyz(*coordinates)
lab = _xyz_to_lab(*xyz, D65)
return Color(space, lab, self.alpha)
elif self.space == 'oklch':
a = coordinates[1] * cos(radians(coordinates[2]))
b = coordinates[1] * sin(radians(coordinates[2]))
xyz = _oklab_to_xyz(coordinates[0], a, b)
lab = _xyz_to_lab(*xyz, D65)
return Color(space, lab, self.alpha)
raise NotImplementedError
def parse_color(input):
"""Parse a color value as defined in CSS Color Level 4.
https://www.w3.org/TR/css-color-4/
:type input: :obj:`str` or :term:`iterable`
:param input: A string or an iterable of :term:`component values`.
:returns:
* :obj:`None` if the input is not a valid color value.
(No exception is raised.)
* The string ``'currentcolor'`` for the ``currentcolor`` keyword
* A :class:`Color` object for every other values, including keywords.
"""
if isinstance(input, str):
token = parse_one_component_value(input, skip_comments=True)
else:
token = input
if token.type == 'ident':
if token.lower_value == 'currentcolor':
return 'currentcolor'
elif token.lower_value == 'transparent':
return Color('srgb', (0, 0, 0), 0)
elif color := _COLOR_KEYWORDS.get(token.lower_value):
rgb = tuple(channel / 255 for channel in color)
return Color('srgb', rgb, 1)
elif token.type == 'hash':
for multiplier, regexp in _HASH_REGEXPS:
match = regexp(token.value)
if match:
channels = [
int(group * multiplier, 16) / 255
for group in match.groups()]
alpha = channels.pop() if len(channels) == 4 else 1
return Color('srgb', channels, alpha)
elif token.type == 'function':
tokens = [
token for token in token.arguments
if token.type not in ('whitespace', 'comment')]
name = token.lower_name
if name == 'color':
space, *tokens = tokens
length = len(tokens)
if length in (5, 7) and all(token == ',' for token in tokens[1::2]):
old_syntax = True
tokens = tokens[::2]
elif length == 3:
old_syntax = False
elif length == 5 and tokens[3] == '/':
tokens.pop(3)
old_syntax = False
else:
return
args, alpha = tokens[:3], _parse_alpha(tokens[3:])
if alpha is None:
return
if name in ('rgb', 'rgba'):
return _parse_rgb(args, alpha)
elif name in ('hsl', 'hsla'):
return _parse_hsl(args, alpha)
elif name == 'hwb':
return _parse_hwb(args, alpha)
elif name == 'lab' and not old_syntax:
return _parse_lab(args, alpha)
elif name == 'lch' and not old_syntax:
return _parse_lch(args, alpha)
elif name == 'oklab' and not old_syntax:
return _parse_oklab(args, alpha)
elif name == 'oklch' and not old_syntax:
return _parse_oklch(args, alpha)
elif name == 'color' and not old_syntax:
return _parse_color(space, args, alpha)
def _parse_alpha(args):
"""Parse a list of one alpha value.
If args is a list of a single INTEGER, NUMBER or PERCENTAGE token,
return its value clipped to the 0..1 range. Otherwise, return None.
"""
if len(args) == 0:
return 1.
elif len(args) == 1:
if args[0].type == 'number':
return min(1, max(0, args[0].value))
elif args[0].type == 'percentage':
return min(1, max(0, args[0].value / 100))
def _parse_rgb(args, alpha):
"""Parse a list of RGB channels.
If args is a list of 3 NUMBER tokens or 3 PERCENTAGE tokens, return
sRGB :class:`Color`. Otherwise, return None.
Input R, G, B ranges are [0, 255], output are [0, 1].
"""
if _types(args) not in (set(), {'number'}, {'percentage'}):
return
coordinates = [
arg.value / 255 if arg.type == 'number' else
arg.value / 100 if arg.type == 'percentage' else None
for arg in args]
return Color('srgb', coordinates, alpha)
def _parse_hsl(args, alpha):
"""Parse a list of HSL channels.
If args is a list of 1 NUMBER or ANGLE token and 2 PERCENTAGE tokens,
return HSL :class:`Color`. Otherwise, return None.
H range is [0, 360). S, L ranges are [0, 100].
"""
if _types(args[1:]) not in (set(), {'number'}, {'percentage'}):
return
if (hue := _parse_hue(args[0])) is None:
return
coordinates = [
None if args[0].type == 'ident' else hue,
None if args[1].type == 'ident' else args[1].value,
None if args[2].type == 'ident' else args[2].value,
]
return Color('hsl', coordinates, alpha)
def _parse_hwb(args, alpha):
"""Parse a list of HWB channels.
If args is a list of 1 NUMBER or ANGLE token and 2 NUMBER or PERCENTAGE
tokens, return HWB :class:`Color`. Otherwise, return None.
H range is [0, 360). W, B ranges are [0, 100].
"""
if not _types(args[1:]) <= {'number', 'percentage'}:
return
if (hue := _parse_hue(args[0])) is None:
return
coordinates = [
None if args[0].type == 'ident' else hue,
None if args[1].type == 'ident' else args[1].value,
None if args[2].type == 'ident' else args[2].value,
]
return Color('hwb', coordinates, alpha)
def _parse_lab(args, alpha):
"""Parse a list of CIE Lab channels.
If args is a list of 3 NUMBER or PERCENTAGE tokens, return Lab
:class:`Color`. Otherwise, return None.
L range is [0, 100]. a, b ranges are [-125, 125].
"""
if not _types(args) <= {'number', 'percentage'}:
return
coordinates = [
None if args[0].type == 'ident' else args[0].value,
None if args[1].type == 'ident' else (
args[1].value * (1 if args[1].type == 'number' else 1.25)),
None if args[2].type == 'ident' else (
args[2].value * (1 if args[2].type == 'number' else 1.25)),
]
return Color('lab', coordinates, alpha)
def _parse_lch(args, alpha):
"""Parse a list of CIE LCH channels.
If args is a list of 2 NUMBER or PERCENTAGE tokens and 1 NUMBER or ANGLE
token, return LCH :class:`Color`. Otherwise, return None.
L range is [0, 100]. C range is [0, 150]. H ranges is [0, 360).
"""
if not _types(args[:2]) <= {'number', 'percentage'}:
return
if (hue := _parse_hue(args[2])) is None:
return
coordinates = [
None if args[0].type == 'ident' else args[0].value,
None if args[1].type == 'ident' else (
args[1].value * (1 if args[1].type == 'number' else 1.5)),
None if args[0].type == 'ident' else hue,
]
return Color('lch', coordinates, alpha)
def _parse_oklab(args, alpha):
"""Parse a list of Oklab channels.
If args is a list of 3 NUMBER or PERCENTAGE tokens, return Oklab
:class:`Color`. Otherwise, return None.
L range is [0, 100]. a, b ranges are [-0.4, 0.4].
"""
if not _types(args) <= {'number', 'percentage'}:
return
coordinates = [
None if args[0].type == 'ident' else (
args[0].value * (1 if args[0].type == 'number' else 0.01)),
None if args[1].type == 'ident' else (
args[1].value * (1 if args[1].type == 'number' else 0.004)),
None if args[2].type == 'ident' else (
args[2].value * (1 if args[2].type == 'number' else 0.004)),
]
return Color('oklab', coordinates, alpha)
def _parse_oklch(args, alpha):
"""Parse a list of Oklch channels.
If args is a list of 2 NUMBER or PERCENTAGE tokens and 1 NUMBER or ANGLE
token, return Oklch :class:`Color`. Otherwise, return None.
L range is [0, 1]. C range is [0, 0.4]. H range is [0, 360).
"""
if not _types(args[:2]) <= {'number', 'percentage'}:
return
if (hue := _parse_hue(args[2])) is None:
return
coordinates = [
None if args[0].type == 'ident' else (
args[0].value * (1 if args[0].type == 'number' else 0.01)),
None if args[1].type == 'ident' else (
args[1].value * (1 if args[1].type == 'number' else 0.004)),
None if args[0].type == 'ident' else hue,
]
return Color('oklch', coordinates, alpha)
def _parse_color(space, args, alpha):
"""Parse a color space name list of coordinates.
Ranges are [0, 1].
"""
if not _types(args) <= {'number', 'percentage'}:
return
if space.type != 'ident' or (space := space.lower_value) not in _FUNCTION_SPACES:
return
if space == 'xyz':
space = 'xyz-d65'
coordinates = [
arg.value if arg.type == 'number' else
arg.value / 100 if arg.type == 'percentage' else None
for arg in args]
return Color(space, coordinates, alpha)
def _parse_hue(token):
"""Parse hue token.
Range is [0, 360). ``none`` value is 0.
"""
if token.type == 'number':
return token.value % 360
elif token.type == 'dimension':
if token.unit == 'deg':
return token.value % 360
elif token.unit == 'grad':
return token.value / 400 * 360 % 360
elif token.unit == 'rad':
return degrees(token.value) % 360
elif token.unit == 'turn':
return token.value * 360 % 360
elif token.type == 'ident' and token.lower_value == 'none':
return 0
def _types(tokens):
"""Get a set of token types, ignoring ``none`` values."""
types = set()
for token in tokens:
if token.type == 'ident' and token.lower_value == 'none':
continue
types.add(token.type)
return types
# Code adapted from https://www.w3.org/TR/css-color-4/#color-conversion-code.
_κ = 24389 / 27
_ε = 216 / 24389
_LMS_TO_XYZ = (
(1.2268798733741557, -0.5578149965554813, 0.28139105017721583),
(-0.04057576262431372, 1.1122868293970594, -0.07171106666151701),
(-0.07637294974672142, -0.4214933239627914, 1.5869240244272418),
)
_OKLAB_TO_LMS = (
(0.99999999845051981432, 0.39633779217376785678, 0.21580375806075880339),
(1.0000000088817607767, -0.1055613423236563494, -0.063854174771705903402),
(1.0000000546724109177, -0.089484182094965759684, -1.2914855378640917399),
)
def _xyz_to_lab(X, Y, Z, d):
x = X / d[0]
y = Y / d[1]
z = Z / d[2]
f0 = x ** (1 / 3) if x > _ε else (_κ * x + 16) / 116
f1 = y ** (1 / 3) if y > _ε else (_κ * y + 16) / 116
f2 = z ** (1 / 3) if z > _ε else (_κ * z + 16) / 116
L = (116 * f1) - 16
a = 500 * (f0 - f1)
b = 200 * (f1 - f2)
return L, a, b
def _lab_to_xyz(L, a, b, d):
f1 = (L + 16) / 116
f0 = a / 500 + f1
f2 = f1 - b / 200
x = (f0 ** 3 if f0 ** 3 > _ε else (116 * f0 - 16) / _κ)
y = (((L + 16) / 116) ** 3 if L > _κ * _ε else L / _κ)
z = (f2 ** 3 if f2 ** 3 > _ε else (116 * f2 - 16) / _κ)
X = x * d[0]
Y = y * d[1]
Z = z * d[2]
return X, Y, Z
def _oklab_to_xyz(L, a, b):
lab = (L, a, b)
lms = [sum(_OKLAB_TO_LMS[i][j] * lab[j] for j in range(3)) for i in range(3)]
X, Y, Z = [sum(_LMS_TO_XYZ[i][j] * lms[j]**3 for j in range(3)) for i in range(3)]
return X, Y, Z
# (r, g, b) in 0..255
_EXTENDED_COLOR_KEYWORDS = _EXTENDED_COLOR_KEYWORDS.copy()
_EXTENDED_COLOR_KEYWORDS.append(('rebeccapurple', (102, 51, 153)))
_COLOR_KEYWORDS = dict(_BASIC_COLOR_KEYWORDS + _EXTENDED_COLOR_KEYWORDS)