Исходный код EJIO.utils.component.color.color

## /utils/component/color/color.py

from typing import Any

__all__ = [
    'Color',
]


[документация] class Color: """ Универсальный класс для работы с цветом. Инкапсулирует парсинг из любых форматов (HEX, RGB, RGBA, float) и лениво преобразует его в нужные рантайм-представления. """ __slots__ = ('_r', '_g', '_b', '_a') def __init__(self, *args: Any) -> None: """ Инициализирует объект цвета. Принимает аргументы в различных форматах. Примеры вызовов: Color("#FF5733") # HEX строка Color("complex", 0.5) # HEX без решетки + прозрачность float Color(255, 87, 51) # Изолированные целые числа RGB Color(255, 87, 51, 128) # RGBA с альфа-каналом Color((0.1, 0.5, 1.0)) # Нормализованный кортеж float (0.0 - 1.0) """ self._r: int = 0 self._g: int = 0 self._b: int = 0 self._a: int = 255 # По умолчанию полностью непрозрачный self._parse_args(args) def _parse_args(self, args: tuple[Any, ...]) -> None: """ Внутренняя функция-хелпер для разбора аргументов конструктора. :param args: Список аргументов. :raises ValueError: Если параметры не заданы или если их количество не соответствует ни одному варианту инициализации. :raises TypeError: Если переданный тип данных не поддерживется. """ if not args: raise ValueError("Color value is needed for initialization.") # Случай 1: Передан один аргумент (строка HEX или кортеж/список чисел) if len(args) == 1: val = args[0] if isinstance(val, str): self._parse_hex(val) elif isinstance(val, tuple | list): self._parse_sequence(val) elif isinstance(val, Color): self._r, self._g, self._b, self._a = val.rgba else: raise TypeError(f"Unsupported data type for color: {type(val).__name__}") # Случай 2: Передана строка HEX + явная альфа-компонента (например, Color("FF5733", 128)) elif len(args) == 2 and isinstance(args[0], str): self._parse_hex(args[0]) self._a = self._clamp_int(args[1]) # Случай 3: Переданы изолированные компоненты чисел (RGB или RGBA) elif len(args) in (3, 4): self._parse_sequence(args) else: raise ValueError("Incorrect number of arguments for initialization of Color.") def _parse_hex(self, hex_str: str) -> None: """ Парсит строковое HEX представление. :param hex_str: Строка хеша цвета. :raises ValueError: Если хеш-строка не соответствует формату. """ clean = hex_str.lstrip('#').strip() # Поддержка коротких HEX (например, 'FFF' -> 'FFFFFF') if len(clean) in (3, 4): clean = "".join(c * 2 for c in clean) if len(clean) == 6: self._r = int(clean[0:2], 16) self._g = int(clean[2:4], 16) self._b = int(clean[4:6], 16) self._a = 255 elif len(clean) == 8: self._r = int(clean[0:2], 16) self._g = int(clean[2:4], 16) self._b = int(clean[4:6], 16) self._a = int(clean[6:8], 16) else: raise ValueError(f"Incorrect length of color HEX-string: '{hex_str}'") def _parse_sequence(self, seq: tuple[Any, ...] | list[Any]) -> None: """ Парсит последовательность чисел (целые 0-255 или float 0.0-1.0). :param seq: Последовательность чисел. :raises ValueError: Если для последовательности цветов переданно неверное количиство аргументов. """ if len(seq) not in (3, 4): raise ValueError("Sequence for a color should contain strictly either 3 or 4 elements.") # Определяем, в каком формате переданы числа. # Если хотя бы одно число является float меньше 1.0, считаем весь массив нормализованным is_float = any(isinstance(x, float) and x <= 1.0 for x in seq) if is_float: self._r = round(float(seq[0]) * 255) self._g = round(float(seq[1]) * 255) self._b = round(float(seq[2]) * 255) if len(seq) == 4: self._a = round(float(seq[3]) * 255) else: self._r = self._clamp_int(seq[0]) self._g = self._clamp_int(seq[1]) self._b = self._clamp_int(seq[2]) if len(seq) == 4: self._a = self._clamp_int(seq[3]) @staticmethod def _clamp_int(val: Any) -> int: """ Ограничивает число в безопасных границах байта [0-255]. :param val: Число. :raises TypeError: Если отдельный байт цвета задан не числом. """ try: ival = int(val) return max(0, min(255, ival)) except (ValueError, TypeError): raise TypeError(f"Color component should be a number, received: {val!r}") @property def rgb(self) -> tuple[int, int, int]: """ Возвращает классический кортеж целых чисел (R, G, B). :return: Цвет в формате RGB. """ return self._r, self._g, self._b @property def rgba(self) -> tuple[int, int, int, int]: """ Возвращает полный кортеж целых чисел с альфа-каналом (R, G, B, A). :return: Цвет в формате RGBA. """ return self._r, self._g, self._b, self._a @property def hex(self) -> str: """ Возвращает стандартную шестнадцатеричную строку (например, '#FF5733'). :return: Цвет в формате HEX. """ return f"#{self._r:02X}{self._g:02X}{self._b:02X}" @property def hex_alpha(self) -> str: """ Возвращает HEX строку, включающую альфа-компоненту (например, '#FF573380'). :return: Цвет в формате HEXA. """ return f"#{self._r:02X}{self._g:02X}{self._b:02X}{self._a:02X}" @property def normalized(self) -> tuple[float, float, float, float]: """ Возвращает нормализованные float-координаты от 0.0 до 1.0. :return: Нормализованный цвет в формате RGBA. """ return self._r / 255.0, self._g / 255.0, self._b / 255.0, self._a / 255.0 def __str__(self) -> str: """ По умолчанию строковое приведение отдает готовый HEX для UI-виджетов. :return: Строковое представление цвета в виде hex-строки. """ return self.hex def __repr__(self) -> str: """ Строковое представление цвета. :return: Представление цвета в виде строки. """ return f"Color(R={self._r}, G={self._g}, B={self._b}, A={self._a})" def __eq__(self, other: Any) -> bool: """ Два цвета равны, если полностью совпадают их RGBA компоненты. :param other: Объект для сравнения. :return: Результат сравнения. """ if isinstance(other, Color): return self.rgba == other.rgba if isinstance(other, tuple | list) and len(other) in (3, 4): # Позволяет сравнивать объект Color напрямую с кортежами чисел try: check_color = Color(other) return self.rgba == check_color.rgba except Exception: return False if isinstance(other, str): try: check_color = Color(other) return self.rgba == check_color.rgba except Exception: return False return False