Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 138 additions & 10 deletions src/palettes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
# SPDX-FileCopyrightText: 2024 Brad Barnett
#
# SPDX-License-Identifier: MIT
"""
`palettes`
====================================================
"""Color palette toolkit for pydisplay.

Provides named color sets, RGB/HSV wheels, RGB cubes, and Material Design
swatches. Palettes map integer indices to display-ready color values at
several bit depths (4, 8, 16, or 24).

Example:
>>> from palettes import get_palette
>>> palette = get_palette(name="wheel", length=256, saturation=1.0)
>>> palette[0]
>>> palette.color_name(0)

Attributes:
WIN16: Mapping of ``0xRRGGBB`` values to Windows 16-color names. Used as
the default name table for :class:`Palette` and
:class:`~palettes.wheel.WheelPalette`.
"""

# The 16 colors of the standard Windows 16-color palette
WIN16 = {
0x000000: "Black",
0x000080: "Navy",
Expand All @@ -28,6 +40,21 @@


def get_palette(name="default", **kwargs):
"""Construct a palette by logical name.

Args:
name: Palette type. One of ``"default"`` (Windows 16-color),
``"wheel"``, ``"cube"``, or ``"material_design"``. Unknown names
fall back to :class:`Palette`.
**kwargs: Forwarded to the palette constructor (for example
``color_depth``, ``length``, ``size``, ``saturation``).

Returns:
A :class:`Palette` subclass instance.

Example:
>>> get_palette(name="cube", size=3, color_depth=16)
"""
if name == "wheel":
from .wheel import WheelPalette as MyPalette
elif name == "material_design":
Expand All @@ -40,8 +67,21 @@ def get_palette(name="default", **kwargs):


class Palette:
"""
A class to represent a color palette.
"""Indexed color palette with optional named color attributes.

Subclasses override :meth:`_get_rgb` to define how each index maps to
red, green, and blue components. :meth:`__getitem__` converts those
components to the configured ``color_depth``.

Named colors from the palette's name table (for example ``palette.RED``)
are attached as attributes during initialization.

Args:
name: Optional label stored in :attr:`name`.
color_depth: Output format for :meth:`__getitem__`: ``4`` (24-bit
index), ``8`` (RGB332), ``16`` (RGB565), or ``24`` (``0xRRGGBB``).
swapped: If ``True``, byte-swap 16-bit colors (little-endian displays).
cached: If ``True``, memoize computed index colors in an internal dict.
"""

def __init__(self, name="", color_depth=16, swapped=False, cached=False):
Expand Down Expand Up @@ -69,16 +109,32 @@ def _define_named_colors(self):

@property
def name(self):
"""Human-readable palette label."""
return self._name

def __iter__(self):
"""Yield each palette entry in index order."""
for i in range(len(self)):
yield self[i]

def __len__(self):
"""Number of colors in the palette."""
return self._length

def __getitem__(self, index):
"""Return the color at ``index`` in the configured bit depth.

Negative indices and indices beyond the palette length wrap around.

Args:
index: Color index (supports negative and out-of-range values).

Returns:
Color value as an integer (format depends on ``color_depth``).

Raises:
ValueError: If ``color_depth`` is not 4, 8, 16, or 24.
"""
index = self._normalize(index)

if self._cache is not None and index in self._cache:
Expand All @@ -101,6 +157,17 @@ def _normalize(self, index):
return index

def color565(self, r, g=None, b=None):
"""Convert RGB to a 16-bit RGB565 value.

Args:
r: Red component (0–255), a 24-bit ``0xRRGGBB`` integer, or an
``(r, g, b)`` sequence.
g: Green component when ``r`` is passed separately.
b: Blue component when ``r`` is passed separately.

Returns:
16-bit color, optionally byte-swapped when ``swapped`` is ``True``.
"""
if isinstance(r, (tuple, list)):
# r is a tuple or list
r, g, b = r
Expand All @@ -115,6 +182,17 @@ def color565(self, r, g=None, b=None):
return color

def color332(self, r, g=None, b=None):
"""Convert RGB to an 8-bit RGB332 value.

Args:
r: Red component (0–255), a 24-bit ``0xRRGGBB`` integer, or an
``(r, g, b)`` sequence.
g: Green component when ``r`` is passed separately.
b: Blue component when ``r`` is passed separately.

Returns:
8-bit RGB332 color.
"""
# Convert r, g, b to 8-bit
if isinstance(r, (tuple, list)):
# r is a tuple or list
Expand All @@ -127,8 +205,14 @@ def color332(self, r, g=None, b=None):
return color

def color_rgb(self, color):
"""
color can be an 16-bit integer or a tuple, list or bytearray of length 2 or 3.
"""Expand a packed color to an ``(r, g, b)`` tuple.

Args:
color: A 16-bit integer, or a 2- or 3-byte sequence in display
byte order.

Returns:
``(red, green, blue)`` with each component in ``0``–``255``.
"""
if isinstance(color, int):
# convert 16-bit int color to 2 bytes
Expand All @@ -142,18 +226,53 @@ def color_rgb(self, color):
return (r, g, b)

def color_name(self, index):
"""Return the name of the color at ``index``.

Args:
index: Palette index (supports wrapping).

Returns:
A name from the palette name table, or a ``"#RRGGBB"`` hex string
when no name matches.
"""
return self.rgb_name(self._get_rgb(self._normalize(index)))

def rgb_name(self, r, g=None, b=None):
"""Look up a color name from RGB components.

Args:
r: Red (0–255), a 24-bit integer, or an ``(r, g, b)`` sequence.
g: Green when ``r`` is passed separately.
b: Blue when ``r`` is passed separately.

