color_tools.conversions

Color space conversion functions.

Handles conversions between: - sRGB (0-255) ↔ XYZ ↔ LAB ↔ LCH - sRGB ↔ HSL (various formats) - sRGB ↔ CMY and CMYK percentages - Gamma correction (sRGB companding)

All conversions use D65 illuminant and proper color science math.

Example

>>> from color_tools import rgb_to_lab, lab_to_lch, rgb_to_hsl
>>>
>>> # Convert vibrant orange from web color
>>> orange_rgb = (255, 128, 0)
>>>
>>> # To LAB for perceptual analysis
>>> lab = rgb_to_lab(orange_rgb)
>>> print(f"LAB: L={lab[0]:.1f} a={lab[1]:.1f} b={lab[2]:.1f}")
LAB: L=67.1 a=42.8 b=74.0
>>>
>>> # To LCH for hue/chroma work
>>> lch = lab_to_lch(lab)
>>> print(f"LCH: L={lch[0]:.1f} C={lch[1]:.1f} H={lch[2]:.1f}°")
LCH: L=67.1 C=85.5 H=59.9°
>>>
>>> # To HSL for web design
>>> hsl = rgb_to_hsl(orange_rgb)
>>> print(f"HSL: {hsl[0]:.0f}° {hsl[1]:.0f}% {hsl[2]:.0f}%")
HSL: 30° 100% 50%
color_tools.conversions.hex_to_rgb(hex_code)[source]

Converts a hex color string to an RGB tuple.

Parameters:

hex_code (str) – RGB, RGBA, RRGGBB, or RRGGBBAA, optionally prefixed with #. Short digits are expanded. CSS/PNG alpha-last order is used, not Windows/Android ARGB; alpha is validated then dropped.

Returns:

Tuple of (R, G, B) where each component is 0-255.

Return type:

Tuple[int, int, int]

Raises:

ValueError – For invalid input, whitespace, or non-ASCII hex digits.

color_tools.conversions.rgb_to_hex(rgb)[source]

Converts an RGB tuple to a hex color string.

Parameters:

rgb (Union[Tuple[int, int, int], list[int]]) – Tuple of (R, G, B) where each component is 0-255.

Return type:

str

Returns:

Hex color string in the format “#RRGGBB”.

Raises:

ValueError – Unless input is three built-in integers in 0-255.

color_tools.conversions.rgb_to_normalized(rgb)[source]

Convert strict 8-bit encoded sRGB channels to the [0.0, 1.0] range.

This scales gamma-encoded sRGB values; it does not convert them to linear light. Values are validated without coercion or clipping before scaling.

Parameters:

rgb (tuple[int, int, int] | list[int]) – Three built-in integer channels in the inclusive 0-255 range.

Return type:

tuple[float, float, float]

Returns:

Three encoded sRGB channels in the inclusive 0.0-1.0 range.

Raises:

ValueError – If rgb does not satisfy validate_rgb().

Example

>>> rgb_to_normalized((0, 128, 255))
(0.0, 0.5019607843137255, 1.0)
color_tools.conversions.rgb_to_xyz(rgb)[source]

Convert sRGB (0-255) to CIE XYZ using D65 illuminant.

XYZ is a device-independent color space that represents how the human eye responds to light. It’s the bridge between RGB and LAB.

Return type:

Tuple[float, float, float]

Parameters:

rgb (Tuple[int | float, int | float, int | float])

color_tools.conversions.xyz_to_lab(xyz)[source]

Convert CIE XYZ to L*a*b* using D65 illuminant.

LAB is perceptually uniform - equal distances in LAB space correspond to roughly equal perceived color differences. Perfect for color matching!

  • L*: Lightness (0=black, 100=white)

  • a*: Green←→Red axis

  • b*: Blue←→Yellow axis

Return type:

Tuple[float, float, float]

Parameters:

xyz (Tuple[float, float, float])

color_tools.conversions.rgb_to_lab(rgb)[source]

Convert sRGB (0-255) to CIE L*a*b*.

This is the main conversion you’ll use for color matching! Goes RGB → XYZ → LAB in one shot.

Return type:

Tuple[float, float, float]

Parameters:

