color_tools.palette

Color palette management with fast lookup.

This module provides: 1. Data class for colors (ColorRecord) 2. Functions to load color data from JSON with user override support 3. Palette class with multiple indices for O(1) lookups 4. Nearest-color search using various distance metrics

For filament management, see color_tools.filament_palette module.

The Palette class is like a database with multiple indexes - you can search by name, RGB, HSL, etc. and get instant results!

User colors contain authored names and hex values. They replace system colors by normalized name, append otherwise, and are reported through structured load events.

Example

>>> from color_tools import Palette
>>>
>>> # Load CSS color database
>>> palette = Palette.load_default()
>>> print(f"Loaded {len(palette.colors)} CSS colors")
Loaded 148 CSS colors
>>>
>>> # Find exact color by name
>>> coral = palette.get_by_name("coral")
>>> print(f"Coral: RGB{coral.rgb} (#{coral.hex})")
Coral: RGB(255, 127, 80) (#FF7F50)
>>>
>>> # Find nearest color to custom RGB
>>> custom_orange = (255, 140, 60)
>>> nearest, distance = palette.nearest_color(custom_orange)
>>> print(f"Nearest to RGB{custom_orange}: {nearest.name} (ΔE: {distance:.1f})")
Nearest to RGB(255, 140, 60): darkorange (ΔE: 7.2)
class color_tools.palette.ColorRecord(name, hex, rgb, hsl, lab, lch, source='colors.json')[source]

Bases: object

Immutable record representing a named CSS color with precomputed color space values.

This dataclass is frozen (immutable) - once created, you can’t change it. This is perfect for colors: a color IS what it IS! 🎨

All color space values are precomputed and stored for fast access without conversion overhead. The source field tracks which JSON file provided this color, enabling user override detection and debugging.

Variables:
  • name (str) – Color name (e.g., “coral”, “lightblue”, “darkslategray”)

  • hex (str) – Hex color code with # prefix (e.g., “#FF7F50”)

  • rgb (Tuple[int, int, int]) – RGB color tuple with values 0-255 (e.g., (255, 127, 80))

  • hsl – HSL color tuple (H: 0-360°, S: 0-100%, L: 0-100%)

  • lab – CIE LAB color tuple (L: 0-100, a: ~-128 to +127, b: ~-128 to +127)

  • lch – CIE LCH color tuple (L: 0-100, C: 0-100+, H: 0-360°)

  • source (str) – JSON filename where this record originated (default: “colors.json”)

Parameters:

Example

>>> from color_tools import Palette
>>> palette = Palette.load_default()
>>> color = palette.get_by_name("coral")
>>> print(f"{color.name}: RGB{color.rgb}")
coral: RGB(255, 127, 80)
>>> print(f"LAB: {color.lab}")
LAB: (67.29, 44.61, 49.72)
name: str
hex: str
rgb: Tuple[int, int, int]
hsl: Tuple[float, float, float]
lab: Tuple[float, float, float]
lch: Tuple[float, float, float]
source: str = 'colors.json'
classmethod from_rgb(rgb, *, name=None, source='custom', auto_name=False)[source]

Create a record from 8-bit RGB, computing all derived color values.

Explicit names take precedence over auto-naming. Without either, the name is Color 1. Auto-naming uses the built-in CSS/descriptive namer. Invalid channel counts, non-integers, and out-of-range values raise ValueError rather than being coerced or clamped.

Example

>>> ColorRecord.from_rgb((255, 0, 0), auto_name=True).name
'red'
Return type:

ColorRecord

Parameters:
classmethod from_hex(hex_code, *, name=None, source='custom', auto_name=False)[source]

Create a record from RGB/RGBA/RRGGBB/RRGGBBAA, optionally prefixed with #.

Naming follows from_rgb(); malformed hex raises ValueError. Alpha uses CSS/PNG ordering and is validated then dropped.

Example

>>> ColorRecord.from_hex("#f00", auto_name=True).name
'red'
Return type:

ColorRecord

Parameters:
to_swatch_png(*, width=64, height=64, mode='solid')[source]

Render a dependency-free PNG swatch for this color.

A color record has one RGB component, so every supported mode produces the same solid image. Dimensions must be positive built-in integers.

Return type:

bytes

Parameters:
  • width (int)

  • height (int)

  • mode (Literal['solid', 'blocks', 'gradient'])

save_swatch_png(path, *, width=64, height=64, mode='solid')[source]

Save a PNG swatch, create parent directories, and return its Path.

Return type:

Path

Parameters:
__str__()[source]

Human-readable color representation: name (#hex)

Return type:

str

color_tools.palette.load_system_colors(json_path)[source]

Load one core-format color file without applying user overlays.

Return type:

List[ColorRecord]

Parameters:

json_path (Path | str)

color_tools.palette.load_colors(json_path=None, *, include_report=False)[source]

Load and merge the color database, optionally returning structured changes.

Overloads:
Parameters:
Return type:

List[ColorRecord] | tuple[List[ColorRecord], ColorLoadReport]

Loads colors from both the core colors.json file and optional user/user-colors.json file in the data directory. User colors replace records by normalized name or append in file order. Effective RGB values must remain unique.

Parameters:
  • json_path (Path | str | None) – Path to directory containing JSON files, or path to specific colors JSON file. If None, looks for colors.json in the package’s data/ directory.

  • include_report (bool) – When true, return the records together with a typed summary of user additions and name replacements.

Returns:

The merged records, or (records, report) when include_report is true.

Return type:

List[ColorRecord] | tuple[List[ColorRecord], ColorLoadReport]

color_tools.palette.load_palette(name, json_path=None)[source]

Load a named retro palette from the palettes directory.

Built-in palettes are loaded from data/palettes/{name}.json. User palettes are loaded from data/user/palettes/user-{name}.json and must be requested with the user- prefix. Built-in and user palettes use the same versioned document envelope. User palette color records require only name and hex; all other color-space values are calculated from the hex value when loaded. Files ending in example.json are excluded from palette discovery.

Built-in system palettes are presentation-style sRGB representations for simulating how an image would look on the named platform. They are not guarantees of electrically exact output from every hardware revision, video standard, signal path, display, capture, specification, or emulator.

Common built-in palettes include: - cga4: CGA 4-color palette (Palette 1, high intensity) - classic gaming! - cga16: CGA 16-color palette (full RGBI) - ega16: EGA 16-color palette (standard/default) - ega64: EGA 64-color palette (full 6-bit RGB) - vga: VGA 256-color palette (Mode 13h) - web: Web-safe 216-color palette (6x6x6 RGB cube) - gameboy: GB Studio 4-shade authoring palette - gameboy-dmg: Original Game Boy screen approximation - gameboy-light: Backlit Game Boy Light screen approximation - gameboy-pocket: Neutral 4-shade Game Boy Pocket grayscale - ti-99-4a: TI-99/4A CRT-style TMS9918 palette - colecovision: ColecoVision CRT-style TMS9918 palette - sg-1000: SG-1000 CRT-style TMS9918 palette - msx1: MSX1 CRT-style TMS9918 palette - bbc-micro: BBC Micro 8-color physical palette - zx-spectrum: ZX Spectrum 15-color display approximation - amstrad-cpc: Amstrad CPC 27-color hardware palette - tic80: TIC-80 default SWEETIE-16 palette

Parameters:
  • name (str) – Palette name (e.g., ‘cga4’, ‘ega16’, ‘vga’, ‘web’, or ‘user-mycustom’)

  • json_path (Path | str | None) – Optional custom data directory. If None, uses package default.

Return type:

Palette

Returns:

Palette object loaded from the specified palette file

Raises:

Example

>>> cga = load_palette("cga4")
>>> color, dist = cga.nearest_color((128, 64, 200))
>>> print(f"Nearest CGA color: {color.name}")
>>> # Load custom user palette from data/user/palettes/user-mycustom.json
>>> custom = load_palette("user-mycustom")
>>> print(f"Loaded {len(custom.records)} colors from user palette")
class color_tools.palette.Palette(records)[source]

Bases: object

CSS color palette with multiple indexing strategies for fast lookup.

Think of this as a database with multiple indexes. Want to find a color by name? O(1). By RGB? O(1). By LAB? O(1). The tradeoff is memory - we keep multiple dictionaries pointing to the same ColorRecords.

For a palette with ~150 CSS colors, this is totally fine! 🚀

Parameters:

records (List[ColorRecord])

classmethod from_rgb(rgb_colors, *, names=None, name_prefix='Color', source='custom', auto_name=False)[source]

Build a searchable palette, preserving input order and duplicates.

Explicit names must match the color count and override auto-naming. Otherwise names are Color 1, Color 2, etc. (or name_prefix). With auto_name=True, the built-in namer receives the entire RGB list for near-match uniqueness checks. Generated names are not deduplicated. Empty input creates an empty palette. Invalid RGB or name counts raise ValueError.

Example

>>> Palette.from_rgb([(255, 0, 0)], auto_name=True).records[0].name
'red'
Return type:

Palette

Parameters:
classmethod from_hex(hex_colors, *, names=None, name_prefix='Color', source='custom', auto_name=False)[source]

Build a palette from RGB/RGBA/RRGGBB/RRGGBBAA strings, with optional #.

Naming, ordering, and validation follow from_rgb(). Invalid hex raises ValueError. CSS/PNG alpha is validated then dropped.

Example

>>> Palette.from_hex(["#f00", "00ff00"]).records[1].hex
'#00FF00'
Return type:

Palette

Parameters:
classmethod load_default()[source]

Load the default CSS color palette from the package data.

This is a convenience method so you don’t have to worry about file paths - just call Palette.load_default() and go!

Return type:

Palette

save_lut_png(path)[source]

Export this palette as an N×1 RGB PNG lookup texture.

Colors retain their palette order and duplicate entries. The registered palette_lut exporter owns encoding, validation, and directory creation. The returned path is the destination supplied by the caller.

Parameters:

path (str | Path) – Destination PNG file.

Return type:

Path

Returns:

The destination as a Path.

Raises:
  • ValueError – If the palette is empty.

  • OSError – If creating directories or writing the file fails.

find_by_name(name)[source]

Find color by trimmed, case-insensitive name.

Return type:

Optional[ColorRecord]

Parameters:

name (str)

find_by_rgb(rgb)[source]

Find color by exact RGB match.

Return type:

Optional[ColorRecord]

Parameters:

rgb (Tuple[int, int, int])

find_by_hsl(hsl, rounding=2)[source]

Find color by HSL match (with rounding for fuzzy matching).

Return type:

Optional[ColorRecord]

Parameters:
find_by_lab(lab, rounding=2)[source]

Find color by LAB match (with rounding for fuzzy matching).

Return type:

Optional[ColorRecord]

Parameters:
find_by_lch(lch, rounding=2)[source]

Find color by LCH match (with rounding for fuzzy matching).

Return type:

Optional[ColorRecord]

Parameters:
nearest_color(value, space='lab', metric='de2000', *, cmc_l=2.0, cmc_c=1.0)[source]

Find nearest color by space/metric.

This is the main search function! It iterates through all colors and finds the one with minimum distance in the specified space.

Parameters:
  • value (Tuple[float, float, float]) – Color in the specified space (RGB, HSL, LAB, or LCH)

  • space (str) – Color space - ‘rgb’, ‘hsl’, ‘lab’, or ‘lch’ (default: ‘lab’)

  • metric (str) – Distance metric - ‘euclidean’, ‘de76’, ‘de94’, ‘de2000’, ‘cmc’

  • cmc_l (float) – Parameters for CMC metric (default 2:1 for acceptability)

  • cmc_c (float) – Parameters for CMC metric (default 2:1 for acceptability)

Return type:

Tuple[ColorRecord, float]

Exact distance ties prefer records from user-* sources. Remaining ties retain the records’ original order. An empty palette returns (None, inf).

Return type:

Tuple[ColorRecord, float]

Returns:

(nearest_color_record, distance) tuple

Parameters:
nearest_colors(value, space='lab', metric='de2000', count=5, *, cmc_l=2.0, cmc_c=1.0)[source]

Find the nearest N colors by space/metric.

Similar to nearest_color but returns multiple results sorted by distance.

Parameters:
  • value (Tuple[float, float, float]) – Color in the specified space (RGB, HSL, LAB, or LCH)

  • space (str) – Color space - ‘rgb’, ‘hsl’, ‘lab’, or ‘lch’ (default: ‘lab’)

  • metric (str) – Distance metric - ‘euclidean’, ‘de76’, ‘de94’, ‘de2000’, ‘cmc’

  • count (int) – Number of results to return (default: 5, max: 50)

  • cmc_l (float) – Parameters for CMC metric (default 2:1 for acceptability)

  • cmc_c (float) – Parameters for CMC metric (default 2:1 for acceptability)

Return type:

List[Tuple[ColorRecord, float]]

Exact distance ties prefer records from user-* sources. Remaining ties retain the records’ original order.

Return type:

List[Tuple[ColorRecord, float]]

Returns:

List of (color_record, distance) tuples sorted by distance (closest first)

Parameters: