diff options
author | Thomas Touhey <thomas@touhey.fr> | 2019-05-15 11:56:30 +0200 |
---|---|---|
committer | Thomas Touhey <thomas@touhey.fr> | 2019-05-15 11:56:30 +0200 |
commit | 191aa991c32b9e0124bf8256c1ae7c130b74b248 (patch) | |
tree | 16b2e4ca0021a50dbf38ca469a8aa8ea0be00c5c | |
parent | 71d92d00bb625a2eb9bf767e81b0b35cd56b9113 (diff) |
Added a few other systems and RGB color profiles.
-rw-r--r-- | docs/colors.rst | 7 | ||||
-rwxr-xr-x | tests/test_text.py | 1 | ||||
-rwxr-xr-x | thcolor/_color.py | 424 | ||||
-rwxr-xr-x | thcolor/_ref.py | 38 | ||||
-rwxr-xr-x | thcolor/_sys.py | 64 | ||||
-rwxr-xr-x | thcolor/builtin/_css.py | 50 | ||||
-rwxr-xr-x | thcolor/builtin/_default.py | 25 |
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. # --- |