from typing import Literal, Any
from termcolor import COLORS
[docs]
class InvalidColorError(Exception):
[docs]
def __init__(self, color: Any | None = None, message: str | None = None):
"""
Exception raised for invalid colors
:param color: Invalid color
:param message: Exception message
"""
self.color = color
self.message = f"Not a valid color: '{self.color}'" if not message else message
super().__init__(self.message)
def _validate_color(color: str | tuple) -> str | tuple:
"""
Checks if a color is a valid termcolor color
:param color: termcolor color code or RGB tuple
:return: termcolor color code or RGB tuple
"""
if isinstance(color, tuple) and not all(0 <= c <= 255 for c in color):
raise InvalidColorError(message="RGB values must be between 0 and 255")
elif isinstance(color, str) and color.lower() not in list(COLORS.keys()):
raise InvalidColorError(message=color)
return color
[docs]
class Color:
[docs]
def __init__(self, color: str | tuple | Color | None = None,
mode: Literal["rgb", "hex", "hsl", "hsv"] = "rgb"):
"""
Converts a color to a termcolor-supported color
**Available Color Types**
- Termcolor color code (Any)
- RGB tuple (rgb)
- HEX code (hex)
- HSV code (hsv)
- HSL code (hsl)
:param color: Color to convert
:param mode: Color type
"""
if isinstance(color, Color):
# Copy termcolor color to new object
self.tc_color: str | tuple[int, int, int] = color.tc_color
return
self._color: str | tuple = color if color else ''
self._mode: Literal["rgb", "hex", "hsl", "hsv"] = mode
self.tc_color: str | tuple[int, int, int] = ""
self._convert()
def _convert(self):
"""
Converts the color to a termcolor-supported color
"""
if not self._color:
return
if isinstance(self._color, str) and self._color.lower() in COLORS:
# Valid termcolor color
self.tc_color = self._color.lower()
return
match self._mode:
case "rgb":
# RGB
# noinspection PyTypeChecker
self.tc_color = _validate_color(color=self._color)
case "hex":
# HEX
if not isinstance(self._color, str):
# Wrong mode used
raise InvalidColorError(message=f"Color '{self._color}' not a valid HEX code."
"\nDid you mean `mode='hsl'` or `mode='hsv'`?")
self.tc_color = self._hex_to_rgb(color=self._color)
case "hsv":
# HSV
if not isinstance(self._color, tuple):
# Wrong mode used
raise InvalidColorError(message=f"Color '{self._color}' not a valid HSV code."
"\nDid you mean `mode='hex'`?")
self.tc_color = self._hsv_to_rgb(color=self._color)
case "hsl":
# HSL
if not isinstance(self._color, tuple):
# Wrong mode used
raise InvalidColorError(message=f"Color '{self._color}' not a valid HSL code."
"\nDid you mean `mode='hex'`?")
self.tc_color = self._hsl_to_rgb(color=self._color)
case _:
raise ValueError(f"Invalid color mode: {self._mode}")
def __str__(self) -> str:
return str(self.tc_color)
# ========================================================
# Color Conversion Methods
# ========================================================
@staticmethod
def _hex_to_rgb(color: str) -> tuple[int, int, int]:
"""
Convert HEX color to RGB
:param color: HEX value
:return: RGB tuple
"""
# Remove '#'
color: str = color.lstrip("#")
# Create colors
r: int = int(color[:2], 16)
g: int = int(color[2:4], 16)
b: int = int(color[4:6], 16)
return r, g, b
@staticmethod
def _hsl_to_rgb(color: tuple) -> tuple[int, int, int]:
"""
Convert HSL color to RGB
Math equations sourced from `Wikipedia <https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB>`_
:param color: HSL tuple
:return: RGB tuple
"""
h = color[0]
s = float(color[1])
l = float(color[2])
# Ensure H, S, L within bounds
if not 0 <= h <= 360:
raise InvalidColorError(message="Hue must be between 0 and 360")
if not 0 <= s <= 1:
raise InvalidColorError(message="Saturation must be between 0 and 1")
if not 0 <= l <= 1:
raise InvalidColorError(message="Lightness must be between 0 and 1")
# Find Chroma (C)
c = (1 - abs(2 * l - 1)) * s
# Get H'
h_p = h / 60
# Get intermediate value (X)
x = c * (1 - abs(h_p % 2 - 1))
# Get color point
if 0 <= h_p < 1:
pr, pg, pb = c, x, 0
elif 1 <= h_p < 2:
pr, pg, pb = x, c, 0
elif 2 <= h_p < 3:
pr, pg, pb = 0, c, x
elif 3 <= h_p < 4:
pr, pg, pb = 0, x, c
elif 4 <= h_p < 5:
pr, pg, pb = x, 0, c
elif 5 <= h_p < 6:
pr, pg, pb = c, 0, x
else:
# Something went wrong. Assume invalid color
raise InvalidColorError(message=f"HSL to RGB conversion failed: {color}")
# Get final color
m = l - (c / 2)
r = round(255 * (pr + m))
g = round(255 * (pg + m))
b = round(255 * (pb + m))
return r, g, b
@staticmethod
def _hsv_to_rgb(color: tuple) -> tuple[int, int, int]:
"""
Convert HSV color to RGB
Math equations sourced from `Wikipedia <https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB>`_
:param color: HSV tuple
:return: RGB tuple
"""
h = color[0]
s = float(color[1])
v = float(color[2])
# Ensure H, S, V within bounds
if not 0 <= h <= 360:
raise InvalidColorError(message="Hue must be between 0 and 360")
if not 0 <= s <= 1:
raise InvalidColorError(message="Saturation must be between 0 and 1")
if not 0 <= v <= 1:
raise InvalidColorError(message="Value must be between 0 and 1")
# Find Chroma (C)
c = v * s
# Get H'
h_p = h / 60
# Get intermediate value (X)
x = c * (1 - abs(h_p % 2 - 1))
# Get color point
if 0 <= h_p < 1:
pr, pg, pb = c, x, 0
elif 1 <= h_p < 2:
pr, pg, pb = x, c, 0
elif 2 <= h_p < 3:
pr, pg, pb = 0, c, x
elif 3 <= h_p < 4:
pr, pg, pb = 0, x, c
elif 4 <= h_p < 5:
pr, pg, pb = x, 0, c
elif 5 <= h_p < 6:
pr, pg, pb = c, 0, x
else:
# Something went wrong. Assume invalid color
raise InvalidColorError(message=f"HSV to RGB conversion failed: {color}")
# Get final color
m = v - c
r = round(255 * (pr + m))
g = round(255 * (pg + m))
b = round(255 * (pb + m))
return r, g, b