rgb (Tuple[int | float, int | float, int | float])

color_tools.conversions.rgb_to_lab_array(rgb)[source]

Convert one or more sRGB colors to CIE L*a*b* using D65.

The final dimension contains the red, green, and blue channels in the 0-255 sRGB range. Any leading dimensions are preserved, so a single color with shape (3,), a palette with shape (n, 3), and an image with shape (height, width, 3) produce LAB arrays with the same respective shapes. The returned array always has the numpy.float64 dtype.

Each color is converted by rgb_to_lab(), keeping the scalar and array conversion behavior consistent. The result can be passed directly to color_tools.distance.delta_e_2000_array() for vectorized color difference calculations.

Parameters:

rgb (ArrayLike) – Array-like sRGB values with a trailing dimension of size 3.

Return type:

NDArray[double]

Returns:

A numpy.float64 array of CIE L*a*b* values with the same shape as rgb.

Raises:
  • ImportError – If NumPy is not installed.

  • ValueError – If rgb does not have a trailing dimension of size 3.

Example

>>> import numpy as np
>>> from color_tools import delta_e_2000_array, rgb_to_lab_array
>>> rgb = np.array([[255, 0, 0], [0, 255, 0]], dtype=np.uint8)
>>> lab = rgb_to_lab_array(rgb)
>>> lab.shape
(2, 3)
>>> delta_e_2000_array(lab, lab[0]).shape
(2,)
color_tools.conversions.lab_to_xyz(lab)[source]

Convert CIE L*a*b* to XYZ using D65 illuminant.

Reverses the LAB → XYZ transformation.

Return type:

Tuple[float, float, float]

Parameters:

lab (Tuple[float, float, float])

color_tools.conversions.xyz_to_rgb(xyz, clamp=True)[source]

Convert CIE XYZ to sRGB (0-255).

Parameters:
  • xyz (Tuple[float, float, float]) – XYZ color tuple

  • clamp (bool) – If True, clamp out-of-gamut values to 0-255. If False, may return rounded values outside valid range. Use xyz_to_rgb_float for precision-sensitive gamut checking.

Return type:

Tuple[int, int, int]

Returns:

RGB tuple (0-255)

color_tools.conversions.xyz_to_rgb_float(xyz)[source]

Convert D65 XYZ (0-100) to unrounded, unclipped encoded sRGB (0-255).

Channels may be outside 0-255. Use this path for gamut boundary checks; xyz_to_rgb remains the integer-returning display conversion.

Parameters:

xyz (Tuple[float, float, float]) – D65 XYZ coordinates on the 0-100 scale.

Return type:

Tuple[float, float, float]

Returns:

Encoded sRGB channels on the 0-255 scale, without rounding or clipping.

Example

>>> xyz_to_rgb_float((0.0, 0.0, 0.0))
(0.0, 0.0, 0.0)
color_tools.conversions.lab_to_rgb(lab, clamp=True)[source]

Convert CIE L*a*b* to sRGB (0-255).

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

  • clamp (bool) – If True, clamp out-of-gamut colors to valid RGB range

Return type:

Tuple[int, int, int]

Returns:

RGB tuple (0-255)

color_tools.conversions.lab_to_lch(lab)[source]

Convert L*a*b* to L*C*h° (cylindrical LAB).

LCH is more intuitive than LAB for certain operations: - L* (Lightness): Same as LAB, 0-100 - C* (Chroma): Color intensity/saturation, sqrt(a² + b²) - h° (Hue): Hue angle in degrees, 0-360

Return type:

Tuple[float, float, float]

Returns:

(L*, C*, h°) tuple

Parameters:

lab (Tuple[float, float, float])

color_tools.conversions.lch_to_lab(lch)[source]

Convert L*C*h° to L*a*b*.

Parameters:

lch (Tuple[float, float, float]) – (L*, C*, h°) tuple

Return type:

Tuple[float, float, float]

Returns:

(L*, a*, b*) tuple

color_tools.conversions.lch_to_rgb(lch, clamp=True)[source]

Convert L*C*h° directly to sRGB (0-255).

Return type:

Tuple[int, int, int]

Parameters:
color_tools.conversions.rgb_to_lch(rgb)[source]

