Skip to content

Commit

Permalink
Allow controlling numerical precision when access values directly (#433)
Browse files Browse the repository at this point in the history
Additionally, allow controlling alpha precision separately when
controlling alpha and color coordinate output, such as when serializing
or exporting to dictionary.
  • Loading branch information
facelessuser authored Aug 28, 2024
1 parent b2cf46e commit efeb868
Show file tree
Hide file tree
Showing 16 changed files with 169 additions and 22 deletions.
2 changes: 1 addition & 1 deletion coloraide/algebra.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def round_to(f: float, p: int = 0, half_up: bool = True) -> float:
return _round(f)

# Ignore infinity
if math.isinf(f):
if not math.isfinite(f):
return f

# Round to the specified precision
Expand Down
58 changes: 41 additions & 17 deletions coloraide/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,10 +526,24 @@ def cct(self, *, method: str | None = None, **kwargs: Any) -> Vector:
cct = temperature.cct(method, self)
return cct.to_cct(self, **kwargs)

def to_dict(self, *, nans: bool = True) -> Mapping[str, Any]:
def to_dict(
self,
*,
nans: bool = True,
precision: int | None = None,
precision_alpha: int | None = None
) -> Mapping[str, Any]:
"""Return color as a data object."""

return {'space': self.space(), 'coords': self.coords(nans=nans), 'alpha': self.alpha(nans=nans)}
# Assume precision of other coordinates for alpha if only precision is specified
if precision is not None and precision_alpha is None:
precision_alpha = precision

return {
'space': self.space(),
'coords': self.coords(nans=nans, precision=precision),
'alpha': self.alpha(nans=nans, precision=precision_alpha)
}

def normalize(self, *, nans: bool = True) -> Color:
"""Normalize the color."""
Expand Down Expand Up @@ -1258,14 +1272,19 @@ def contrast(self, color: ColorInput, method: str | None = None) -> float:
return contrast.contrast(method, self, color)

@overload
def get(self, name: str, *, nans: bool = True) -> float:
def get(self, name: str, *, nans: bool = True, precision: int | None = None) -> float:
...

@overload
def get(self, name: list[str] | tuple[str, ...], *, nans: bool = True) -> Vector:
def get(self, name: list[str] | tuple[str, ...], *, nans: bool = True, precision: int | None = None) -> Vector:
...

def get(self, name: str | list[str] | tuple[str, ...], *, nans: bool = True) -> float | Vector:
def get(
self, name: str | list[str] | tuple[str, ...],
*,
nans: bool = True,
precision: int | None = None
) -> float | Vector:
"""Get channel."""

# Handle single channel
Expand All @@ -1274,16 +1293,17 @@ def get(self, name: str | list[str] | tuple[str, ...], *, nans: bool = True) ->
if '.' in name:
space, channel = name.split('.', 1)
if nans:
return self.convert(space)[channel]
v = self.convert(space)[channel]
else:
obj = self.convert(space, norm=nans)
i = obj._space.get_channel_index(channel)
return obj._space.resolve_channel(i, obj._coords)
v = obj._space.resolve_channel(i, obj._coords)
elif nans:
return self[name]
v = self[name]
else:
i = self._space.get_channel_index(name)
return self._space.resolve_channel(i, self._coords)
v = self._space.resolve_channel(i, self._coords)
return v if precision is None else alg.round_to(v, precision)

# Handle list of channels
else:
Expand All @@ -1298,10 +1318,12 @@ def get(self, name: str | list[str] | tuple[str, ...], *, nans: bool = True) ->
obj = self if space == original_space else self.convert(space, norm=nans)
current_space = space
if nans:
values.append(obj[channel])
v = obj[channel]
values.append(v if precision is None else alg.round_to(v, precision))
else:
i = obj._space.get_channel_index(channel)
values.append(obj._space.resolve_channel(i, obj._coords))
v = obj._space.resolve_channel(i, obj._coords)
values.append(v if precision is None else alg.round_to(v, precision))
return values

def set( # noqa: A003
Expand Down Expand Up @@ -1363,21 +1385,23 @@ def set( # noqa: A003

return self

def coords(self, *, nans: bool = True) -> Vector:
def coords(self, *, nans: bool = True, precision: int | None = None) -> Vector:
"""Get the color channels and optionally remove undefined values."""

if nans:
return self[:-1]
value = self[:-1]
else:
return [self._space.resolve_channel(index, self._coords) for index in range(len(self._coords) - 1)]
value = [self._space.resolve_channel(index, self._coords) for index in range(len(self._coords) - 1)]
return value if precision is None else [alg.round_to(v, precision) for v in value]

def alpha(self, *, nans: bool = True) -> float:
def alpha(self, *, nans: bool = True, precision: int | None = None) -> float:
"""Get the alpha channel."""

if nans:
return self[-1]
value = self[-1]
else:
return self._space.resolve_channel(-1, self._coords)
value = self._space.resolve_channel(-1, self._coords)
return value if precision is None else alg.round_to(value, precision)


Color.register(
Expand Down
11 changes: 8 additions & 3 deletions coloraide/css/serialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def color_function(
func: str | None,
alpha: bool | None,
precision: int,
precision_alpha: int,
fit: str | bool | dict[str, Any],
none: bool,
percent: bool | Sequence[bool],
Expand Down Expand Up @@ -93,7 +94,7 @@ def color_function(
string.append(
util.fmt_float(
value,
precision,
precision_alpha if is_last else precision,
span,
offset
)
Expand Down Expand Up @@ -178,6 +179,7 @@ def serialize_css(
color: bool = False,
alpha: bool | None = None,
precision: int | None = None,
precision_alpha: int | None = None,
fit: bool | str | dict[str, Any] = True,
none: bool = False,
percent: bool | Sequence[bool] = False,
Expand All @@ -193,9 +195,12 @@ def serialize_css(
if precision is None:
precision = obj.PRECISION

if precision_alpha is None:
precision_alpha = precision

# Color format
if color:
return color_function(obj, None, alpha, precision, fit, none, percent, False, 1.0)
return color_function(obj, None, alpha, precision, precision_alpha, fit, none, percent, False, 1.0)

# CSS color names
if name:
Expand All @@ -209,6 +214,6 @@ def serialize_css(

# Normal CSS named function format
if func:
return color_function(obj, func, alpha, precision, fit, none, percent, legacy, scale)
return color_function(obj, func, alpha, precision, precision_alpha, fit, none, percent, legacy, scale)

raise RuntimeError('Could not identify a CSS format to serialize to') # pragma: no cover
2 changes: 2 additions & 0 deletions coloraide/spaces/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ def to_string(
*,
alpha: bool | None = None,
precision: int | None = None,
precision_alpha: int | None = None,
fit: str | bool | dict[str, Any] = True,
none: bool = False,
percent: bool | Sequence[bool] = False,
Expand All @@ -271,6 +272,7 @@ def to_string(
color=True,
alpha=alpha,
precision=precision,
precision_alpha=precision_alpha,
fit=fit,
none=none,
percent=percent
Expand Down
2 changes: 2 additions & 0 deletions coloraide/spaces/hsl/css.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def to_string(
*,
alpha: bool | None = None,
precision: int | None = None,
precision_alpha: int | None = None,
fit: bool | str | dict[str, Any] = True,
none: bool = False,
color: bool = False,
Expand All @@ -44,6 +45,7 @@ def to_string(
func='hsl',
alpha=alpha,
precision=precision,
precision_alpha=precision_alpha,
fit=fit,
none=none,
color=color,
Expand Down
2 changes: 2 additions & 0 deletions coloraide/spaces/hwb/css.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def to_string(
*,
alpha: bool | None = None,
precision: int | None = None,
precision_alpha: int | None = None,
fit: bool | str | dict[str, Any] = True,
none: bool = False,
color: bool = False,
Expand All @@ -35,6 +36,7 @@ def to_string(
func='hwb',
alpha=alpha,
precision=precision,
precision_alpha=precision_alpha,
fit=fit,
none=none,
color=color,
Expand Down
2 changes: 2 additions & 0 deletions coloraide/spaces/lab/css.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def to_string(
*,
alpha: bool | None = None,
precision: int | None = None,
precision_alpha: int | None = None,
fit: bool | str | dict[str, Any] = True,
none: bool = False,
color: bool = False,
Expand All @@ -32,6 +33,7 @@ def to_string(
func='lab',
alpha=alpha,
precision=precision,
precision_alpha=precision_alpha,
fit=fit,
none=none,
color=color,
Expand Down
2 changes: 2 additions & 0 deletions coloraide/spaces/lch/css.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def to_string(
*,
alpha: bool | None = None,
precision: int | None = None,
precision_alpha: int | None = None,
fit: bool | str | dict[str, Any] = True,
none: bool = False,
color: bool = False,
Expand All @@ -32,6 +33,7 @@ def to_string(
func='lch',
alpha=alpha,
precision=precision,
precision_alpha=precision_alpha,
fit=fit,
none=none,
color=color,
Expand Down
2 changes: 2 additions & 0 deletions coloraide/spaces/oklab/css.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def to_string(
*,
alpha: bool | None = None,
precision: int | None = None,
precision_alpha: int | None = None,
fit: bool | str | dict[str, Any] = True,
none: bool = False,
color: bool = False,
Expand All @@ -32,6 +33,7 @@ def to_string(
func='oklab',
alpha=alpha,
precision=precision,
precision_alpha=precision_alpha,
fit=fit,
none=none,
color=color,
Expand Down
2 changes: 2 additions & 0 deletions coloraide/spaces/oklch/css.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def to_string(
*,
alpha: bool | None = None,
precision: int | None = None,
precision_alpha: int | None = None,
fit: bool | str | dict[str, Any] = True,
none: bool = False,
color: bool = False,
Expand All @@ -32,6 +33,7 @@ def to_string(
func='oklch',
alpha=alpha,
precision=precision,
precision_alpha=precision_alpha,
fit=fit,
none=none,
color=color,
Expand Down
2 changes: 2 additions & 0 deletions coloraide/spaces/srgb/css.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def to_string(
*,
alpha: bool | None = None,
precision: int | None = None,
precision_alpha: int | None = None,
fit: bool | str | dict[str, Any] = True,
none: bool = False,
color: bool = False,
Expand All @@ -37,6 +38,7 @@ def to_string(
func='rgb',
alpha=alpha,
precision=precision,
precision_alpha=precision_alpha,
fit=fit,
none=none,
color=color,
Expand Down
5 changes: 5 additions & 0 deletions docs/src/markdown/about/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

- **NEW**: Officially support Python 3.13.
- **NEW**: Define HTML output representation for Jupyter via `_repr_html_`.
- **NEW**: `get()`, `coords()`, `alpha()`, `to_dict()` can now return channel values with a specified precision via
the new `precision` parameter.
- **NEW**: `to_string()` and `to_dict()` now have an optional `precision_alpha` parameter to control the precision of
alpha channel independently from other channels. If only `precision` is specified, the alpha channel will assume the
same precision as all other channels.
- **NEW**: Remove deprecated `model` parameter from `cam16` ∆E method. Space should be used instead.
- **NEW**: Remove deprecated `algebra.npow` function. `algebra.spow` should be used instead.
- **NEW**: New generic `minde-chroma` gamut mapping method that allows specifying any Lab-ish or LCh-ish to operate
Expand Down
22 changes: 21 additions & 1 deletion docs/src/markdown/color.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,31 @@ Dictionaries must define the `space` key and the `coords` key containing values
`alpha` channel is kept separate and can be omitted, and if so, will be assumed as 1.

```py play
d = Color('red').to_dict()
d = Color('purple').to_dict()
print(d)
Color(d)
```

You can also control the precision of the output values with the `precision` parameter.

```py play
d = Color('purple').to_dict(precision=3)
print(d)
Color(d)
```

If you need to control alpha precision separately, you can also specify the alpha channels precision separately with
`precision_alpha`. This can be useful if you have radically different scaling between alpha and color coordinates.

```py play
d = Color('purple').set('alpha', 0.75).convert('lab').to_dict(precision=0, precision_alpha=3)
print(d)
Color(d)
```

/// new | New in 4.0: Precision Output Control
///

### String Inputs

By default, ColorAide accepts input strings as outlined in the CSS color specification. Accepted syntax includes legacy
Expand Down
24 changes: 24 additions & 0 deletions docs/src/markdown/manipulation.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ color.coords()
color.coords(nans=False)
```

You can control the precision of output values in either `coords()` or `alpha()` with the `precision` parameter.

```py play
color = Color("hsl", [NaN, 0, 0.7534848], 0.523456)
color
color.coords(precision=2)
color.alpha(precision=1)
```

/// new | New in 4.0: Precision Output Control
///

### Access By Functions

Colors can also be accessed and modified in more advanced ways with special access functions `get()` and `set()`.
Expand Down Expand Up @@ -192,6 +204,15 @@ color.set(
)
```

Lastly, you can control the precision of your output values with the `precision` parameter.

```py play
color = Color("hsl", [NaN, 0, 0.7534848], 0.523456)
color
color.get('lightness', precision=2)
color.get('alpha', precision=1)
```

/// warning | Indirect Channel Modifications
Indirect channel modification is very useful, but keep in mind that it may give you access to color spaces that are
incompatible due to gamut size. Additionally, the feature converts the color to the target color space, modifies it,
Expand All @@ -201,6 +222,9 @@ and then converts it back making it susceptible to any possible [round trip erro
/// new | New in 1.5: Getting/Setting Multiple Channels
///

/// new | New in 4.0: Precision Output Control
///

## Undefined Values

Colors can sometimes have undefined channels. This can actually happen in a number of ways. In almost all cases,
Expand Down
Loading

0 comments on commit efeb868

Please sign in to comment.