aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Touhey <thomas@touhey.fr>2019-05-15 11:56:30 +0200
committerThomas Touhey <thomas@touhey.fr>2019-05-15 11:56:30 +0200
commit191aa991c32b9e0124bf8256c1ae7c130b74b248 (patch)
tree16b2e4ca0021a50dbf38ca469a8aa8ea0be00c5c
parent71d92d00bb625a2eb9bf767e81b0b35cd56b9113 (diff)
Added a few other systems and RGB color profiles.
-rw-r--r--docs/colors.rst7
-rwxr-xr-xtests/test_text.py1
-rwxr-xr-xthcolor/_color.py424
-rwxr-xr-xthcolor/_ref.py38
-rwxr-xr-xthcolor/_sys.py64
-rwxr-xr-xthcolor/builtin/_css.py50
-rwxr-xr-xthcolor/builtin/_default.py25
7 files changed, 544 insertions, 65 deletions
diff --git a/docs/colors.rst b/docs/colors.rst
index e5d3b65..7bff8d1 100644
--- a/docs/colors.rst
+++ b/docs/colors.rst
@@ -5,7 +5,12 @@ Colors can have one of the following types:
.. autoclass:: thcolor.Color.Type
+RGB colors can have one of the following profiles:
+
+.. autoclass:: thcolor.Color.Profile
+
Colors are represented in ``thcolor`` as instances of the following class:
.. autoclass:: thcolor.Color
- :members: type, rgb, rgba, hls, hlsa, hwb, hwba, cmyk, cmyka, css
+ :members: type, rgb, rgba, hls, hlsa, hwb, hwba, cmyk, cmyka, lab, laba,
+ lch, lcha, xyz, xyza, css
diff --git a/tests/test_text.py b/tests/test_text.py
index 45a39fe..069ef9c 100755
--- a/tests/test_text.py
+++ b/tests/test_text.py
@@ -33,6 +33,7 @@ def _deg(value):
('B20 50% 32%', (137, 128, 173, 1.00)),
('ncol(B20 / 50% 32%)', (137, 128, 173, 1.00)),
('cmyk(0% 37% 0.13 .78)', ( 56, 35, 49, 1.00)),
+ ('lab(50 50 0)', (193, 78, 121, 1.00)),
))
def test_rgba(test_input, expected):
assert Color.from_text(test_input).rgba() == expected
diff --git a/thcolor/_color.py b/thcolor/_color.py
index 828ab21..1bc8f6e 100755
--- a/thcolor/_color.py
+++ b/thcolor/_color.py
@@ -23,6 +23,8 @@ from ._angle import Angle as _Angle
from ._sys import (hls_to_rgb as _hls_to_rgb, rgb_to_hls as _rgb_to_hls,
rgb_to_hwb as _rgb_to_hwb, hwb_to_rgb as _hwb_to_rgb,
rgb_to_cmyk as _rgb_to_cmyk, cmyk_to_rgb as _cmyk_to_rgb,
+ lab_to_rgb as _lab_to_rgb, rgb_to_lab as _rgb_to_lab,
+ lab_to_lch as _lab_to_lch, lch_to_lab as _lch_to_lab,
netscape_color as _netscape_color)
from ._exc import (\
ColorExpressionDecodingError as _ColorExpressionDecodingError,
@@ -68,6 +70,15 @@ def _get_color_pattern():
# Color initialization varargs utilities.
# ---
+def _color_profile(name, value):
+ try:
+ value = Color.Profile.from_value(value)
+ except (TypeError, ValueError):
+ raise ValueError(f"{name} is not a valid color profile " \
+ f"(got {repr(value)}).")
+
+ return value
+
def _byte(name, value):
try:
assert value == int(value)
@@ -78,9 +89,36 @@ def _byte(name, value):
return value
+def _signed(name, value):
+ try:
+ value = float(value)
+ except (AssertionError, TypeError, ValueError):
+ raise ValueError(f"{name} should be a signed number")
+
+ return round(value, 4)
+
+def _unsigned(name, value):
+ try:
+ value = float(value)
+ assert value >= 0
+ except (AssertionError, TypeError, ValueError):
+ raise ValueError(f"{name} should be a positive number")
+
+ return round(value, 4)
+
+def _unrestricted_percentage(name, value):
+ try:
+ value = float(value)
+ assert 0.0 <= value
+ except (AssertionError, TypeError, ValueError):
+ raise ValueError(f"{name} should be a proportion starting from 0") \
+ from None
+
+ return round(value, 4)
+
def _percentage(name, value):
try:
- assert value == float(value)
+ value = float(value)
assert 0.0 <= value <= 1.0
except (AssertionError, TypeError, ValueError):
raise ValueError(f"{name} should be a proportion between 0 " \
@@ -116,6 +154,18 @@ class Color:
An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be
appended to the base components.
+ .. function:: Color(Color.Type.RGB, profile, red, green, blue, """ \
+ """alpha = 1.0)
+
+ Create a color using its profile, red, green and blue component.
+ Each is expressed as a byte value, from 0 (dark) to 255 (light).
+
+ The profile is one of the :class:`thcolor.Color.Profile`
+ constants, and represents the RGB profile.
+
+ An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be
+ appended to the base components.
+
.. function:: Color(Color.Type.HSL, hue, saturation, lightness, """ \
"""alpha = 1.0)
@@ -143,17 +193,34 @@ class Color:
components, which are all values going from 0.0 to 1.0.
An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be
- appended to the base components. """
+ appended to the base components.
- # Properties to work with:
- #
- # `_type`: the type as one of the `Color.Type` constants.
- # `_alpha`: alpha value.
- # `_r`, `_g`, `_b`: rgb components, as bytes.
- # `_hue`: hue for HSL and HWB notations.
- # `_sat`, `_lgt`: saturation and light for HSL.
- # `_wht`, `_blk`: whiteness and blackness for HWB.
- # `_cy`, `_ma`, `_ye`, `_bl`: CMYK components.
+ .. function:: Color(Color.Type.LAB, lightness, a, b, alpha = 1.0)
+
+ Create a color using its CIE Lightness (similar to the lightness
+ in the HSL representation) and the A and B axises in the Lab
+ colorspace, represented by signed numbers.
+
+ An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be
+ appended to the base components.
+
+ .. function:: Color(Color.Type.LCH, lightness, chroma, hue, """ \
+ """alpha = 1.0)
+
+ Create a color using its CIE Lightness (similar to the lightness
+ in the HSL representation), its chroma (as a positive number
+ theoretically unbounded) and its hue.
+
+ An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be
+ appended to the base components.
+
+ .. function:: Color(Color.Type.XYZ, x, y, z, alpha = 1.0)
+
+ Create a color using its CIE XYZ components (as numbers between
+ 0 and 1).
+
+ An alpha value going from 0.0 (invisible) to 1.0 (opaque) can be
+ appended to the base components. """
class Type(_Enum):
""" Class representing the type of a color, or how it is expressed.
@@ -183,14 +250,123 @@ class Color:
A color expressed through its CMYK components: cyan, magenta,
yellow and black.
+ .. data:: LAB
+
+ A color expressed through its lightness and Lab colorspace
+ coordinates (A and B).
+
+ .. data:: LCH
+
+ A color expressed through its lightness, chroma and hue.
+
+ .. data:: XYZ
+
+ A color expressed through its CIE XYZ coordinates.
+
An alpha component can be added to every single one of these
types, so it is not included in the type names. """
- INVALID = 0
- RGB = 1
- HSL = 2
- HWB = 3
- CMYK = 4
+ # Values start at 65600 in order not to infer with "normal" values
+ # going up to 65535, just in case.
+
+ INVALID = 65600
+
+ RGB = 65601
+ HSL = 65602
+ HWB = 65603
+ CMYK = 65604
+ LAB = 65605
+ LCH = 65606
+ XYZ = 65607
+
+ class Profile(_Enum):
+ """ Class representing the profile of a color, or how it is expressed.
+ The following profiles are available:
+
+ .. data:: SRGB
+
+ A basic sRGB profile.
+
+ .. data:: IMAGE_P3
+
+ See `the description in CSS Module Level 4
+ <https://drafts.csswg.org/css-color/#valdef-color-image-p3>`_.
+
+ .. data:: A98RGB
+
+ The AdobeĀ® RGB (1998) color profile. See `the description
+ in CSS Module Level 4
+ <https://drafts.csswg.org/css-color/#valdef-color-a98rgb>`_.
+
+ .. data:: PROPHOTORGB
+
+ The ProPHOTO RGB color profile. See `the description in
+ CSS Module Level 4
+ <https://drafts.csswg.org/css-color/#valdef-color-prophotorgb>`_.
+
+ .. data:: REC2020
+
+ The REC.2020 colorspace. See `the description in CSS Module
+ Level 4
+ <https://drafts.csswg.org/css-color/#valdef-color-rec2020>`_. """
+
+ SRGB = 65700
+ IMAGE_P3 = 65701
+ A98RGB = 65702
+ PROPHOTORGB = 65703
+ REC2020 = 65704
+
+ def from_value(value):
+ _profiles = {
+ 'srgb': 'SRGB',
+ 'imagep3': 'IMAGE_P3',
+ 'a98rgb': 'A98RGB',
+ 'prophotorgb': 'PROPHOTORGB',
+ 'rec2020': 'REC2020'}
+
+ if type(value) == str:
+ newval = ''.join(c for c in value.casefold() if c in \
+ '0123456789abcdefghijklmnopqrstuvwxyz')
+ try:
+ value = _profiles[newval]
+ except:
+ pass
+
+ return getattr(Color.Profile, value)
+
+ return Color.Profile(value)
+
+ # Properties to work with:
+ #
+ # `_type`: the type as one of the `Color.Type` constants.
+ # `_alpha`: alpha value.
+ #
+ # RGB colors:
+ # `_r`, `_g`, `_b`: rgb components, as bytes.
+ # `_profile`: the color profile.
+ #
+ # HSL colors:
+ # `_hue`: hue.
+ # `_sat`, `_lgt`: saturation and light for HSL.
+ #
+ # HWB colors:
+ # `_hue`: hue.
+ # `_wht`, `_blk`: whiteness and blackness for HWB.
+ #
+ # CMYK colors:
+ # `_cy`, `_ma`, `_ye`, `_bl`: CMYK components.
+ #
+ # LAB colors:
+ # `_lgt`: lightness.
+ # `_a`, `_b`: coordinates in the Lab colorspace.
+ #
+ # LCH colors:
+ # `_lgt`: lightness.
+ # `_hue`: the hue.
+ # `_chr`: the chroma.
+ #
+ # XYZ colors:
+ # `_x`, `_y`, `_z`: XYZ components.
def __init__(self, *args, **kwargs):
self._type = Color.Type.INVALID
@@ -199,7 +375,9 @@ class Color:
def __repr__(self):
args = (('type', f'{self.__class__.__name__}.{str(self._type)}'),)
if self._type == Color.Type.RGB:
- args += (('red', repr(self._r)), ('green', repr(self._g)),
+ args += (('profile',
+ f'{self.__class__.__name__}.{str(self._profile)}'),
+ ('red', repr(self._r)), ('green', repr(self._g)),
('blue', repr(self._b)))
elif self._type == Color.Type.HSL:
args += (('hue', repr(self._hue)), ('saturation', repr(self._sat)),
@@ -210,6 +388,15 @@ class Color:
elif self._type == Color.Type.CMYK:
args += (('cyan', repr(self._cy)), ('magenta', repr(self._ma)),
('yellow', repr(self._ye)), ('black', repr(self._bl)))
+ elif self._type == Color.Type.LAB:
+ args += (('lightness', repr(self._lgt)), ('a', repr(self._a)),
+ ('b', repr(self._b)))
+ elif self._type == Color.Type.LCH:
+ args += (('lightness', repr(self._lgt)),
+ ('chroma', repr(self._chr)), ('hue', repr(self._hue)))
+ elif self._type == Color.Type.XYZ:
+ args += (('x', repr(self._x)), ('y', repr(self._y)),
+ ('z', repr(self._z)))
args += (('alpha', self._alpha),)
@@ -226,6 +413,14 @@ class Color:
return self.hsla() == other.hsla()
elif other.type == Color.Type.HWB:
return self.hwba() == other.hwba()
+ elif other.type == Color.Type.CMYK:
+ return self.cmyka() == other.cmyka()
+ elif other.type == Color.Type.LAB:
+ return self.laba() == other.laba()
+ elif other.type == Color.Type.LCH:
+ return self.lcha() == other.lcha()
+ elif other.type == Color.Type.XYZ:
+ return self.xyza() == other.xyza()
return self.rgba() == other.rgba()
@@ -240,14 +435,31 @@ class Color:
args = list(args)
def _decode_varargs(*keys):
+ class _UNDEFINED_CLASS:
+ pass
+ _UNDEFINED = _UNDEFINED_CLASS()
+
+ def _get_value(value_array):
+ if not value_array:
+ value = _UNDEFINED
+ else:
+ value = value_array[0] if len(value_array) == 1 \
+ else value_array
+
+ return value
+
# Check for each key.
results = ()
+ left_args = len(keys)
+
for names, convert_func, *value in keys:
+ value = _get_value(value)
+
for name in names:
if name in kwargs:
- if args:
+ if value is _UNDEFINED and args:
raise TypeError(f"{self.__class__.__name__}() " \
f"got multiple values for argument {name}")
@@ -255,10 +467,10 @@ class Color:
break
else:
name = names[0]
- if args:
+ if value is not _UNDEFINED and len(args) < left_args:
+ raw_result = value
+ elif args:
raw_result = args.pop(0)
- elif value:
- raw_result = value[0] if len(value) == 1 else value
else:
raise TypeError(f"{self.__class__.__name__}() " \
"missing a required positional argument: " \
@@ -267,6 +479,8 @@ class Color:
result = convert_func(name, raw_result)
results += (result,)
+ left_args -= 1
+
# Check for keyword arguments for which keys are not in the set.
if kwargs:
@@ -307,31 +521,55 @@ class Color:
# Initialize the properties.
if type == Color.Type.RGB:
- self._r, self._g, self._b, self._alpha = _decode_varargs(\
- (('r', 'red'), _byte),
- (('g', 'green'), _byte),
- (('b', 'blue'), _byte),
- (('a', 'alpha'), _percentage, 1.0))
+ self._profile, self._r, self._g, self._b, self._alpha = \
+ _decode_varargs(\
+ (('profile', 'p'), _color_profile, 'srgb'),
+ (('red', 'r'), _byte),
+ (('green', 'g'), _byte),
+ (('blue', 'b'), _byte),
+ (('alpha', 'a'), _percentage, 1.0))
+
+ if self._profile != Color.Profile.SRGB:
+ raise NotImplementedError("rgb profile " \
+ f"{repr(self._profile)} isn't managed yet.")
elif type == Color.Type.HSL:
self._hue, self._sat, self._lgt, self._alpha = _decode_varargs(\
- (('h', 'hue'), _hue),
- (('s', 'sat', 'saturation'), _percentage),
- (('l', 'lig', 'light', 'lightness'), _percentage),
- (('a', 'alpha'), _percentage, 1.0))
+ (('hue', 'h'), _hue),
+ (('saturation', 'sat', 's'), _percentage),
+ (('lightness', 'light', 'lig', 'l'), _percentage),
+ (('alpha', 'a'), _percentage, 1.0))
elif type == Color.Type.HWB:
self._hue, self._wht, self._blk, self._alpha = _decode_varargs(\
- (('h', 'hue'), _hue),
- (('w', 'white', 'whiteness'), _percentage),
- (('b', 'black', 'blackness'), _percentage),
- (('a', 'alpha'), _percentage, 1.0))
+ (('hue', 'h'), _hue),
+ (('whiteness', 'white', 'w'), _percentage),
+ (('blackness', 'black', 'b'), _percentage),
+ (('alpha', 'a'), _percentage, 1.0))
elif type == Color.Type.CMYK:
self._cy, self._ma, self._ye, self._bl, self._alpha = \
_decode_varargs(\
- (('c', 'cyan'), _percentage),
- (('m', 'magenta'), _percentage),
- (('y', 'yellow'), _percentage),
- (('b', 'black'), _percentage),
- (('a', 'alpha'), _percentage, 1.0))
+ (('cyan', 'c'), _percentage),
+ (('magenta', 'm'), _percentage),
+ (('yellow', 'y'), _percentage),
+ (('black', 'b'), _percentage),
+ (('alpha', 'a'), _percentage, 1.0))
+ elif type == Color.Type.LAB:
+ self._lgt, self._a, self._b, self._alpha = _decode_varargs(\
+ (('lightness', 'light', 'lig', 'l'), _unrestricted_percentage),
+ (('a',), _signed),
+ (('b',), _signed),
+ (('alpha', 'a'), _percentage, 1.0))
+ elif type == Color.Type.LCH:
+ self._lgt, self._chr, self._hue, self._alpha = _decode_varargs(\
+ (('lightness', 'light', 'lig', 'l'), _percentage),
+ (('chroma', 'chr', 'c'), _unsigned),
+ (('hue', 'h'), _hue),
+ (('alpha', 'a'), _percentage, 1.0))
+ elif type == Color.Type.XYZ:
+ self._x, self._y, self._z, self._alpha = _decode_varargs(\
+ (('x',), _percentage),
+ (('y',), _percentage),
+ (('z',), _percentage),
+ (('alpha', 'a'), _percentage, 1.0))
else:
raise ValueError(f"invalid color type: {type}")
@@ -373,6 +611,10 @@ class Color:
return _hwb_to_rgb(self._hue, self._wht, self._blk)
elif self._type == Color.Type.CMYK:
return _cmyk_to_rgb(self._cy, self._ma, self._ye, self._bl)
+ elif self._type == Color.Type.LAB:
+ return _lab_to_rgb(self._lgt, self._a, self._b)
+ elif self._type == Color.Type.LCH:
+ return _lch_to_rgb(self._lgt, self._chr, self._hue)
raise ValueError(f"color type {self._type} doesn't translate to rgb")
@@ -439,6 +681,65 @@ class Color:
return _rgb_to_cmyk(*rgb)
+ def lab(self):
+ """ Get the LAB (lightness, Lab colorspace coordinates) components
+ of the color. For example:
+
+ >>> Color.from_text("lab(50 50 0)").lab()
+ ... (0.5, 50, 0)
+
+ If the color is not represented as LAB internally, it will be
+ converted. """
+
+ if self._type == Color.Type.LAB:
+ return (self._lgt, self._a, self._b)
+ elif self._type == Color.Type.LCH:
+ return _lch_to_lab(self._lgt, self._chr, self._hue)
+
+ try:
+ rgb = self.rgb()
+ except ValueError:
+ raise ValueError(f"color type {self._type} doesn't translate " \
+ "to lab") from None
+
+ return _rgb_to_lab(*rgb)
+
+ def lch(self):
+ """ Get the LCH (lightness, chroma, hue) components of the color.
+ For example:
+
+ >>> Color.from_text("lch(50 230 0deg)").lch()
+ ... (0.5, 230, Angle(Angle.Type.DEG, 0))
+
+ If the color is not represented as LCH internally, it will be
+ converted. """
+
+ if self._type == Color.Type.LCH:
+ return (self._lgt, self._chr, self._hue)
+
+ try:
+ lab = self.lab()
+ except ValueError:
+ raise ValueError(f"color type {self._type} doesn't translate " \
+ "to lch") from None
+
+ return _lab_to_lch(*lab)
+
+ def xyz(self):
+ """ Get the XYZ components of the color.
+ For example:
+
+ >>> Color.from_text("xyz(0.2, 0.4, 0.5)")
+ ... (0.2, 0.4, 0.5)
+
+ If the color is not represented as XYZ internally, it will be
+ converted. """
+
+ if self._type == Color.Type.XYZ:
+ return (self._x, self._y, self._z)
+
+ raise NotImplementedError # TODO
+
def rgba(self):
""" Get the sRGB (red, green, blue) and alpha components of the color.
For example:
@@ -513,6 +814,51 @@ class Color:
return (c, m, y, k, a)
+ def laba(self):
+ """ Get the LAB (lightness, Lab colorspace coordinates) and alpha
+ components of the color. For example:
+
+ >>> Color.from_text("lab(50 50 0 / 0.75)").laba()
+ ... (0.5, 50, 0, 0.75)
+
+ If the color is not represented as LAB internally, it will be
+ converted naively. """
+
+ l, a, b = self.lab()
+ alpha = self._alpha
+
+ return (l, a, b, alpha)
+
+ def lcha(self):
+ """ Get the LCH (lightness, chroma, hue) and alpha components
+ of the color. For example:
+
+ >>> Color.from_text("lch(50 230 0deg)").lcha()
+ ... (0.5, 230, Angle(Angle.Type.DEG, 0), 1.0)
+
+ If the color is not represented as LCH internally, it will be
+ converted. """
+
+ l, c, h = self.lch()
+ alpha = self._alpha
+
+ return (l, c, h, alpha)
+
+ def xyza(self):
+ """ Get the XYZ and alpha components of the color.
+ For example:
+
+ >>> Color.from_text("xyz(0.2, 0.4, 0.5 / 65%)")
+ ... (0.2, 0.4, 0.5, 0.65)
+
+ If the color is not represented as XYZ internally, it will be
+ converted. """
+
+ x, y, z = self.xyz()
+ alpha = self._alpha
+
+ return (x, y, z)
+
def css(self):
""" Get the CSS color descriptions, with older CSS specifications
compatibility, as a list of strings.
diff --git a/thcolor/_ref.py b/thcolor/_ref.py
index 96a9c51..1229634 100755
--- a/thcolor/_ref.py
+++ b/thcolor/_ref.py
@@ -115,6 +115,10 @@ class Reference:
def __str__(self):
return self._strvalue
+ @property
+ def value(self):
+ return self._value
+
def to_byte(self):
""" Make a byte (from 0 to 255) out of the number. """
@@ -129,9 +133,20 @@ class Reference:
return value
- def to_factor(self):
+ def to_factor(self, min = 0.0, max = 1.0):
""" Make a factor (usually from 0.0 to 1.0) out of the number. """
+ if (min is not None and self._value < min) \
+ or (max is not None and self._value > max):
+ if max is None:
+ msg = f"above {min}"
+ elif min is None:
+ msg = f"under {max}"
+ else:
+ msg = f"between {min} and {max}"
+
+ raise ValueError(f"expected a value {msg}, got {self._value}")
+
try:
assert 0.0 <= self._value <= 1.0
except:
@@ -164,16 +179,23 @@ class Reference:
def __repr__(self):
return f"{self._value} %"
- def to_factor(self):
+ def to_factor(self, min = 0.0, max = 1.0):
""" Make a factor (usually from 0.0 to 1.0) out of the number. """
- try:
- assert 0 <= self._value <= 100
- except:
- raise ValueError("expected a value between 0.0 and 1.0, got " \
- f"{self._value}")
+ value = self._value / 100
+ if (min is not None and value < min) \
+ or (max is not None and value > max):
+ if max is None:
+ msg = f"above {min * 100}%"
+ elif min is None:
+ msg = f"under {max * 100}%"
+ else:
+ msg = f"between {min * 100}% and {max * 100}%"
+
+ raise ValueError(f"expected a percentage {msg}, " \
+ f"got {self._value}%")
- return self._value / 100
+ return value
def to_byte(self):
""" Make a byte (from 0 to 255) out of the number. """
diff --git a/thcolor/_sys.py b/thcolor/_sys.py
index d0e0227..23c1ec0 100755
--- a/thcolor/_sys.py
+++ b/thcolor/_sys.py
@@ -5,12 +5,14 @@
#******************************************************************************
""" Conversions between color systems. """
-from math import ceil as _ceil
+from math import (ceil as _ceil, atan2 as _atan2, sqrt as _sqrt, cos as _cos,
+ sin as _sin, pow as _pow)
from ._angle import Angle as _Angle
__all__ = ["hls_to_rgb", "rgb_to_hls", "rgb_to_hwb", "hwb_to_rgb",
- "cmyk_to_rgb", "rgb_to_cmyk", "netscape_color"]
+ "cmyk_to_rgb", "rgb_to_cmyk", "lab_to_rgb", "rgb_to_lab",
+ "lch_to_lab", "lab_to_lch", "netscape_color"]
# ---
# Color systems conversion utilities.
@@ -21,6 +23,18 @@ def _rgb(r, g, b):
def _hls(hue, s, l):
return _Angle(_Angle.Type.DEG, round(hue, 2)), round(l, 2), round(s, 2)
+def _rgb_to_lrgb(r, g, b):
+ def _linearize(val):
+ # Undo gamma encoding.
+
+ val /= 255
+ if val < 0.04045:
+ return val / 12.92
+
+ return _pow((val + 0.055) / 1.055, 2.4)
+
+ return (*map(_linearize, (r, g, b)))
+
def hls_to_rgb(hue, l, s):
""" Convert HLS to RGB. """
@@ -62,6 +76,43 @@ def hwb_to_rgb(hue, w, bl):
w, bl = map(lambda x: x / (w + bl), (w, bl))
return _rgb(*map(lambda x: x * (1 - w - bl) + w, (r, g, b)))
+def cmyk_to_rgb(c, m, y, k):
+ """ Convert CMYK to RGB. """
+
+ r = 1 - min(1, c * (1 - k) + k)
+ g = 1 - min(1, m * (1 - k) + k)
+ b = 1 - min(1, y * (1 - k) + k)
+
+ return _rgb(r, g, b)
+
+def lab_to_rgb(l, a, b):
+ """ Convert LAB to RGB. """
+
+ # TODO
+ raise NotImplementedError
+
+def rgb_to_lab(r, g, b):
+ """ Convert RGB to LAB. """
+
+ # TODO
+ raise NotImplementedError
+
+def lab_to_lch(l, a, b):
+ """ Convert RGB to LAB. """
+
+ h = _Angle(_Angle.Type.RAD, _atan2(b, a))
+ c = _sqrt(a * a + b * b)
+
+ return (l, c, h)
+
+def lch_to_lab(l, c, h):
+ """ Convert LCH to LAB. """
+
+ a = c * _cos(h.radians)
+ b = c * _sin(h.radians)
+
+ return (l, a, b)
+
def rgb_to_hls(r, g, b):
""" Convert RGB to HLS. """
@@ -117,15 +168,6 @@ def rgb_to_hwb(r, g, b):
return _Angle(_Angle.Type.DEG, hue), w, b
-def cmyk_to_rgb(c, m, y, k):
- """ Convert CMYK to RGB. """
-
- r = 1 - min(1, c * (1 - k) + k)
- g = 1 - min(1, m * (1 - k) + k)
- b = 1 - min(1, y * (1 - k) + k)
-
- return _rgb(r, g, b)
-
def rgb_to_cmyk(r, g, b):
""" Convert RGB to CMYK. """
diff --git a/thcolor/builtin/_css.py b/thcolor/builtin/_css.py
index 12bb1aa..f60cdaa 100755
--- a/thcolor/builtin/_css.py
+++ b/thcolor/builtin/_css.py
@@ -411,13 +411,51 @@ class CSS4Reference(CSS3Reference):
return _Reference.color(_Color(_Color.Type.RGB, g, g, g, alpha))
def lab(self, l: number, a: number, b: number,
- alpha: percentage = number(1.0)):
- # TODO: lab
- raise NotImplementedError
+ alpha: number | percentage = number(1.0)):
+
+ try:
+ l = l.value
+ if l < 0:
+ l = 0
+ l /= 100
+ except ValueError as e:
+ raise _InvalidArgumentValueError(0, str(e))
+
+ a = a.value
+ b = b.value
+
+ try:
+ alpha = alpha.to_factor()
+ except ValueError as e:
+ raise _InvalidArgumentValueError(3, str(e))
+
+ return _Reference.color(_Color(_Color.Type.LAB, l, a, b, alpha))
def lch(self, l: number, c: number, h: number | angle,
- alpha: percentage = number(1.0)):
- # TODO: lch
- raise NotImplementedError
+ alpha: number | percentage = number(1.0)):
+
+ try:
+ l = l.value
+ if l < 0:
+ l = 0
+ l /= 100
+ except ValueError as e:
+ raise _InvalidArgumentValueError(0, str(e))
+
+ c = c.value
+ if c < 0:
+ c = 0
+
+ try:
+ h = h.to_hue()
+ except ValueError as e:
+ raise _InvalidArgumentValueError(2, str(e))
+
+ try:
+ alpha = alpha.to_factor()
+ except ValueError as e:
+ raise _InvalidArgumentValueError(3, str(e))
+
+ return _Reference.color(_Color(_Color.Type.LCH, l, c, h, alpha))
# End of file.
diff --git a/thcolor/builtin/_default.py b/thcolor/builtin/_default.py
index bbbf8d1..31b1434 100755
--- a/thcolor/builtin/_default.py
+++ b/thcolor/builtin/_default.py
@@ -137,6 +137,31 @@ class DefaultReference(_CSS4Reference):
return _Reference.color(_Color(_Color.Type.CMYK, c, m, y, k, alpha))
+ def xyz(self, x: number | percentage, y: number | percentage,
+ z: number | percentage, alpha: number | percentage = number(1.0)):
+
+ try:
+ x = x.to_factor()
+ except ValueError as e:
+ raise _InvalidArgumentValueError(0, str(e))
+
+ try:
+ y = y.to_factor()
+ except ValueError as e:
+ raise _InvalidArgumentValueError(1, str(e))
+
+ try:
+ z = z.to_factor()
+ except ValueError as e:
+ raise _InvalidArgumentValueError(2, str(e))
+
+ try:
+ alpha = alpha.to_factor()
+ except ValueError as e:
+ raise _InvalidArgumentValueError(3, str(e))
+
+ return _Reference.color(_Color(_Color.Type.XYZ, x, y, z, alpha))
+
# ---
# Get the RGB components of a color.
# ---