Skip to content

Commit

Permalink
More flexible percentage and precision fix (#370)
Browse files Browse the repository at this point in the history
* More flexible percentage and precision fix

* Bump version

* Add tests

* Simplify some logic
  • Loading branch information
facelessuser authored Oct 19, 2023
1 parent fbfa429 commit dfe1356
Show file tree
Hide file tree
Showing 20 changed files with 171 additions and 88 deletions.
2 changes: 1 addition & 1 deletion coloraide/__meta__.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,5 +193,5 @@ def parse_version(ver: str) -> Version:
return Version(major, minor, micro, release, pre, post, dev)


__version_info__ = Version(2, 11, 0, "final")
__version_info__ = Version(2, 12, 0, "final")
__version__ = __version_info__._get_canonical()
96 changes: 50 additions & 46 deletions coloraide/css/serialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from .color_names import to_name
from ..channels import FLG_PERCENT, FLG_OPT_PERCENT, FLG_ANGLE
from ..types import Vector
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Sequence

if TYPE_CHECKING: # pragma: no cover
from ..color import Color
Expand All @@ -33,72 +33,76 @@ def named_color(
return to_name(get_coords(obj, fit, False, False) + [a])


def named_color_function(
def color_function(
obj: 'Color',
func: str,
func: str | None,
alpha: bool | None,
precision: int,
fit: str | bool,
none: bool,
percent: bool,
percent: bool | Sequence[bool],
legacy: bool,
scale: float
) -> str:
"""Translate to CSS function form `name(...)`."""

# Create the function `name` or `namea` if old legacy form.
# Prepare coordinates to be serialized
a = get_alpha(obj, alpha, none, legacy)
string = ['{}{}('.format(func, 'a' if legacy and a is not None else EMPTY)]

# Iterate the coordinates formatting them for percent, not percent, and even scaling them (sRGB).
coords = get_coords(obj, fit, none, legacy)
channels = obj._space.CHANNELS
if a is not None:
coords.append(a)

# `color` should include the color space serialized name.
if func is None:
string = ['color({} '.format(obj._space._serialize()[0])]
# Create the function `name` or `namea` if old legacy form.
else:
string = ['{}{}('.format(func, 'a' if legacy and a is not None else EMPTY)]

# Get channel object and calculate length and the alpha index (last)
channels = obj._space.channels
l = len(channels)
last = l - 1

# Ensure percent is configured
# - `True` assumes all but alpha are attempted to be formatted as percents.
# - A list of booleans will attempt formatting the associated channel as percent,
# anything not specified is assumed `False`.
if isinstance(percent, bool):
plist = obj._space._percents if percent else []
else:
diff = l - len(percent)
plist = list(percent) + ([False] * diff) if diff > 0 else list(percent)

# Iterate the coordinates formatting them by scaling the values, formatting for percent, etc.
for idx, value in enumerate(coords):
channel = channels[idx]
use_percent = channel.flags & FLG_PERCENT or (percent and channel.flags & FLG_OPT_PERCENT)
is_angle = channel.flags & FLG_ANGLE
if not use_percent and not is_angle:
value *= scale
if idx != 0:
is_last = idx == last
if is_last:
string.append(COMMA if legacy else SLASH)
elif idx != 0:
string.append(COMMA if legacy else SPACE)
channel = channels[idx]

if channel.flags & FLG_PERCENT or (plist and plist[idx] and channel.flags & FLG_OPT_PERCENT):
span, offset = channel.span, channel.offset
else:
span = offset = 0.0
if not channel.flags & FLG_ANGLE and not is_last:
value *= scale

string.append(
util.fmt_float(
value,
precision,
channel.span if use_percent else 0.0,
channel.offset if use_percent else 0.0
span,
offset
)
)

# Add alpha if needed
if a is not None:
string.append('{}{})'.format(COMMA if legacy else SLASH, util.fmt_float(a, max(precision, util.DEF_PREC))))
else:
string.append(')')
string.append(')')
return EMPTY.join(string)


def color_function(
obj: 'Color',
alpha: bool | None,
precision: int,
fit: str | bool,
none: bool
) -> str:
"""Color format."""

# Export in the `color(space ...)` format
coords = get_coords(obj, fit, none, False)
a = get_alpha(obj, alpha, none, False)
return (
'color({} {}{})'.format(
obj._space._serialize()[0],
SPACE.join([util.fmt_float(coord, precision) for coord in coords]),
SLASH + util.fmt_float(a, max(precision, util.DEF_PREC)) if a is not None else EMPTY
)
)


def get_coords(
obj: 'Color',
fit: str | bool,
Expand Down Expand Up @@ -168,7 +172,7 @@ def serialize_css(
precision: int | None = None,
fit: str | bool = True,
none: bool = False,
percent: bool = False,
percent: bool | Sequence[bool] = False,
hexa: bool = False,
upper: bool = False,
compress: bool = False,
Expand All @@ -183,7 +187,7 @@ def serialize_css(

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

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

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

raise RuntimeError('Could not identify a CSS format to serialize to') # pragma: no cover
11 changes: 7 additions & 4 deletions coloraide/spaces/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""Color base."""
from __future__ import annotations
from abc import ABCMeta, abstractmethod
from ..channels import Channel
from ..channels import Channel, FLG_OPT_PERCENT
from ..css import serialize
from ..deprecate import deprecated
from ..types import VectorLike, Vector, Plugin
from typing import Any, TYPE_CHECKING
from typing import Any, TYPE_CHECKING, Sequence
import math

if TYPE_CHECKING: # pragma: no cover
Expand Down Expand Up @@ -138,7 +138,7 @@ def indexes(self) -> list[int]:
return [self.get_channel_index(name) for name in self.names()] # type: ignore[attr-defined]


alpha_channel = Channel('alpha', 0.0, 1.0, bound=True, limit=(0.0, 1.0))
alpha_channel = Channel('alpha', 0.0, 1.0, bound=True, limit=(0.0, 1.0), flags=FLG_OPT_PERCENT)


class SpaceMeta(ABCMeta):
Expand Down Expand Up @@ -190,6 +190,7 @@ def __init__(self, **kwargs: Any) -> None:

self.channels = self.CHANNELS + (alpha_channel,)
self._color_ids = (self.NAME,) if not self.SERIALIZE else self.SERIALIZE
self._percents = ([True] * (len(self.channels) - 1)) + [False]

def get_channel_index(self, name: str) -> int:
"""Get channel index."""
Expand Down Expand Up @@ -234,6 +235,7 @@ def to_string(
precision: int | None = None,
fit: bool | str = True,
none: bool = False,
percent: bool | Sequence[bool] = False,
**kwargs: Any
) -> str:
"""Convert to CSS 'color' string: `color(space coords+ / alpha)`."""
Expand All @@ -244,7 +246,8 @@ def to_string(
alpha=alpha,
precision=precision,
fit=fit,
none=none
none=none,
percent=percent
)

def match(
Expand Down
17 changes: 14 additions & 3 deletions coloraide/spaces/hsl/css.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ...css import parse
from ...css import serialize
from ...types import Vector
from typing import Any, TYPE_CHECKING
from typing import Any, TYPE_CHECKING, Sequence

if TYPE_CHECKING: # pragma: no cover
from ...color import Color
Expand All @@ -22,12 +22,23 @@ def to_string(
fit: str | bool = True,
none: bool = False,
color: bool = False,
percent: bool = True,
percent: bool | Sequence[bool] | None = None,
comma: bool = False,
**kwargs: Any
) -> str:
"""Convert to CSS."""

if percent is None:
if not color:
percent = True
else:
percent = False
elif isinstance(percent, bool):
if comma:
percent = True
elif comma:
percent = [False, True, True] + list(percent[3:4])

return serialize.serialize_css(
parent,
func='hsl',
Expand All @@ -37,7 +48,7 @@ def to_string(
none=none,
color=color,
legacy=comma,
percent=True if comma else percent,
percent=percent,
scale=100
)

Expand Down
7 changes: 5 additions & 2 deletions coloraide/spaces/hwb/css.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ...css import parse
from ...css import serialize
from ...types import Vector
from typing import Any, TYPE_CHECKING
from typing import Any, TYPE_CHECKING, Sequence

if TYPE_CHECKING: # pragma: no cover
from ...color import Color
Expand All @@ -22,11 +22,14 @@ def to_string(
fit: str | bool = True,
none: bool = False,
color: bool = False,
percent: bool = True,
percent: bool | Sequence[bool] | None = None,
**kwargs: Any
) -> str:
"""Convert to CSS."""

if percent is None:
percent = False if color else True

return serialize.serialize_css(
parent,
func='hwb',
Expand Down
4 changes: 2 additions & 2 deletions coloraide/spaces/lab/css.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ...css import parse
from ...css import serialize
from ...types import Vector
from typing import Any, TYPE_CHECKING
from typing import Any, TYPE_CHECKING, Sequence

if TYPE_CHECKING: # pragma: no cover
from ...color import Color
Expand All @@ -22,7 +22,7 @@ def to_string(
fit: str | bool = True,
none: bool = False,
color: bool = False,
percent: bool = False,
percent: bool | Sequence[bool] = False,
**kwargs: Any
) -> str:
"""Convert to CSS."""
Expand Down
4 changes: 2 additions & 2 deletions coloraide/spaces/lch/css.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ...css import parse
from ...css import serialize
from ...types import Vector
from typing import Any, TYPE_CHECKING
from typing import Any, TYPE_CHECKING, Sequence

if TYPE_CHECKING: # pragma: no cover
from ...color import Color
Expand All @@ -22,7 +22,7 @@ def to_string(
fit: str | bool = True,
none: bool = False,
color: bool = False,
percent: bool = False,
percent: bool | Sequence[bool] = False,
**kwargs: Any
) -> str:
"""Convert to CSS."""
Expand Down
4 changes: 2 additions & 2 deletions coloraide/spaces/oklab/css.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ...css import parse
from ...css import serialize
from ...types import Vector
from typing import Any, TYPE_CHECKING
from typing import Any, TYPE_CHECKING, Sequence

if TYPE_CHECKING: # pragma: no cover
from ...color import Color
Expand All @@ -22,7 +22,7 @@ def to_string(
fit: str | bool = True,
none: bool = False,
color: bool = False,
percent: bool = False,
percent: bool | Sequence[bool] = False,
**kwargs: Any
) -> str:
"""Convert to CSS."""
Expand Down
4 changes: 2 additions & 2 deletions coloraide/spaces/oklch/css.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ...css import parse
from ...css import serialize
from ...types import Vector
from typing import Any, TYPE_CHECKING
from typing import Any, TYPE_CHECKING, Sequence

if TYPE_CHECKING: # pragma: no cover
from ...color import Color
Expand All @@ -22,7 +22,7 @@ def to_string(
fit: str | bool = True,
none: bool = False,
color: bool = False,
percent: bool = False,
percent: bool | Sequence[bool] = False,
**kwargs: Any
) -> str:
"""Convert to CSS."""
Expand Down
4 changes: 2 additions & 2 deletions coloraide/spaces/srgb/css.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from .. import srgb as base
from ...css import parse
from ...css import serialize
from typing import Any, Tuple, TYPE_CHECKING
from typing import Any, Tuple, TYPE_CHECKING, Sequence
from ...types import Vector

if TYPE_CHECKING: # pragma: no cover
Expand All @@ -26,7 +26,7 @@ def to_string(
names: bool = False,
comma: bool = False,
upper: bool = False,
percent: bool = False,
percent: bool | Sequence[bool] = False,
compress: bool = False,
**kwargs: Any
) -> str:
Expand Down
1 change: 1 addition & 0 deletions docs/src/dictionary/en-custom.txt
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ barycentric
bezier
bilinear
boolean
booleans
broadcasted
bz
cd
Expand Down
8 changes: 8 additions & 0 deletions docs/src/markdown/about/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 2.12

- **NEW**: When serializing, `percent` can now take a sequence of booleans to indicate which channels are desired to
be represented as a percentage, alpha included.
- **NEW**: `color()` serializing now supports string output with `percent`.
- **FIX**: When serializing, the alpha channel is no longer handled special with a minimum value of `5`. Precision is
equally applied to all channels.

## 2.11

- **NEW**: Add new `css-linear` interpolator that provides compatibility with the CSS specification. This deviates
Expand Down
2 changes: 1 addition & 1 deletion docs/src/markdown/demos/3d_models.html
Original file line number Diff line number Diff line change
Expand Up @@ -742,7 +742,7 @@ <h1>ColorAide Color Space Models</h1>
let colorSpaces = null
let colorGamuts = null
let lastModel = null
let package = 'coloraide-2.11-py3-none-any.whl'
let package = 'coloraide-2.12-py3-none-any.whl'
const defaultSpace = 'lab'
const defaultGamut = 'srgb'
const exceptions = new Set(['hwb', 'ryb', 'ryb-biased'])
Expand Down
Loading

0 comments on commit dfe1356

Please sign in to comment.