"""Python utilities for colors"""
__version__ = "0.5.0"
from colorsys import hls_to_rgb, hsv_to_rgb, rgb_to_hls, rgb_to_hsv
from math import sqrt
from random import randint
from averager import average
from .exceptions import InvalidRGBValue, WrongLengthError
from .vars import CSS_COLORS
[docs]def normalize_hex(hex_code: str) -> str:
"""Normalizes a hex color code
Removes the leading ``#`` if there is one, expands 3-character hex codes,
and lowercases the hex code
Args:
hex_code (:class:`str`): A hex code to normalize
Returns:
:class:`str`: The normalized hex code
Raises:
:exception:`~pigment.exceptions.WrongLengthError`: The provided hex
code had the wrong length
"""
hex_code = hex_code.lstrip("#").lower()
if len(hex_code) == 3:
hex_code = "".join(c * 2 for c in hex_code)
if len(hex_code) != 6:
raise WrongLengthError("hex_code")
return hex_code
[docs]class Color:
"""Represents a color
Args:
red (:class:`int`): The color's red RGB component (0-255)
green (:class:`int`): The color's green RGB component (0-255)
blue (:class:`int`): The color's blue RGB component (0-255)
Note:
The :attr:`~cmyk`, :attr:`~hex_code`, :attr:`~hls`, :attr:`~hsv`, and
:attr:`~rgb` properties can be set to update the color object.
"""
def __init__(self, red: int, green: int, blue: int):
self.rgb = (red, green, blue)
def __repr__(self):
return "<pigment.Color r=%r g=%r b=%r>" % self.rgb
def __eq__(self, other):
if isinstance(other, Color):
return self.rgb == other.rgb
return self.rgb == other
@property
def rgb(self):
"""The color as an RGB tuple
* Red (0-255)
* Green (0-255)
* Blue (0-255)
"""
return self._rgb
@rgb.setter
def rgb(self, value):
if len(value) != 3:
raise WrongLengthError()
for c in value:
if c not in range(256):
raise InvalidRGBValue()
self._rgb = value
@property
def hex_code(self):
"""The color's hex code"""
return "%02x%02x%02x" % (self.rgb[0], self.rgb[1], self.rgb[2])
@hex_code.setter
def hex_code(self, value):
self.rgb = tuple(
int(normalize_hex(value)[i : i + 2], 16) for i in (0, 2, 4)
)
@property
def hsv(self):
"""The color as an HSV tuple
* Hue: color (0-360)
* Saturation: amount of gray vs. color (0-100)
* Value: amount of black vs. color (0-100)
"""
res = rgb_to_hsv(
self.rgb[0] / 255, self.rgb[1] / 255, self.rgb[2] / 255
)
return (
round(res[0] * 360),
round(res[1] * 100),
round(res[2] * 100),
)
@hsv.setter
def hsv(self, value):
self.rgb = tuple(
round(x * 255)
for x in hsv_to_rgb(value[0] / 360, value[1] / 100, value[2] / 100)
)
@property
def hue(self):
"""The color's hue (0-360)"""
return self.hsv[0]
@hue.setter
def hue(self, value):
self.hsv = (value, *self.hsv[1:])
@property
def hls(self):
"""The color as an HLS tuple
* Hue: color (0-360)
* Lightness: amount of white vs. color (0-100)
* Saturation: amount of gray vs. color (0-100)
"""
res = rgb_to_hls(
self.rgb[0] / 255, self.rgb[1] / 255, self.rgb[2] / 255
)
return (
round(res[0] * 360),
round(res[1] * 100),
round(res[2] * 100),
)
@hls.setter
def hls(self, value):
self.rgb = tuple(
round(x * 255)
for x in hls_to_rgb(value[0] / 360, value[1] / 100, value[2] / 100)
)
@property
def cmyk(self):
"""The color as a CMYK tuple
* Cyan (0-100)
* Magenta (0-100)
* Yellow (0-100)
* Key/Black (0-100)
"""
if self.rgb == (0, 0, 0):
return (0, 0, 0, 100)
c = 1 - self.rgb[0] / 255
m = 1 - self.rgb[1] / 255
y = 1 - self.rgb[2] / 255
min_cmy = min(c, m, y)
c = (c - min_cmy) / (1 - min_cmy)
m = (m - min_cmy) / (1 - min_cmy)
y = (y - min_cmy) / (1 - min_cmy)
k = min_cmy
return (round(c * 100), round(m * 100), round(y * 100), round(k * 100))
@cmyk.setter
def cmyk(self, value):
cyan, magenta, yellow, key = value
self.rgb = (
round(255 * (1 - cyan / 100) * (1 - key / 100)),
round(255 * (1 - magenta / 100) * (1 - key / 100)),
round(255 * (1 - yellow / 100) * (1 - key / 100)),
)
[docs] @classmethod
def from_css_name(cls, css_color: str):
"""Gets a color from a CSS color name
Args:
css_color (:class:`str`): The name of the CSS color
Returns:
:class:`~pigment.Color`
"""
return Color(*CSS_COLORS[css_color])
[docs] @classmethod
def random(
cls,
red: tuple = (0, 255),
green: tuple = (0, 255),
blue: tuple = (0, 255),
):
"""Generates a random color
This works by generating random red, green, and blue values using
:func:`random.randint` from the standard library using the min/max
values specified if any
Args:
red (:class:`tuple`): The two arguments to pass for the red value
green (:class:`tuple`): The two arguments to pass for the green
value
blue (:class:`tuple`): The two arguments to pass for the blue value
Returns:
:class:`~pigment.Color`
"""
return Color(randint(*red), randint(*green), randint(*blue))
[docs]def blend(color1: Color, color2: Color) -> Color:
"""Blends two colors together
Args:
color1 (:class:`~pigment.Color`): The first color
color2 (:class:`~pigment.Color`): The second color
Returns:
:class:`~pigment.Color`
"""
colors = (color1.rgb, color2.rgb)
return Color(
*(round(sqrt(average([c[i] ** 2 for c in colors]))) for i in range(3))
)