Convert sRGB (0-255) directly to L*C*h°.

Return type:

Tuple[float, float, float]

Parameters:

rgb (Tuple[int, int, int])

color_tools.conversions.rgb_to_hsl(rgb)[source]

Convert RGB (0-255) to HSL (H: 0-360, S: 0-100, L: 0-100).

This is the standard HSL representation: - H: Hue in degrees (0° = red, 120° = green, 240° = blue) - S: Saturation as percentage (0% = gray, 100% = pure color) - L: Lightness as percentage (0% = black, 50% = pure color, 100% = white)

Return type:

Tuple[float, float, float]

Parameters:

rgb (Tuple[int, int, int])

color_tools.conversions.rgb_to_winhsl240(rgb)[source]

Convert RGB (0-255) to winHSL240 — the Windows OS variant.

Used by Windows applications including Paint, WordPad, and Win32 GDI APIs. Components are scaled to:

  • H: 0–239 (240 = 0° again, so the valid range stops at 239)

  • S: 0–240

  • L: 0–240

This is subtly different from the range you might expect: hue stops at 239, not 240, because 240 wraps back to 0° (red) and would be a duplicate.

Return type:

Tuple[int, int, int]

Parameters:

rgb (Tuple[int, int, int])

color_tools.conversions.rgb_to_winhsl255(rgb)[source]

Convert RGB (0-255) to winHSL255 — the Microsoft Office variant.

Used by Microsoft Office applications (Word, Excel, PowerPoint colour pickers). Components are scaled to:

  • H: 0–254 (255 = 0° again, so the valid range stops at 254)

  • S: 0–255

  • L: 0–255

Like winHSL240, the hue ceiling is one less than the scale factor because the maximum would alias back to 0° (red).

Return type:

Tuple[int, int, int]

Parameters:

rgb (Tuple[int, int, int])

color_tools.conversions.rgb_to_winhsl(rgb)

Convert RGB (0-255) to winHSL240 — the Windows OS variant.

Used by Windows applications including Paint, WordPad, and Win32 GDI APIs. Components are scaled to:

  • H: 0–239 (240 = 0° again, so the valid range stops at 239)

  • S: 0–240

  • L: 0–240

This is subtly different from the range you might expect: hue stops at 239, not 240, because 240 wraps back to 0° (red) and would be a duplicate.

Return type:

Tuple[int, int, int]

Parameters:

rgb (Tuple[int, int, int])

color_tools.conversions.hsl_to_rgb(hsl)[source]

Convert HSL (H: 0-360, S: 0-100, L: 0-100) to RGB (0-255).

This is the inverse of rgb_to_hsl(), converting from the standard HSL representation back to 8-bit RGB values.

Parameters:

hsl (Tuple[float, float, float]) – HSL tuple (Hue 0-360°, Saturation 0-100%, Lightness 0-100%)

Return type:

Tuple[int, int, int]

Returns:

RGB tuple (0-255 for each component)

color_tools.conversions.rgb_to_cmy(rgb)[source]

Convert RGB (0-255) to CMY (C: 0-100, M: 0-100, Y: 0-100).

CMY is the simple subtractive complement of RGB. Each channel is the percentage of ink needed to absorb the corresponding RGB primary:

  • C = 100 × (1 - R/255) (cyan absorbs red)

  • M = 100 × (1 - G/255) (magenta absorbs green)

  • Y = 100 × (1 - B/255) (yellow absorbs blue)

Unlike CMYK, CMY has no black channel; pure black requires C=M=Y=100. CMY is useful for simple subtractive-model analysis and as the intermediate step when computing CMYK.

Parameters:

rgb (Tuple[int, int, int]) – RGB tuple (0-255 for each component)

Return type:

Tuple[float, float, float]

Returns:

CMY tuple (Cyan 0-100%, Magenta 0-100%, Yellow 0-100%)

Example

>>> rgb_to_cmy((255, 0, 0))       # pure red
(0.0, 100.0, 100.0)
>>> rgb_to_cmy((0, 0, 0))         # black
(100.0, 100.0, 100.0)
>>> rgb_to_cmy((255, 255, 255))   # white
(0.0, 0.0, 0.0)
color_tools.conversions.cmy_to_rgb(cmy)[source]

