Source code for colorzero.color

# vim: set et sw=4 sts=4 fileencoding=utf-8:
#
# The colorzero color library
#
# Copyright (c) 2016-2021 Dave Jones <dave@waveform.org.uk>
#
# SPDX-License-Identifier: BSD-3-Clause

"Defines the main :class:`Color` class of the package."

import re

from . import conversions as cv, types, attr, deltae, tables, easings

# Lots of the methods below use single character parameter names (r for red, y
# for luma, etc.); this is is normal and in keeping with most of the referenced
# sources
# pylint: disable=invalid-name


[docs]class Color(types.RGB): """ The Color class is a tuple which represents a color as linear red, green, and blue components. The class has a flexible constructor which allows you to create an instance from any built-in color system. There are also explicit constructors for every known system that can convert (directly or indirectly) to linear RGB. For example, an instance of :class:`Color` can be constructed in any of the following ways:: >>> Color('#f00') <Color html='#ff0000' rgb=(1, 0, 0)> >>> Color('green') <Color html='#008000' rgb=(0.0, 0.501961, 0.0)> >>> Color(0, 0, 1) <Color html='#0000ff' rgb=(0, 0, 1)> >>> Color(h=0, s=1, v=0.5) <Color html='#800000' rgb=(0.5, 0, 0)> >>> Color(y=0.4, u=-0.05, v=0.615) <Color html='#ff104c' rgb=(1, 0.0626644, 0.298394)> The specific forms that the default constructor will accept are enumerated below: .. tabularcolumns:: |p{60mm}|p{70mm}| +------------------------------+------------------------------------------+ | Style | Description | +==============================+==========================================+ | Single scalar parameter | Equivalent to calling | | | :meth:`Color.from_string`, or | | | :meth:`Color.from_rgb24`. | +------------------------------+------------------------------------------+ | Three positional parameters | Equivalent to calling | | or a 3-tuple with no field | :meth:`Color.from_rgb` if all three | | names | parameters are between 0.0 and 1.0, or | | | :meth:`Color.from_rgb_bytes` otherwise. | +------------------------------+ | | Three named parameters, or a | | | 3-tuple with fields | | | "r", "g", "b" | | +------------------------------+ | | Three named parameters, or a | | | 3-tuple with fields | | | "red", "green", "blue" | | +------------------------------+------------------------------------------+ | Three named parameters, or a | Equivalent to calling | | 3-tuple with fields | :meth:`Color.from_yuv` if "y" is between | | "y", "u", "v" | 0.0 and 1.0, "u" is between -0.436 and | | | 0.436, and "v" is between -0.615 and | | | 0.615, or :meth:`Color.from_yuv_bytes` | | | otherwise. | +------------------------------+------------------------------------------+ | Three named parameters, or a | Equivalent to calling | | 3-tuple with fields | :meth:`Color.from_yiq`. | | "y", "i", "q" | | +------------------------------+------------------------------------------+ | Three named parameters, or a | Equivalent to calling | | 3-tuple with fields | :meth:`Color.from_hls`. | | "h", "l", "s" | | +------------------------------+ | | Three named parameters, or a | | | 3-tuple with fields | | | "hue", "lightness", | | | "saturation" | | +------------------------------+------------------------------------------+ | Three named parameters, or a | Equivalent to calling | | 3-tuple with fields | :meth:`Color.from_hsv` | | "h", "s", "v" | | +------------------------------+ | | Three named parameters, or a | | | 3-tuple with fields | | | "hue", "saturation", "value" | | +------------------------------+------------------------------------------+ | Three named parameters, or a | Equivalent to calling | | 3-tuple with fields | :meth:`Color.from_xyz` | | "x", "y", "z" | | +------------------------------+------------------------------------------+ | Three named parameters, or a | Equivalent to calling | | 3-tuple with fields | :meth:`Color.from_lab` | | "l", "a", "b" | | +------------------------------+------------------------------------------+ | Three named parameters, or a | Equivalent to calling | | 3-tuple with fields | :meth:`Color.from_luv` | | "l", "u", "v" | | +------------------------------+------------------------------------------+ If the constructor parameters do not conform to any of the variants in the table above, a :exc:`ValueError` will be raised. Internally, the color is *always* represented as 3 :class:`float` values corresponding to the red, green, and blue components of the color. These values take a value from 0.0 to 1.0 (least to full intensity). The class provides several attributes which can be used to convert one color system into another:: >>> Color('#f00').hls HLS(h=0.0, l=0.5, s=1.0) >>> Color.from_string('green').hue Hue(deg=120.0) >>> Color.from_rgb_bytes(0, 0, 255).yuv YUV(y=0.114, u=0.436, v=-0.10001426533523537) As :class:`Color` derives from tuple, instances are immutable. While this provides the advantage that they can be used in a :class:`set` or as keys of a :class:`dict`, it does mean that colors themselves cannot be *directly* manipulated (e.g. by setting the red component). However, several auxilliary classes in the module provide the ability to perform simple transformations of colors via operators which produce a new :class:`Color` instance. For example, you can add, subtract, and multiply colors directly:: >>> Color('red') + Color('blue') <Color html='#ff00ff' rgb=(1, 0, 1)> >>> Color('magenta') - Color('red') <Color html='#0000ff' rgb=(0, 0, 1)> Values are clipped to ensure the resulting color is still valid:: >>> Color('#ff00ff') + Color('#ff0000') <Color html='#ff00ff' rgb=(1, 0, 1)> You can wrap numbers in constructors like :class:`Red` (or obtain elements of existing colors), then add, subtract, or multiply them with a :class:`Color`:: >>> Color('red') - Red(0.5) <Color html='#800000' rgb=(0.5, 0, 0)> >>> Color('green') + Color('grey').red <Color html='#808000' rgb=(0.501961, 0.501961, 0)> You can even manipulate non-primary attributes like hue, saturation, and lightness with standard addition, subtraction or multiplication operators:: >>> Color.from_hls(0.5, 0.5, 1.0) <Color html='#00ffff' rgb=(0, 1, 1)> >>> Color.from_hls(0.5, 0.5, 1.0) * Lightness(0.8) <Color html='#00cccc' rgb=(0, 0.8, 0.8)> >>> (Color.from_hls(0.5, 0.5, 1.0) * Lightness(0.8)).hls HLS(h=0.5, l=0.4, s=1.0) In the last example above, a :class:`Color` instance is constructed from HLS (hue, lightness, saturation) values with a lightness of 0.5. This is multiplied by a :class:`Lightness` a value of 0.8 which constructs a new :class:`Color` with the same hue and saturation, but a lightness of 0.4 (0.8 × 0.5). If an instance is converted to a string (with :func:`str`) it will return a string containing the 7-character HTML code for the color (e.g. "#ff0000" for red). As can be seen in the examples above, a similar representation is included for the output of :func:`repr`. The output of :func:`repr` can be customized by assigning values to :attr:`Color.repr_style`. .. _RGB: https://en.wikipedia.org/wiki/RGB_color_space .. _Y'UV: https://en.wikipedia.org/wiki/YUV .. _Y'IQ: https://en.wikipedia.org/wiki/YIQ .. _HLS: https://en.wikipedia.org/wiki/HSL_and_HSV .. _HSV: https://en.wikipedia.org/wiki/HSL_and_HSV .. attribute:: red Return the red value as a :class:`Red` instance .. attribute:: green Return the green value as a :class:`Green` instance .. attribute:: blue Return the blue value as a :class:`Blue` instance .. attribute:: repr_style Specifies the style of output returned when using :func:`repr` against a :class:`Color` instance. This is an attribute of the class, not of instances. For example:: >>> Color('#f00') <Color html='#ff0000' rgb=(1, 0, 0)> >>> Color.repr_style = 'html' >>> Color('#f00') Color('#ff0000') The following values are valid: * 'default' - The style shown above * 'term16m' - Similar to the default style, but instead of the HTML style being included, a swatch previewing the color is output. Note that the terminal must support `24-bit color ANSI codes`_ for this to work. * 'term256' - Similar to 'termtrue', but uses the closest color that can be found in the standard 256-color xterm palette. Note that the terminal must support `8-bit color ANSI codes`_ for this to work. * 'html' - Outputs a valid :class:`Color` constructor using the HTML style, e.g. ``Color('#ff99bb')`` * 'rgb' - Outputs a valid :class:`Color` constructor using the floating point RGB values, e.g. ``Color(1, 0.25, 0)`` """ # pylint: disable=too-many-public-methods __slots__ = () repr_style = 'default' def __new__(cls, *args, **kwargs): def from_rgb(r, g, b): "Determine whether bytes or floats are being passed for RGB" if 0.0 <= r <= 1.0 and 0.0 <= g <= 1.0 and 0.0 <= b <= 1.0: return cls.from_rgb(r, g, b) else: return cls.from_rgb_bytes(r, g, b) def from_yuv(y, u, v): "Determine whether bytes or floats are being passed for YUV" if ( 0.0 <= y <= 1.0 and abs(u) <= cv.BT601.Umax and abs(v) <= cv.BT601.Vmax): return cls.from_yuv(y, u, v) else: return cls.from_yuv_bytes(y, u, v) if kwargs: try: # Yes, lambdas are fine here # pylint: disable=unnecessary-lambda return { frozenset('rgb'): from_rgb, frozenset('yuv'): from_yuv, frozenset('yiq'): cls.from_yiq, frozenset('hls'): cls.from_hls, frozenset('hsv'): cls.from_hsv, frozenset('xyz'): cls.from_xyz, frozenset('lab'): cls.from_lab, frozenset('luv'): cls.from_luv, frozenset('cmy'): cls.from_cmy, frozenset('cmyk'): cls.from_cmyk, frozenset(('red', 'green', 'blue')): lambda red, green, blue: from_rgb(red, green, blue), frozenset(('cyan', 'magenta', 'yellow')): lambda cyan, magenta, yellow: cls.from_cmy(cyan, magenta, yellow), frozenset(('cyan', 'magenta', 'yellow', 'black')): lambda cyan, magenta, yellow, black: cls.from_cmyk(cyan, magenta, yellow, black), frozenset(('hue', 'lightness', 'saturation')): lambda hue, lightness, saturation: cls.from_hls(hue, lightness, saturation), frozenset(('hue', 'saturation', 'value')): lambda hue, saturation, value: cls.from_hsv(hue, saturation, value), }[frozenset(kwargs.keys())](**kwargs) except KeyError: pass else: if len(args) == 1: if isinstance(args[0], bytes): spec = args[0].decode('ascii') else: spec = args[0] if isinstance(spec, str): return cls.from_string(spec) elif isinstance(spec, tuple): try: return cls(**spec._asdict()) except AttributeError: if len(spec) == 3: return from_rgb(*spec) elif isinstance(spec, int): return cls.from_rgb24(spec) elif len(args) == 3: r, g, b = args return from_rgb(r, g, b) raise ValueError('Unable to construct Color from provided arguments')
[docs] @classmethod def from_string(cls, s): """ Construct a :class:`Color` from a 4 or 7 character CSS-like representation (e.g. "#f00" or "#ff0000" for red), or from one of the named colors (e.g. "green" or "wheat") from the `CSS standard`_. Any other string format will result in a :exc:`ValueError`. .. _CSS standard: http://www.w3.org/TR/css3-color/#svg-color """ if s[:1] != '#': s = cv.name_to_html(s) return cls.from_rgb_bytes(*cv.html_to_rgb_bytes(s))
[docs] @classmethod def from_rgb(cls, r, g, b): """ Construct a :class:`Color` from three linear `RGB`_ float values between 0.0 and 1.0. """ return super().__new__(cls, cv.clamp_float(r), cv.clamp_float(g), cv.clamp_float(b))
[docs] @classmethod def from_rgb24(cls, n): """ Construct a :class:`Color` from an unsigned 24-bit integer number of the form 0x00BBGGRR. """ return cls.from_rgb_bytes(*cv.rgb24_to_rgb_bytes(n))
[docs] @classmethod def from_rgb565(cls, n): """ Construct a :class:`Color` from an unsigned 16-bit integer number in RGB565 format. """ return cls.from_rgb(*cv.rgb565_to_rgb(n))
[docs] @classmethod def from_rgb_bytes(cls, r, g, b): """ Construct a :class:`Color` from three `RGB`_ byte values between 0 and 255. .. _RGB: https://en.wikipedia.org/wiki/RGB_color_space """ return cls.from_rgb(*cv.rgb_bytes_to_rgb(r, g, b))
[docs] @classmethod def from_yuv(cls, y, u, v): """ Construct a :class:`Color` from three `Y'UV`_ float values. The Y value may be between 0.0 and 1.0. U may be between -0.436 and 0.436, while V may be between -0.615 and 0.615. .. _Y'UV: https://en.wikipedia.org/wiki/YUV """ return cls.from_rgb(*cv.yuv_to_rgb(y, u, v))
[docs] @classmethod def from_yuv_bytes(cls, y, u, v): """ Construct a :class:`Color` from three `Y'UV`_ byte values between 0 and 255. The U and V values are biased by 128 to prevent negative values as is typical in video applications. The Y value is biased by 16 for the same purpose. .. _Y'UV: https://en.wikipedia.org/wiki/YUV """ return cls.from_rgb_bytes(*cv.yuv_bytes_to_rgb_bytes(y, u, v))
[docs] @classmethod def from_yiq(cls, y, i, q): """ Construct a :class:`Color` from three `Y'IQ`_ float values. Y' can be between 0.0 and 1.0, while I and Q can be between -1.0 and 1.0. .. _Y'IQ: https://en.wikipedia.org/wiki/YIQ """ return cls.from_rgb(*cv.yiq_to_rgb(y, i, q))
[docs] @classmethod def from_hls(cls, h, l, s): """ Construct a :class:`Color` from `HLS`_ (hue, lightness, saturation) floats between 0.0 and 1.0. .. _HLS: https://en.wikipedia.org/wiki/HSL_and_HSV """ return cls.from_rgb(*cv.hls_to_rgb(h, l, s))
[docs] @classmethod def from_hsv(cls, h, s, v): """ Construct a :class:`Color` from `HSV`_ (hue, saturation, value) floats between 0.0 and 1.0. .. _HSV: https://en.wikipedia.org/wiki/HSL_and_HSV """ return cls.from_rgb(*cv.hsv_to_rgb(h, s, v))
[docs] @classmethod def from_cmy(cls, c, m, y): """ Construct a :class:`Color` from `CMY`_ (cyan, magenta, yellow) floats between 0.0 and 1.0. .. note:: This conversion uses the basic subtractive method which is not accurate for color reproduction on print devices. See the `Color FAQ`_ for more information. .. _Color FAQ: http://poynton.ca/notes/colour_and_gamma/ColorFAQ.html#RTFToC24 .. _CMY: https://en.wikipedia.org/wiki/CMYK_color_model """ return cls.from_rgb(*cv.cmy_to_rgb(c, m, y))
[docs] @classmethod def from_cmyk(cls, c, m, y, k): """ Construct a :class:`Color` from `CMYK`_ (cyan, magenta, yellow, black) floats between 0.0 and 1.0. .. note:: This conversion uses the basic subtractive method which is not accurate for color reproduction on print devices. See the `Color FAQ`_ for more information. .. _Color FAQ: http://poynton.ca/notes/colour_and_gamma/ColorFAQ.html#RTFToC24 .. _CMYK: https://en.wikipedia.org/wiki/CMYK_color_model """ return cls.from_cmy(*cv.cmyk_to_cmy(c, m, y, k))
[docs] @classmethod def from_xyz(cls, x, y, z): """ Construct a :class:`Color` from (X, Y, Z) float values representing a color in the `CIE 1931 color space`_. The conversion assumes the sRGB working space with reference white D65. .. _CIE 1931 color space: https://en.wikipedia.org/wiki/CIE_1931_color_space """ return cls.from_rgb(*cv.xyz_to_rgb(x, y, z))
[docs] @classmethod def from_lab(cls, l, a, b): """ Construct a :class:`Color` from (L*, a*, b*) float values representing a color in the `CIE Lab color space`_. The conversion assumes the sRGB working space with reference white D65. .. _CIE Lab color space: https://en.wikipedia.org/wiki/Lab_color_space """ return cls.from_xyz(*cv.lab_to_xyz(l, a, b))
[docs] @classmethod def from_luv(cls, l, u, v): """ Construct a :class:`Color` from (L*, u*, v*) float values representing a color in the `CIE Luv color space`_. The conversion assumes the sRGB working space with reference white D65. .. _CIE Luv color space: https://en.wikipedia.org/wiki/CIELUV """ return cls.from_xyz(*cv.luv_to_xyz(l, u, v))
def __add__(self, other): if isinstance(other, types.RGB): return Color.from_rgb(self.r + other.r, self.g + other.g, self.b + other.b) elif isinstance(other, (attr.Red, attr.Green, attr.Blue)): r, g, b = self return Color.from_rgb( r + other if isinstance(other, attr.Red) else r, g + other if isinstance(other, attr.Green) else g, b + other if isinstance(other, attr.Blue) else b, ) elif isinstance(other, (attr.Hue, attr.Lightness, attr.Saturation)): h, l, s = self.hls return Color.from_hls( h + other if isinstance(other, attr.Hue) else h, l + other if isinstance(other, attr.Lightness) else l, s + other if isinstance(other, attr.Saturation) else s, ) elif isinstance(other, attr.Luma): y, u, v = self.yuv return Color.from_yuv(y + other, u, v) return NotImplemented def __radd__(self, other): # Addition is commutative if isinstance(other, (types.RGB, attr.Red, attr.Green, attr.Blue, attr.Hue, attr.Lightness, attr.Saturation, attr.Luma)): return self.__add__(other) return NotImplemented def __sub__(self, other): if isinstance(other, types.RGB): return Color.from_rgb(self.r - other.r, self.g - other.g, self.b - other.b) elif isinstance(other, (attr.Red, attr.Green, attr.Blue)): r, g, b = self.rgb return Color.from_rgb( r - other if isinstance(other, attr.Red) else r, g - other if isinstance(other, attr.Green) else g, b - other if isinstance(other, attr.Blue) else b, ) elif isinstance(other, (attr.Hue, attr.Lightness, attr.Saturation)): h, l, s = self.hls return Color.from_hls( h - other if isinstance(other, attr.Hue) else h, l - other if isinstance(other, attr.Lightness) else l, s - other if isinstance(other, attr.Saturation) else s, ) elif isinstance(other, attr.Luma): y, u, v = self.yuv return Color.from_yuv(y - other, u, v) return NotImplemented def __rsub__(self, other): if isinstance(other, (attr.Red, attr.Green, attr.Blue)): r, g, b = self.rgb return Color.from_rgb( other - r if isinstance(other, attr.Red) else 0.0, other - g if isinstance(other, attr.Green) else 0.0, other - b if isinstance(other, attr.Blue) else 0.0, ) return NotImplemented def __mul__(self, other): if isinstance(other, types.RGB): return Color.from_rgb(self.r * other.r, self.g * other.g, self.b * other.b) elif isinstance(other, (attr.Red, attr.Green, attr.Blue)): r, g, b = self return Color.from_rgb( r * other if isinstance(other, attr.Red) else r, g * other if isinstance(other, attr.Green) else g, b * other if isinstance(other, attr.Blue) else b, ) elif isinstance(other, (attr.Hue, attr.Lightness, attr.Saturation)): h, l, s = self.hls return Color.from_hls( h * other if isinstance(other, attr.Hue) else h, l * other if isinstance(other, attr.Lightness) else l, s * other if isinstance(other, attr.Saturation) else s, ) elif isinstance(other, attr.Luma): y, u, v = self.yuv return Color.from_yuv(y * other, u, v) return NotImplemented def __rmul__(self, other): # Multiplication is commutative if isinstance(other, (types.RGB, attr.Red, attr.Green, attr.Blue, attr.Hue, attr.Lightness, attr.Saturation, attr.Luma)): return self.__mul__(other) return NotImplemented _format_re = re.compile( r'^(' r'(?P<html>html)|' r'(?P<css>css(?P<cssfmt>rgb|hsl)?)|' r'(?P<back>[fb])?(?P<term>0|8|256|16[mM])?' r')$') def __format__(self, format_spec): m = Color._format_re.match(format_spec) if not m: raise ValueError( 'Invalid format {:r} for Color'.format(format_spec)) if m.group('html'): return self.html elif m.group('css'): return self._format_css(m.group('cssfmt')) else: return self._format_term(m.group('back'), m.group('term')) def _format_css(self, cssfmt): return { None: lambda self: 'rgb({0:d}, {1:d}, {2:d})'.format( *self.rgb_bytes), 'rgb': lambda self: 'rgb({0:d}, {1:d}, {2:d})'.format( *self.rgb_bytes), 'hsl': lambda self: 'hsl({0:g}deg, {1:g}%, {2:g}%)'.format( self.hls.hue.deg, self.hls.saturation * 100, self.hls.lightness * 100), }[cssfmt](self) def _format_term(self, back, term): if term == '0': args = ({ None: 0, 'f': 39, 'b': 49, }[back],) elif term in (None, '8'): table = tables.DOS_COLORS if back == 'b': code = 40 table = { k: (bold, index) for k, (bold, index) in table.items() if not bold } else: code = 30 try: bold, index = table[self.rgb_bytes] except KeyError: bold, index = sorted( (self.difference(Color.from_rgb_bytes(*color)), bold, index) for color, (bold, index) in table.items() )[0][1:] args = (1,) if bold else () args += (code + index,) elif term == '256': code = 48 if back == 'b' else 38 try: index = tables.XTERM_COLORS[self.rgb_bytes] except KeyError: index = sorted( (self.difference(Color.from_rgb_bytes(*color)), index) for color, index in tables.XTERM_COLORS.items() )[0][1] args = (48 if back == 'b' else 38, 5, index) elif term.lower() == '16m': args = (48 if back == 'b' else 38, 2) + self.rgb_bytes else: assert False # pragma: no cover return '\x1b[' + ';'.join(str(i) for i in args) + 'm' def __str__(self): return self.html def __repr__(self): try: return { 'default': '<Color html={self.html!r} ' 'rgb=({self.r:g}, {self.g:g}, {self.b:g})>', 'term16m': '<Color {self:16m}###{self:0} ' 'rgb=({self.r:g}, {self.g:g}, {self.b:g})>', 'term256': '<Color {self:256}###{self:0} ' 'rgb=({self.r:g}, {self.g:g}, {self.b:g})>', 'html': 'Color({self.html!r})', 'rgb': 'Color({self.r:g}, {self.g:g}, {self.b:g})', }[Color.repr_style].format(self=self) except KeyError: raise ValueError( 'invalid repr_style value: {}'.format(Color.repr_style)) @property def html(self): """ Returns the color as a string in HTML #RRGGBB format. """ return cv.rgb_bytes_to_html(*self.rgb_bytes) @property def rgb(self): """ Return a simple 3-tuple of (r, g, b) float values in the range 0.0 <= n <= 1.0. .. note:: The :class:`Color` class can already be treated as such a 3-tuple but for the cases where you want a straight :func:`~collections.namedtuple` this property is available. """ return types.RGB(*self) @property def rgb565(self): """ Returns an unsigned 16-bit integer number representing the color in the RGB565 encoding. """ return cv.rgb_to_rgb565(*self) @property def rgb_bytes(self): """ Returns a 3-tuple of (red, green, blue) byte values. """ return cv.rgb_to_rgb_bytes(*self) @property def yuv(self): """ Returns a 3-tuple of (y, u, v) float values; Y values can be between 0.0 and 1.0, U values are between -0.436 and 0.436, and V values are between -0.615 and 0.615. """ return cv.rgb_to_yuv(*self) @property def yuv_bytes(self): """ Returns a 3-tuple of (y, u, v) byte values. Y values are biased by 16 in the result to prevent negatives. U and V values are biased by 128 for the same purpose. """ return cv.rgb_bytes_to_yuv_bytes(*self.rgb_bytes) @property def yiq(self): """ Returns a 3-tuple of (y, i, q) float values; y values can be between 0.0 and 1.0, whilst i and q values can be between -1.0 and 1.0. """ return cv.rgb_to_yiq(*self) @property def xyz(self): """ Returns a 3-tuple of (X, Y, Z) float values representing the color in the `CIE 1931 color space`_. The conversion assumes the sRGB working space, with reference white D65. .. _CIE 1931 color space: https://en.wikipedia.org/wiki/CIE_1931_color_space """ return cv.rgb_to_xyz(*self) @property def lab(self): """ Returns a 3-tuple of (L*, a*, b*) float values representing the color in the `CIE Lab color space`_ with the `D65 standard illuminant`_. .. _CIE Lab color space: https://en.wikipedia.org/wiki/Lab_color_space .. _D65 standard illuminant: https://en.wikipedia.org/wiki/Illuminant_D65 """ return cv.xyz_to_lab(*self.xyz) @property def luv(self): """ Returns a 3-tuple of (L*, u*, v*) float values representing the color in the `CIE Luv color space`_ with the `D65 standard illuminant`_. .. _CIE Luv color space: https://en.wikipedia.org/wiki/CIELUV """ return cv.xyz_to_luv(*self.xyz) @property def hls(self): """ Returns a 3-tuple of (hue, lightness, saturation) float values (between 0.0 and 1.0). """ return cv.rgb_to_hls(*self) @property def hsv(self): """ Returns a 3-tuple of (hue, saturation, value) float values (between 0.0 and 1.0). """ return cv.rgb_to_hsv(*self) @property def cmy(self): """ Returns a 3-tuple of (cyan, magenta, yellow) float values (between 0.0 and 1.0). .. note:: This conversion uses the basic subtractive method which is not accurate for color reproduction on print devices. See the `Color FAQ`_ for more information. .. _Color FAQ: http://poynton.ca/notes/colour_and_gamma/ColorFAQ.html#RTFToC24 """ return cv.rgb_to_cmy(*self) @property def cmyk(self): """ Returns a 4-tuple of (cyan, magenta, yellow, black) float values (between 0.0 and 1.0). .. note:: This conversion uses the basic subtractive method which is not accurate for color reproduction on print devices. See the `Color FAQ`_ for more information. .. _Color FAQ: http://poynton.ca/notes/colour_and_gamma/ColorFAQ.html#RTFToC24 """ return cv.cmy_to_cmyk(*self.cmy) @property def hue(self): """ Returns the hue of the color as a :class:`Hue` instance which can be used in operations with other :class:`Color` instances. """ return attr.Hue(self.hls[0]) @property def lightness(self): """ Returns the lightness of the color as a :class:`Lightness` instance which can be used in operations with other :class:`Color` instances. """ return attr.Lightness(self.hls[1]) @property def saturation(self): """ Returns the saturation of the color as a :class:`Saturation` instance which can be used in operations with other :class:`Color` instances. """ return attr.Saturation(self.hls[2]) @property def luma(self): """ Returns the `luma`_ of the color as a :class:`Luma` instance which can be used in operations with other :class:`Color` instances. .. _luma: https://en.wikipedia.org/wiki/Luma_(video) """ return attr.Luma(self.yuv[0])
[docs] def difference(self, other, method='euclid'): """ Determines the difference between this color and *other* using the specified *method*. :param Color other: The color to compare this color to. :param str method: The algorithm to use in the comparison. Valid values are: * 'euclid' - This is the default method. Calculate the `Euclidian distance`_. This is by far the fastest method, but also the least accurate in terms of human perception. * 'cie1976' - Use the `CIE 1976`_ formula for calculating the difference between two colors in CIE Lab space. * 'cie1994g' - Use the `CIE 1994`_ formula with the "graphic arts" bias for calculating the difference. * 'cie1994t' - Use the `CIE 1994`_ forumula with the "textiles" bias for calculating the difference. * 'ciede2000' - Use the `CIEDE 2000`_ formula for calculating the difference. :returns: A :class:`float` indicating how different the two colors are. Note that the Euclidian distance will be significantly different to the other calculations; effectively this just measures the distance between the two colors by treating them as coordinates in a three dimensional Euclidian space. All other methods are means of calculating a `Delta E`_ value in which 2.3 is considered a `just-noticeable difference`_ (JND). For example:: >>> Color('red').difference(Color('red')) 0.0 >>> Color('red').difference(Color('red'), method='cie1976') 0.0 >>> Color('red').difference(Color('#900')) 0.4 >>> Color('red').difference(Color('#900'), method='cie1976') 40.17063087142142 >>> Color('red').difference(Color('#900'), method='ciede2000') 21.078146289272155 >>> Color('red').difference(Color('blue')) 1.4142135623730951 >>> Color('red').difference(Color('blue'), method='cie1976') 176.31403908880046 .. note:: Instead of using this method, you may wish to simply use the various difference functions (:func:`euclid`, :func:`cie1976`, etc.) directly. .. _Delta E: https://en.wikipedia.org/wiki/Color_difference .. _just-noticeable difference: https://en.wikipedia.org/wiki/Just-noticeable_difference .. _Euclidian distance: https://en.wikipedia.org/wiki/Euclidean_distance .. _CIE 1976: https://en.wikipedia.org/wiki/Color_difference#CIE76 .. _CIE 1994: https://en.wikipedia.org/wiki/Color_difference#CIE94 .. _CIEDE 2000: https://en.wikipedia.org/wiki/Color_difference#CIEDE2000 """ if isinstance(method, bytes): method = method.decode('ascii') try: fn = getattr(deltae, method) except AttributeError: raise ValueError('invalid method: {}'.format(method)) else: if method.startswith('cie'): return fn(self.lab, other.lab) else: return fn(self, other)
[docs] def gradient(self, other, steps=10, easing=easings.linear): """ Returns a generator which fades between this color and *other* in the specified number of *steps*. :param Color other: The color that will end the gradient (with the color the method is called upon starting the gradient) :param int steps: The unqiue number of colors to include in the generated gradient. Defaults to 10 if unspecified. :param callable easing: A function which controls the speed of the progression. If specified, if must be a function which takes a single parameter, the number of *steps*, and yields a sequence of values between 0.0 (representing the start of the gradient) and 1.0 (representing the end). The default is :func:`linear`. :return: A generator yielding *steps* :class:`Color` instances which fade from this color to *other*. For example:: >>> Color.repr_style = 'html' >>> print('\\n'.join( ... repr(c) for c in ... Color('red').gradient(Color('green')) ... )) Color('#ff0000') Color('#e30e00') Color('#c61c00') Color('#aa2b00') Color('#8e3900') Color('#714700') Color('#555500') Color('#396400') Color('#1c7200') Color('#008000') .. versionadded:: 1.1 """ if steps < 2: raise ValueError('steps must be >= 2') # NOTE: Can't simply subtract self from other here, as the result will # be clamped and we want the actual result. delta = types.RGB(*( other_i - self_i for self_i, other_i in zip(self, other) )) for t in easing(steps): yield self + types.RGB(*(delta_i * t for delta_i in delta))