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
9 changes: 6 additions & 3 deletions coloraide/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -1026,7 +1026,7 @@ def fit(

# Handle special gamut requests
if space in gamut.SPECIAL_GAMUTS:
return cast(Self, gamut.SPECIAL_GAMUTS[space]['fit'](self))
return cast(Self, gamut.SPECIAL_GAMUTS[space]['fit'](self, **kwargs))

# If within gamut, just normalize hue range by calling clip.
if self.in_gamut(space, tolerance=0):
Expand All @@ -1049,15 +1049,18 @@ def fit(
mapping.fit(self, target, **kwargs)
return self

def in_gamut(self, space: str | None = None, *, tolerance: float = util.DEF_FIT_TOLERANCE) -> bool:
def in_gamut(self, space: str | None = None, *, tolerance: float | None = None, **kwargs: Any) -> bool:
"""Check if current color is in gamut."""

if space is None:
space = self.space()

# Handle special gamut requests
if space in gamut.SPECIAL_GAMUTS:
return cast(bool, gamut.SPECIAL_GAMUTS[space]['check'](self, tolerance=tolerance))
return cast(bool, gamut.SPECIAL_GAMUTS[space]['check'](self, tolerance=tolerance, **kwargs))

if tolerance is None:
tolerance = util.DEF_FIT_TOLERANCE

# Check if gamut is in the provided space
c = self.convert(space, norm=False) if space is not None and space != self.space() else self
Expand Down
4 changes: 4 additions & 0 deletions coloraide/gamut/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
'macadam-limits': {
'check': visible_spectrum.in_macadam_limits,
'fit': visible_spectrum.fit_macadam_limits
},
'visible-spectrum': {
'check': visible_spectrum.in_visible_spectrum,
'fit': visible_spectrum.fit_visible_spectrum
}
} # type: dict[str, dict[str, Callable[..., Any]]]

Expand Down
9 changes: 6 additions & 3 deletions coloraide/gamut/pointer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from .. import algebra as alg
from .. import util
from ..types import Vector, Matrix, AnyColor, VectorLike # noqa: F401
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING: #pragma: no cover
from ..color import Color
Expand Down Expand Up @@ -144,7 +144,7 @@ def get_chroma_limit(l: float, h: float) -> float:
return alg.lerp(alg.lerp(row1[li], row1[li + 1], lf), alg.lerp(row2[li], row2[li + 1], lf), hf)


def fit_pointer_gamut(color: AnyColor) -> AnyColor:
def fit_pointer_gamut(color: AnyColor, **kwargs: Any) -> AnyColor:
"""Fit a color to the Pointer gamut."""

# Convert to CIE LCh with the SC illuminant
Expand All @@ -162,7 +162,7 @@ def fit_pointer_gamut(color: AnyColor) -> AnyColor:
return from_lch_sc(color, [new_l, new_c, h]) if adjusted else color


def in_pointer_gamut(color: Color, tolerance: float) -> bool:
def in_pointer_gamut(color: Color, tolerance: float | None = None, **kwargs: Any) -> bool:
"""
See if color is within the pointer gamut.

Expand All @@ -172,6 +172,9 @@ def in_pointer_gamut(color: Color, tolerance: float) -> bool:
color's chroma does not exceed the limit.
"""

if tolerance is None:
tolerance = util.DEF_FIT_TOLERANCE

# Convert to CIE LCh with the SC illuminant
l, c, h = to_lch_sc(color)

Expand Down
98 changes: 95 additions & 3 deletions coloraide/gamut/visible_spectrum.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""Check if color is in visible gamut."""
from __future__ import annotations
import math
import bisect
from ..cat import WHITES
from .. import algebra as alg
from .. import util
from ..types import Matrix, AnyColor # noqa: F401
from .rosch_macadam_solid import LUT, LUMINANCE, HUE
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING: #pragma: no cover
from ..color import Color
Expand Down Expand Up @@ -73,7 +74,7 @@ def get_chroma_limit(l: float, h: float) -> float:
return alg.lerp(alg.lerp(row1[li], row1[li + 1], lf), alg.lerp(row2[li], row2[li + 1], lf), hf)


def fit_macadam_limits(color: AnyColor) -> AnyColor:
def fit_macadam_limits(color: AnyColor, **kwargs: Any) -> AnyColor:
"""Fit a color to the approximation of the Macadam limits at the color's given luminance."""

# Convert to xyY
Expand All @@ -96,7 +97,7 @@ def fit_macadam_limits(color: AnyColor) -> AnyColor:
return color.update(color.new('xyz-d65', util.xy_to_xyz((x, y), new_Y), color[-1])) if adjusted else color


def in_macadam_limits(color: Color, tolerance: float) -> bool:
def in_macadam_limits(color: Color, tolerance: float | None = None, **kwargs: Any) -> bool:
"""
See if color is within the approximation of the Macadam limits for the color's luminance.

Expand All @@ -105,6 +106,9 @@ def in_macadam_limits(color: Color, tolerance: float) -> bool:
color's chroma does not exceed the limit.
"""

if tolerance is None:
tolerance = util.DEF_FIT_TOLERANCE

# Convert to xyY
xyz = (color.convert('xyz-d65', norm=False) if color.space() != 'xyz-d65' else color.normalize(nans=False))[:-1]
x, y, Y = util.xyz_to_xyY(xyz, WHITES['2deg']['D65'])
Expand Down Expand Up @@ -142,3 +146,91 @@ def macadam_limits(luminance: float | None = None) -> Matrix:
# Luminance exceeds threshold
else:
raise ValueError(f'Luminance must be between {LUMINANCE[0]} and {LUMINANCE[-1]}, but was {luminance}')


def in_visible_spectrum(
color: Color,
tolerance: float | None = None,
ignore_luminance: bool = False,
**kwargs: Any
) -> bool:
"""See if color is within the spectral locus."""

if tolerance is None:
tolerance = 1e-3

# Get white and xyY coordinates
white = color.white('xy-1931')
l = color.luminance(white=white)
xy = color.xy()

# Get the dominant wavelength which will yield the point on the spectral locus in our direction
wave, dominant = color.wavelength()[:2]

# See if we have an achromatic color
if math.isnan(wave):
oog_chroma = False
else:
# Calculate magnitude with vector normalized such that white is the origin
xy_temp = alg.subtract(xy, white, dims=alg.D1)
m1 = math.sqrt(xy_temp[0] ** 2 + xy_temp[1] ** 2)
xy_temp = alg.subtract(dominant, white, dims=alg.D1)
m2 = math.sqrt(xy_temp[0] ** 2 + xy_temp[1] ** 2)
oog_chroma = m1 > (m2 + tolerance)

oog_lum = False if ignore_luminance else (l > (1 + tolerance) or l < (0 - tolerance))

# See if we are within tolerance
return not oog_lum and not oog_chroma


def fit_visible_spectrum(
color: AnyColor,
tolerance: float = 1e-3,
ignore_luminance: bool = False,
**kwargs: Any
) -> AnyColor:
"""Fit color to the visible spectrum."""

# Get white and xyY coordinates
white = color.white('xy-1931')
l = color.luminance(white=white)
xy = color.xy()

# Get the dominant wavelength which will yield the point on the spectral locus in our direction
wave, dominant = color.wavelength()[:2]

# See if we have an achromatic color
if math.isnan(wave):
dominant = white
oog_chroma = False
else:
# Calculate magnitude with vector normalized such that white is the origin
xy_temp = alg.subtract(xy, white, dims=alg.D1)
m1 = math.sqrt(xy_temp[0] ** 2 + xy_temp[1] ** 2)
xy_temp = alg.subtract(dominant, white, dims=alg.D1)
m2 = math.sqrt(xy_temp[0] ** 2 + xy_temp[1] ** 2)

# Adjust range to pull in color relative to the spectral locus
if tolerance:
m2 += tolerance
h = math.degrees(math.atan2(xy_temp[1], xy_temp[0])) % 360
dominant = list(alg.add(alg.polar_to_rect(m2, h), white, dims=alg.D1))

# Check if color is outside the spectral locus
oog_chroma = m1 > m2

oog_lum = False if ignore_luminance else (l > 1 or l < 0)

# Adjust color is out of luminance range or outside the spectral locus limits
if oog_lum or l < 0 or oog_chroma:
color.update(
color.chromaticity(
color.space(),
[*(dominant if oog_chroma else xy), alg.clamp(l, 0, 1)],
'xy-1931',
white=white,
scale=False
)
)
return color
1 change: 1 addition & 0 deletions docs/src/markdown/about/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ icon: lucide/scroll-text

## 8.6

- **NEW**: Add new special gamut `visible-spectrum`.
- **NEW**: Add support for `carryforward` within `average()`.
- **ENHANCE**: More accurate dominant wavelength calculations.
- **FIX**: Ensure luminance calculations are all in the same white point with `scale` gamut mapping and color
Expand Down
11 changes: 6 additions & 5 deletions docs/src/markdown/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,7 @@ def average(
space: str | None = None,
out_space: str | None = None,
premultiplied: bool = True,
carryforward: bool = False,
**kwargs: Any
) -> Self:
...
Expand Down Expand Up @@ -1308,13 +1309,11 @@ Description
Parameters

-
Some methods could have additional parameters to configure the behavior, these would be done through `**kwargs`.
None of built-in gamut mapping methods currently have additional parameters.

Parameters | Defaults | Description
---------- | ------------------ | -----------
`space` | `#!py None` | The color space that the color must be mapped to. If space is `#!py None`, then the current color space will be used.
`method` | `#!py None` | String that specifies which gamut mapping method to use. If `#!py None`, `lch-chroma` will be used.
`**kwargs` | | Any supported parameters that should be passed to special gamuts.

Return

Expand All @@ -1327,7 +1326,8 @@ Return
def in_gamut(
self, space: str | None = None,
*,
tolerance: float = util.DEF_FIT_TOLERANCE
tolerance: float | None = None,
**kwargs: Any
) -> bool:
...
```
Expand All @@ -1343,7 +1343,8 @@ Parameters
Parameters | Defaults | Description
---------- | ---------------- | -----------
`space` | `#!py None` | The color space that the color must be fit within. If space is `#!py None`, then the current color space will be used.
`tolerance`| `#!py 0.000075` | Tolerance allowed when checking bounds of color.
`tolerance`| `#!py None` | Tolerance allowed when checking bounds of color. If `#!py None`, the default is used.
`**kwargs` | | Any supported parameters that should be passed to special gamuts.

Return

Expand Down
Loading
Loading