Convert CMY (C: 0-100, M: 0-100, Y: 0-100) to RGB (0-255).

Inverts the CMY model: each RGB channel is the fraction of light not absorbed by the corresponding ink:

  • R = 255 × (1 - C/100)

  • G = 255 × (1 - M/100)

  • B = 255 × (1 - Y/100)

Parameters:

cmy (tuple[int | float, int | float, int | float] | list[int | float]) – CMY tuple (Cyan 0-100%, Magenta 0-100%, Yellow 0-100%)

Return type:

Tuple[int, int, int]

Returns:

RGB tuple (0-255 for each component)

Raises:

ValueError – Unless input contains three finite numeric percentages in the inclusive 0-100 range. Values are not clipped.

Example

>>> cmy_to_rgb((0.0, 100.0, 100.0))   # cyan=0, full magenta+yellow → red
(255, 0, 0)
>>> cmy_to_rgb((100.0, 100.0, 100.0)) # full ink → black
(0, 0, 0)
>>> cmy_to_rgb((0.0, 0.0, 0.0))       # no ink → white
(255, 255, 255)
color_tools.conversions.rgb_to_cmyk(rgb)[source]

Convert RGB (0-255) to CMYK (C: 0-100, M: 0-100, Y: 0-100, K: 0-100).

CMYK is the standard four-channel subtractive color model used in print. The black (K) channel is extracted from the CMY values so that dark colors use less ink overall and produce a richer, more accurate black:

  • K = 100 × (1 - max(R, G, B) / 255)

  • C = 100 × (1 - R/255 - K/100) / (1 - K/100) [0 if K = 100]

  • M = 100 × (1 - G/255 - K/100) / (1 - K/100)

  • Y = 100 × (1 - B/255 - K/100) / (1 - K/100)

Pure black (0, 0, 0) maps to C=0, M=0, Y=0, K=100 rather than C=100, M=100, Y=100, K=0, which is the key advantage over plain CMY.

Parameters:

rgb (Tuple[int, int, int]) – RGB tuple (0-255 for each component)

Return type:

Tuple[float, float, float, float]

Returns:

CMYK tuple (Cyan 0-100%, Magenta 0-100%, Yellow 0-100%, Key/Black 0-100%)

Example

>>> rgb_to_cmyk((255, 0, 0))       # pure red
(0.0, 100.0, 100.0, 0.0)
>>> rgb_to_cmyk((0, 0, 0))         # black → K=100, C=M=Y=0
(0.0, 0.0, 0.0, 100.0)
>>> rgb_to_cmyk((255, 255, 255))   # white → all zero
(0.0, 0.0, 0.0, 0.0)
>>> rgb_to_cmyk((128, 64, 192))
(33.3333, 66.6667, 0.0, 24.7059)
color_tools.conversions.cmyk_to_rgb(cmyk)[source]

Convert CMYK (C: 0-100, M: 0-100, Y: 0-100, K: 0-100) to RGB (0-255).

Inverts the CMYK model. The K channel reduces the effective lightness before the CMY channels are applied:

  • R = 255 × (1 - C/100) × (1 - K/100)

  • G = 255 × (1 - M/100) × (1 - K/100)

  • B = 255 × (1 - Y/100) × (1 - K/100)

Parameters:

cmyk (tuple[int | float, int | float, int | float, int | float] | list[int | float]) – CMYK tuple (Cyan 0-100%, Magenta 0-100%, Yellow 0-100%, Key/Black 0-100%)

Return type:

Tuple[int, int, int]

Returns:

RGB tuple (0-255 for each component)

Raises:

ValueError – Unless input contains four finite numeric percentages in the inclusive 0-100 range. Values are not clipped.

Example

>>> cmyk_to_rgb((0.0, 100.0, 100.0, 0.0))   # red
(255, 0, 0)
>>> cmyk_to_rgb((0.0, 0.0, 0.0, 100.0))     # black via K
(0, 0, 0)
>>> cmyk_to_rgb((0.0, 0.0, 0.0, 0.0))       # white
(255, 255, 255)