Source code for framed_text.color

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