Returns:
Matching name from :attr:`_names`, or ``"#RRGGBB"`` if unknown.
"""
if isinstance(r, (tuple, list)):
r, g, b = r
return self._names.get(r << 16 | g << 8 | b, f"#{r:02X}{g:02X}{b:02X}")

def luminance(self, index):
"""Perceived brightness of the color at ``index`` (ITU-R BT.601).

Args:
index: Palette index.

Returns:
Luminance in ``0.0``–``255.0``.
"""
r, g, b = self._get_rgb(index)
return 0.299 * r + 0.587 * g + 0.114 * b

def brightness(self, index):
"""Average channel brightness of the color at ``index``.

Args:
index: Palette index.

Returns:
Normalized brightness in ``0.0``–``1.0``.
"""
r, g, b = self._get_rgb(index)
return (r + g + b) / 3 / 255

Expand All @@ -163,8 +282,17 @@ def _get_rgb(self, index):


class MappedPalette(Palette):
"""
A class to represent a color palette with a color map.
"""Palette backed by a flat RGB byte map.

Each color occupies three consecutive bytes ``(r, g, b)`` in
``color_map``. Subclasses such as :class:`~palettes.material_design.MDPalette`
supply a pre-built map and named-color attributes.

Args:
name: Optional label stored in :attr:`name`.
color_depth: Output format for :meth:`Palette.__getitem__`.
swapped: Byte-swap 16-bit colors when ``True``.
color_map: ``bytes`` or buffer of RGB triplets, length ``3 * n_colors``.
"""

def __init__(self, name, color_depth, swapped, color_map):
Expand Down
44 changes: 23 additions & 21 deletions src/palettes/cube.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,34 @@
# SPDIX:# SPDX-FileCopyrightText: 2024 Brad Barnett
# SPDX-FileCopyrightText: 2024 Brad Barnett
#
# SPDX-License-Identifier: MIT
"""
`palettes.cube`
====================================================
Makes a color cube palette.

Usage:
from palettes import get_palette
palette = get_palette(name="cube", size=5, color_depth=16, swapped=False)
# OR
palette = get_palette(name="cube")

# OR
from palettes.cube import CubePalette
palette = CubePalette(size=5, color_depth=24)

print(f"Palette: {palette.name}, Length: {len(palette)}")
for i, color in enumerate(palette):
for i, color in enumerate(palette): print(f"{i}. {color:#06X} {palette.color_name(i)}")
"""RGB color-cube palettes.

Samples the RGB cube at evenly spaced points. Supported cube sizes are
2, 3, 4, and 5 (8, 27, 64, or 125 colors). Each size uses a built-in
name table for :meth:`~palettes.Palette.color_name`.

Example:
>>> from palettes import get_palette
>>> palette = get_palette(name="cube", size=5, color_depth=16)
>>> len(palette)
125
"""

from . import Palette as _Palette


class CubePalette(_Palette):
"""
A color cube palette. The size of the cube is determined by the size parameter.
"""Evenly spaced RGB cube palette.

Indices traverse the cube in ``x``, then ``y``, then ``z`` order. Channel
values are spaced from ``0`` to ``255`` inclusive.

Args:
name: Prefix for :attr:`~palettes.Palette.name` (length suffix is added).
color_depth: Output format; see :class:`~palettes.Palette`.
swapped: Byte-swap 16-bit colors when ``True``.
cached: Memoize index lookups when ``True`` (default).
size: Cube edge length. Must be ``2``, ``3``, ``4``, or ``5``.
"""

def __init__(self, name="", color_depth=16, swapped=False, cached=True, size=5):
Expand Down
59 changes: 24 additions & 35 deletions src/palettes/material_design.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,39 @@
# SPDX-FileCopyrightText: 2024 Brad Barnett
#
# SPDX-License-Identifier: MIT
"""
`palettes.material_design`
====================================================
This module contains the Material Design color palette as a class object.


Usage:
from palettes import get_palette
palette = get_palette(name="material_design", color_depth=16, swapped=False)
# OR
palette = get_palette("material_design")

# OR
from palettes.material_design import MDPalette
palette = MDPalette(size=5, color_depth=24)

# to access the primary variant of a color family by name:
x = palette.RED
x = palette.BLACK
"""Material Design color palette.

# to access all 256 colors directly:
x = palette[127] # color at index 127
256 swatches from the Material Design spec, exposed as indexed colors and
named attributes. Each hue family includes shades ``S50``–``S900``; most
families also provide accent colors ``A100``, ``A200``, ``A400``, and
``A700``.

# to access a shade by name:
x = palette.RED_S500 # shade 500
x = palette.RED_S900 # shade 900
x = palette.RED_S50 # shade 50

# to access an accent of a color family by name:
x = palette.RED_A100
x = palette.RED_A700

# to iterate over all 256 colors:
for x in palette:
pass
Example:
>>> from palettes import get_palette
>>> palette = get_palette(name="material_design", color_depth=16)
>>> palette.RED
>>> palette.RED_S900
>>> palette[127]
"""

from . import MappedPalette
from ._material_design import COLORS, FAMILIES, LENGTHS


class MDPalette(MappedPalette):
"""
A class to represent the Material Design color palette.
"""Material Design swatch palette.

Colors are stored in a flat RGB map. During initialization, named
attributes are created for each family and shade (for example
``RED``, ``RED_S500``, ``RED_A700``). The unsuffixed name always
refers to the ``S500`` primary shade.

Args:
name: Label for :attr:`~palettes.Palette.name`; defaults to
``"MaterialDesign"`` when empty.
color_depth: Output format; see :class:`~palettes.Palette`.
swapped: Byte-swap 16-bit colors when ``True``.
color_map: RGB byte map; defaults to the built-in Material Design table.
"""

_shades = [
Expand Down
Loading
Loading