color_tools.gamut

Gamut checking and mapping for sRGB color space.

Not all LAB colors can be represented in sRGB! Some theoretical colors that humans can perceive simply can’t be displayed on a monitor. This module helps you: 1. Check if a LAB color is displayable (in gamut) 2. Find the nearest displayable color if it’s not

The key insight: sRGB is a cube (R, G, B all 0-255), but LAB describes a much larger volume. The intersection is the “sRGB gamut.”

color_tools.gamut.is_in_srgb_gamut(lab, tolerance=None)[source]

Check if a LAB color can be represented in sRGB without clipping.

This is the fundamental question of color reproduction: “Can my monitor actually show this color, or will it have to fake it by clamping?”

How it works: Convert LAB → RGB without clamping. If all RGB components are naturally in the 0-255 range (with a small tolerance), the color is in gamut!

Why tolerance? Floating point math is imprecise. A value of 255.0000001 should be considered “in gamut” even though it’s technically over 255.

Examples of out-of-gamut colors: - Super saturated colors at mid-lightness (like “laser red”) - Colors that would need negative RGB values - Colors that would need RGB values > 255

Parameters:
  • lab (Tuple[float, float, float]) – L*a*b* color tuple

  • tolerance (float | None) – How close to 0/255 boundaries before considering out-of-gamut. If None, uses the current context value (default 0.01).

Return type:

bool

Returns:

True if the color can be represented in sRGB, False if it would clip

Raises:
  • ValueError – If LAB is malformed, non-finite, or exceeds the supported conversion range, or tolerance is negative/non-finite.

  • TypeError – If tolerance is not numeric or is a boolean.

Example

>>> is_in_srgb_gamut((50, 0, 0))  # Mid gray
True
>>> is_in_srgb_gamut((50, 150, 100))  # Super saturated - impossible!
False
color_tools.gamut.find_nearest_in_gamut(lab, max_iterations=None)[source]

Reduce chroma to find an in-gamut LAB color at constant lightness and hue.

This is not a globally nearest-color optimization. Invalid coordinates or lightness with no feasible neutral endpoint raise ValueError.

The Strategy: When a color is out of gamut, it’s usually because it’s too saturated (high chroma). So we convert to LCH, then use binary search to find the maximum chroma that still fits in sRGB. This preserves: - ✅ Lightness (L) - stays the same - ✅ Hue (h) - stays the same - ❌ Chroma (C) - reduced until it fits

Why this works: Think of it like turning down the “saturation” slider in Photoshop until the color becomes displayable. The hue doesn’t change (red stays red), it just becomes less “punchy.”

Binary search magic: Instead of trying every possible chroma value (0.0, 0.01, 0.02, …), we do a binary search. Each iteration cuts the search space in half. After 20 iterations, we’ve checked 2^20 = 1 million possible values! 🚀

Parameters:
  • lab (Tuple[float, float, float]) – Potentially out-of-gamut L*a*b* color

  • max_iterations (int | None) – Maximum desaturation steps (binary search iterations). If None, uses current context value (default 20).

Return type:

Tuple[float, float, float]

Returns:

Desaturated in-gamut L*a*b* at the same LCH lightness and hue.

Raises:
  • ValueError – If LAB is invalid, iterations is less than one, or no in-gamut neutral exists at the requested lightness.

  • TypeError – If iterations is not an integer or is a boolean.

Example

>>> # Super saturated red - can't be displayed!
>>> out_of_gamut = (50, 100, 80)
>>> displayable = find_nearest_in_gamut(out_of_gamut)
>>> # Result: same lightness and hue, but less saturated
>>> is_in_srgb_gamut(displayable)
True
color_tools.gamut.clamp_to_gamut(lab)[source]

Convert LAB to RGB and back to LAB (with clamping).

This is the “quick and dirty” approach to gamut mapping. Instead of carefully desaturating, we just let lab_to_rgb() clamp the RGB values to 0-255, then convert back to LAB to see what we got.

Pros: Fast!

Cons: Can change hue (not just chroma). A pure red might become a slightly orange-red after clamping.

For most applications, find_nearest_in_gamut() is better because it preserves hue. This function exists for when you need speed over accuracy.

Parameters:

lab (Tuple[float, float, float]) – Any L*a*b* color (in-gamut or not)

Return type:

Tuple[float, float, float]

Returns:

In-gamut L*a*b* color (clamped)