pypindou.color.model 源代码

"""
Palette and bead-color data models.

This module defines the shared schema used for domestic and international bead
palette resources.  Resource builders normalize upstream data into
:class:`BeadColor` and :class:`Palette`, while pattern generation consumes the
same objects for color matching and reporting.

Example::

    >>> from pypindou.color.model import BeadColor, Palette
    >>> palette = Palette("demo", "Demo", (BeadColor("001", (255, 255, 255)),))
    >>> palette.size
    1
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple


RGB = Tuple[int, int, int]


def _check_rgb(rgb: Sequence[int]) -> RGB:
    if len(rgb) != 3:
        raise ValueError("RGB value should contain exactly 3 channels.")

    values = tuple(int(item) for item in rgb)
    for item in values:
        if not 0 <= item <= 255:
            raise ValueError(f"RGB channel should be in [0, 255], but {item!r} found.")
    return values  # type: ignore[return-value]


[文档] def rgb_to_hex(rgb: Sequence[int]) -> str: """ Convert an RGB triplet to ``#RRGGBB`` format. :param rgb: Three RGB channel values in ``0..255``. :type rgb: Sequence[int] :return: Uppercase hex color string. :rtype: str :raises ValueError: If ``rgb`` does not contain exactly three valid channel values. Example:: >>> rgb_to_hex((255, 128, 0)) '#FF8000' """ r, g, b = _check_rgb(rgb) return "#{:02X}{:02X}{:02X}".format(r, g, b)
[文档] def hex_to_rgb(value: str) -> RGB: """ Convert ``#RRGGBB`` or ``RRGGBB`` to an RGB triplet. :param value: Hex color text. :type value: str :return: RGB tuple. :rtype: RGB :raises ValueError: If the input is not a six-digit hex color. Example:: >>> hex_to_rgb("#FF8000") (255, 128, 0) """ text = value.strip() if text.startswith("#"): text = text[1:] if len(text) != 6: raise ValueError(f"Invalid hex color {value!r}.") try: return int(text[0:2], 16), int(text[2:4], 16), int(text[4:6], 16) except ValueError as err: raise ValueError(f"Invalid hex color {value!r}.") from err
[文档] @dataclass(frozen=True) class BeadColor: """ One color entry in a fuse-bead palette. :param code: Palette-specific bead code. :type code: str :param rgb: RGB channel tuple in ``0..255``. :type rgb: RGB :param name: Human-readable color name, defaults to ``None``. :type name: Optional[str], optional :param hex: Hex color string. When omitted it is derived from ``rgb``. :type hex: Optional[str], optional :param group: Optional upstream color group. :type group: Optional[str], optional :param source: Optional source label. :type source: Optional[str], optional :param unidentified: Whether upstream marks the color as unidentified, defaults to ``False``. :type unidentified: bool, optional :param original_code: Raw upstream code before normalization, defaults to ``None``. :type original_code: Optional[str], optional :param metadata: Extra normalized upstream fields. :type metadata: Mapping[str, Any], optional Example:: >>> BeadColor("001", (255, 255, 255)).hex '#FFFFFF' """ code: str rgb: RGB name: Optional[str] = None hex: Optional[str] = None group: Optional[str] = None source: Optional[str] = None unidentified: bool = False original_code: Optional[str] = None metadata: Mapping[str, Any] = field(default_factory=dict) def __post_init__(self) -> None: object.__setattr__(self, "rgb", _check_rgb(self.rgb)) object.__setattr__(self, "hex", (self.hex or rgb_to_hex(self.rgb)).upper())
[文档] def to_dict(self) -> Dict[str, Any]: """ Convert this color to a JSON-serializable dictionary. :return: Dictionary representation using plain JSON-compatible values. :rtype: Dict[str, Any] """ data: Dict[str, Any] = { "code": self.code, "name": self.name, "rgb": list(self.rgb), "hex": self.hex, "group": self.group, "source": self.source, "unidentified": self.unidentified, "original_code": self.original_code, "metadata": dict(self.metadata), } return data
[文档] @dataclass(frozen=True) class Palette: """ A named collection of bead colors. :param id: Stable palette id. :type id: str :param title: Human-readable palette title. :type title: str :param colors: Palette colors in source order. :type colors: Tuple[BeadColor, ...] :param description: Optional palette description, defaults to ``None``. :type description: Optional[str], optional :param standard: Palette standard, usually ``"domestic"`` or ``"international"``, defaults to ``"domestic"``. :type standard: str, optional :param source: Optional source family. :type source: Optional[str], optional :param source_id: Optional upstream source id. :type source_id: Optional[str], optional :param source_url: Optional upstream URL. :type source_url: Optional[str], optional :param metadata: Extra palette metadata. :type metadata: Mapping[str, Any], optional :raises ValueError: If no colors are provided or color codes are duplicated. Example:: >>> Palette("demo", "Demo", (BeadColor("001", (1, 2, 3)),)).by_code("001").rgb (1, 2, 3) """ id: str title: str colors: Tuple[BeadColor, ...] description: Optional[str] = None standard: str = "domestic" source: Optional[str] = None source_id: Optional[str] = None source_url: Optional[str] = None metadata: Mapping[str, Any] = field(default_factory=dict) def __post_init__(self) -> None: object.__setattr__(self, "colors", tuple(self.colors)) if not self.colors: raise ValueError("Palette should contain at least one color.") codes = [color.code for color in self.colors] if len(codes) != len(set(codes)): duplicated = sorted({code for code in codes if codes.count(code) > 1}) raise ValueError(f"Duplicated color codes in palette {self.id!r}: {duplicated!r}.") @property def size(self) -> int: """ Number of colors in this palette. :return: Palette size. :rtype: int """ return len(self.colors)
[文档] def by_code(self, code: str) -> BeadColor: """ Get a color by code. :param code: Palette-specific bead code. :type code: str :return: Matching bead color. :rtype: BeadColor :raises KeyError: If the code does not exist in this palette. """ index = {color.code: color for color in self.colors} try: return index[code] except KeyError as err: raise KeyError(f"Color code {code!r} not found in palette {self.id!r}.") from err
[文档] def filter( self, *, include_codes: Optional[Iterable[str]] = None, exclude_codes: Optional[Iterable[str]] = None, allow_unidentified: bool = False, max_colors: Optional[int] = None, ) -> "Palette": """ Return a filtered palette while preserving source order. :param include_codes: Optional allow-list of color codes. :type include_codes: Optional[Iterable[str]], optional :param exclude_codes: Optional deny-list of color codes. :type exclude_codes: Optional[Iterable[str]], optional :param allow_unidentified: Whether to keep colors marked ``unidentified``, defaults to ``False``. :type allow_unidentified: bool, optional :param max_colors: Maximum number of colors to keep after filtering, defaults to ``None``. :type max_colors: Optional[int], optional :return: Filtered palette. :rtype: Palette :raises ValueError: If ``max_colors`` is not positive. """ include_set = set(include_codes) if include_codes is not None else None exclude_set = set(exclude_codes or ()) colors: List[BeadColor] = [] for color in self.colors: if include_set is not None and color.code not in include_set: continue if color.code in exclude_set: continue if color.unidentified and not allow_unidentified: continue colors.append(color) if max_colors is not None: if max_colors <= 0: raise ValueError("max_colors should be positive.") colors = colors[:max_colors] return Palette( id=self.id, title=self.title, colors=tuple(colors), description=self.description, standard=self.standard, source=self.source, source_id=self.source_id, source_url=self.source_url, metadata=dict(self.metadata), )
[文档] def to_dict(self) -> Dict[str, Any]: """ Convert this palette to a JSON-serializable dictionary. :return: Dictionary representation using plain JSON-compatible values. :rtype: Dict[str, Any] """ data: Dict[str, Any] = { "id": self.id, "title": self.title, "description": self.description, "standard": self.standard, "source": self.source, "source_id": self.source_id, "source_url": self.source_url, "count": len(self.colors), "metadata": dict(self.metadata), "colors": [color.to_dict() for color in self.colors], } return data