Skip to content

Commit ef9a424

Browse files
authored
Chore/upgrade coloraide (#240)
* Upgrade coloraide to 1.5 * Update changelog
1 parent 1c9f17c commit ef9a424

File tree

18 files changed

+219
-94
lines changed

18 files changed

+219
-94
lines changed

CHANGES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## 6.1.0
44

5-
- **NEW**: Update to ColorAide 1.4.
5+
- **NEW**: Update to ColorAide 1.5.
66
- **FIX**: Fix issue where if a view does not have a syntax it could
77
cause an exception.
88

lib/coloraide/__meta__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,5 +192,5 @@ def parse_version(ver: str) -> Version:
192192
return Version(major, minor, micro, release, pre, post, dev)
193193

194194

195-
__version_info__ = Version(1, 4, 0, "final")
195+
__version_info__ = Version(1, 5, 0, "final")
196196
__version__ = __version_info__._get_canonical()

lib/coloraide/color.py

Lines changed: 107 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from itertools import zip_longest as zipl
1616
from .css import parse
1717
from .types import VectorLike, Vector, ColorInput
18-
from .spaces import Space, Cylindrical
18+
from .spaces import Space
1919
from .spaces.hsv import HSV
2020
from .spaces.srgb.css import sRGB
2121
from .spaces.srgb_linear import sRGBLinear
@@ -65,6 +65,8 @@
6565
class ColorMatch:
6666
"""Color match object."""
6767

68+
__slots__ = ('color', 'start', 'end')
69+
6870
def __init__(self, color: 'Color', start: int, end: int) -> None:
6971
"""Initialize."""
7072

@@ -588,9 +590,8 @@ def chromatic_adaptation(
588590
) -> Vector:
589591
"""Chromatic adaptation."""
590592

591-
try:
592-
adapter = cls.CAT_MAP[method if method is not None else cls.CHROMATIC_ADAPTATION]
593-
except KeyError:
593+
adapter = cls.CAT_MAP.get(method if method is not None else cls.CHROMATIC_ADAPTATION)
594+
if not adapter:
594595
raise ValueError("'{}' is not a supported CAT".format(method))
595596

596597
return adapter.adapt(w1, w2, xyz)
@@ -604,14 +605,7 @@ def clip(self, space: Optional[str] = None) -> 'Color':
604605

605606
# Convert to desired space
606607
c = self.convert(space, in_place=True)
607-
608-
# If we are perfectly in gamut, don't waste time clipping.
609-
if c.in_gamut(tolerance=0.0):
610-
if isinstance(c._space, Cylindrical):
611-
name = c._space.hue_name()
612-
c.set(name, util.constrain_hue(c[name]))
613-
else:
614-
gamut.clip_channels(c)
608+
gamut.clip_channels(c)
615609

616610
# Adjust "this" color
617611
return c.convert(orig_space, in_place=True)
@@ -625,60 +619,48 @@ def fit(
625619
) -> 'Color':
626620
"""Fit the gamut using the provided method."""
627621

622+
if method is None:
623+
method = self.FIT
624+
628625
# Dedicated clip method.
629-
orig_space = self.space()
630-
if method == 'clip' or (method is None and self.FIT == "clip"):
626+
if method == 'clip':
627+
631628
return self.clip(space)
632629

630+
orig_space = self.space()
633631
if space is None:
634632
space = self.space()
635633

636-
if method is None:
637-
method = self.FIT
638-
639634
# Select appropriate mapping algorithm
640-
if method in self.FIT_MAP:
641-
func = self.FIT_MAP[method].fit
642-
else:
635+
mapping = self.FIT_MAP.get(method)
636+
if not mapping:
637+
643638
# Unknown fit method
644639
raise ValueError("'{}' gamut mapping is not currently supported".format(method))
645640

646641
# Convert to desired space
647-
c = self.convert(space, in_place=True)
642+
self.convert(space, in_place=True)
648643

649-
# If we are perfectly in gamut, don't waste time fitting, just normalize hues.
650-
# If out of gamut, apply mapping/clipping/etc.
651-
if c.in_gamut(tolerance=0.0):
652-
if isinstance(c._space, Cylindrical):
653-
name = c._space.hue_name()
654-
c.set(name, util.constrain_hue(c[name]))
655-
else:
656-
# Doesn't seem to be an easy way that `mypy` can know whether this is the ABC class or not
657-
func(c, **kwargs)
644+
# Call the appropriate gamut mapping algorithm
645+
mapping.fit(self, **kwargs)
658646

659-
# Adjust "this" color
660-
return c.convert(orig_space, in_place=True)
647+
# Convert back to the original color space
648+
return self.convert(orig_space, in_place=True)
661649

662650
def in_gamut(self, space: Optional[str] = None, *, tolerance: float = util.DEF_FIT_TOLERANCE) -> bool:
663651
"""Check if current color is in gamut."""
664652

665653
if space is None:
666654
space = self.space()
667655

668-
# Check gamut in the provided space
669-
if space is not None and space != self.space():
670-
c = self.convert(space)
671-
return c.in_gamut(tolerance=tolerance)
656+
# Check if gamut is in the provided space
657+
c = self.convert(space) if space is not None and space != self.space() else self
672658

673659
# Check the color space specified for gamut checking.
674660
# If it proves to be in gamut, we will then test if the current
675661
# space is constrained properly.
676-
if self._space.GAMUT_CHECK is not None:
677-
c = self.convert(self._space.GAMUT_CHECK)
678-
if not c.in_gamut(tolerance=tolerance):
679-
return False
680-
681-
return gamut.verify(self, tolerance)
662+
if c._space.GAMUT_CHECK is not None and not c.convert(c._space.GAMUT_CHECK).in_gamut(tolerance=tolerance):
663+
return False
682664

683665
def mask(self, channel: Union[str, Sequence[str]], *, invert: bool = False, in_place: bool = False) -> 'Color':
684666
"""Mask color channels."""
@@ -710,7 +692,11 @@ def mix(
710692

711693
if not self._is_color(color) and not isinstance(color, (str, Mapping)):
712694
raise TypeError("Unexpected type '{}'".format(type(color)))
713-
mixed = self.interpolate([self, color], **interpolate_args)(percent)
695+
i = self.interpolate([self, color], **interpolate_args)
696+
# Scale really needs to be between 0 and 1 as mix deals in percentages.
697+
if i.domain:
698+
i.domain = interpolate.normalize_domain(i.domain)
699+
mixed = i(percent)
714700
return self.mutate(mixed) if in_place else mixed
715701

716702
@classmethod
@@ -726,7 +712,11 @@ def steps(
726712
) -> List['Color']:
727713
"""Discrete steps."""
728714

729-
return cls.interpolate(colors, **interpolate_args).steps(steps, max_steps, max_delta_e, delta_e)
715+
i = cls.interpolate(colors, **interpolate_args)
716+
# Scale really needs to be between 0 and 1 or steps will break
717+
if i.domain:
718+
i.domain = interpolate.normalize_domain(i.domain)
719+
return i.steps(steps, max_steps, max_delta_e, delta_e)
730720

731721
@classmethod
732722
def interpolate(
@@ -739,6 +729,7 @@ def interpolate(
739729
hue: str = util.DEF_HUE_ADJ,
740730
premultiplied: bool = True,
741731
extrapolate: bool = False,
732+
domain: Optional[List[float]] = None,
742733
method: str = "linear",
743734
**kwargs: Any
744735
) -> Interpolator:
@@ -764,6 +755,7 @@ def interpolate(
764755
hue=hue,
765756
premultiplied=premultiplied,
766757
extrapolate=extrapolate,
758+
domain=domain,
767759
**kwargs
768760
)
769761

@@ -828,10 +820,10 @@ def delta_e(
828820
if method is None:
829821
method = self.DELTA_E
830822

831-
try:
832-
return self.DE_MAP[method].distance(self, color, **kwargs)
833-
except KeyError:
823+
delta = self.DE_MAP.get(method)
824+
if not delta:
834825
raise ValueError("'{}' is not currently a supported distancing algorithm.".format(method))
826+
return delta.distance(self, color, **kwargs)
835827

836828
def distance(self, color: ColorInput, *, space: str = "lab") -> float:
837829
"""Delta."""
@@ -860,33 +852,84 @@ def contrast(self, color: ColorInput, method: Optional[str] = None) -> float:
860852
color = self._handle_color_input(color)
861853
return contrast.contrast(method, self, color)
862854

863-
def get(self, name: str) -> float:
855+
@overload
856+
def get(self, name: str) -> float: # noqa: D105
857+
...
858+
859+
@overload
860+
def get(self, name: Union[List[str], Tuple[str, ...]]) -> List[float]: # noqa: D105
861+
...
862+
863+
def get(self, name: Union[str, List[str], Tuple[str, ...]]) -> Union[float, List[float]]:
864864
"""Get channel."""
865865

866-
# Handle space.attribute
867-
if '.' in name:
868-
space, channel = name.split('.', 1)
869-
obj = self.convert(space)
870-
return obj[channel]
866+
# Handle single channel
867+
if isinstance(name, str):
868+
# Handle space.channel
869+
if '.' in name:
870+
space, channel = name.split('.', 1)
871+
return self.convert(space)[channel]
872+
return self[name]
871873

872-
return self[name]
874+
# Handle list of channels
875+
else:
876+
original_space = current_space = self.space()
877+
obj = self
878+
values = []
879+
880+
for n in name:
881+
# Handle space.channel
882+
space, channel = n.split('.', 1) if '.' in n else (original_space, n)
883+
if space != current_space:
884+
obj = self if space == original_space else self.convert(space)
885+
current_space = space
886+
values.append(obj[channel])
887+
return values
873888

874889
def set( # noqa: A003
875890
self,
876-
name: str,
877-
value: Union[float, Callable[..., float]]
891+
name: Union[str, Dict[str, Union[float, Callable[..., float]]]],
892+
value: Optional[Union[float, Callable[..., float]]] = None
878893
) -> 'Color':
879894
"""Set channel."""
880895

881-
# Handle space.attribute
882-
if '.' in name:
883-
space, channel = name.split('.', 1)
884-
obj = self.convert(space)
885-
obj[channel] = value(obj[channel]) if callable(value) else value
886-
return self.update(obj)
896+
# Set all the channels in a dictionary.
897+
# Sort by name to reduce how many times we convert
898+
# when dealing with different color spaces.
899+
if value is None:
900+
if isinstance(name, str):
901+
raise ValueError("Missing the positional 'value' argument for channel '{}'".format(name))
902+
903+
original_space = current_space = self.space()
904+
obj = self.clone()
905+
906+
for k, v in name.items():
907+
908+
# Handle space.channel
909+
space, channel = k.split('.', 1) if '.' in k else (original_space, k)
910+
if space != current_space:
911+
obj.convert(space, in_place=True)
912+
current_space = space
913+
obj[channel] = v(obj[channel]) if callable(v) else v
914+
915+
# Update the original color
916+
self.update(obj)
917+
918+
# Set a single channel value
919+
else:
920+
if isinstance(name, dict):
921+
raise ValueError("A dict of channels and values cannot be used with the positional 'value' parameter")
922+
923+
# Handle space.channel
924+
if '.' in name:
925+
space, channel = name.split('.', 1)
926+
obj = self.convert(space)
927+
obj[channel] = value(obj[channel]) if callable(value) else value
928+
return self.update(obj)
929+
930+
# Handle a function that modifies the value or a direct value
931+
self[name] = value(self[name]) if callable(value) else value
887932

888-
# Handle a function that modifies the value or a direct value
889-
self[name] = value(self[name]) if callable(value) else value
890933
return self
891934

892935

lib/coloraide/compositing/blend_modes.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ def apply(self, cb: Vector, cs: Vector) -> Vector:
304304
def get_blender(blend: str) -> Blend:
305305
"""Get desired blend mode."""
306306

307-
try:
308-
return SUPPORTED[blend]
309-
except KeyError:
307+
blender = SUPPORTED.get(blend)
308+
if not blender:
310309
raise ValueError("'{}' is not a recognized blend mode".format(blend))
310+
return blender

lib/coloraide/compositing/porter_duff.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ def fb(self) -> float:
237237
def compositor(name: str) -> Type[PorterDuff]:
238238
"""Get the requested compositor."""
239239

240-
try:
241-
return SUPPORTED[name]
242-
except KeyError:
240+
composite = SUPPORTED.get(name)
241+
if not composite:
243242
raise ValueError("'{}' compositing is not supported".format(name))
243+
return composite

lib/coloraide/contrast/__init__.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,8 @@ def contrast(name: Optional[str], color1: 'Color', color2: 'Color', **kwargs: An
2323
if name is None:
2424
name = color1.CONTRAST
2525

26-
try:
27-
func = color1.CONTRAST_MAP[name].contrast
28-
except KeyError:
26+
method = color1.CONTRAST_MAP.get(name)
27+
if not method:
2928
raise ValueError("'{}' contrast method is not supported".format(name))
3029

31-
return func(color1, color2, **kwargs)
30+
return method.contrast(color1, color2, **kwargs)

lib/coloraide/css/parse.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@
142142
\b(oklch)\(\s*
143143
(?:
144144
# Space separated format
145-
(?:(?:{strict_percent}|{float}){space}){{2}}{angle}{angle}(?:{slash}(?:{strict_percent}|{float}))?
145+
(?:(?:{strict_percent}|{float}){space}){{2}}{angle}(?:{slash}(?:{strict_percent}|{float}))?
146146
)
147147
\s*\)
148148
""".format(**COLOR_PARTS)

lib/coloraide/distance/delta_e_hyab.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def distance(self, color: 'Color', sample: 'Color', space: Optional[str] = None,
3636
raise ValueError("The space '{}' is not a 'lab-ish' color space and cannot use HyAB".format(space))
3737

3838
names = color._space.labish_names()
39-
l1, a1, b1 = alg.no_nans([color.get(names[0]), color.get(names[1]), color.get(names[2])])
40-
l2, a2, b2 = alg.no_nans([sample.get(names[0]), sample.get(names[1]), sample.get(names[2])])
39+
l1, a1, b1 = alg.no_nans(color.get(names))
40+
l2, a2, b2 = alg.no_nans(sample.get(names))
4141

4242
return abs(l1 - l2) + math.sqrt((a1 - a2) ** 2 + (b1 - b2) ** 2)

lib/coloraide/filters/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,8 @@ def filters(
2929
) -> 'Color':
3030
"""Filter."""
3131

32-
try:
33-
f = color.FILTER_MAP[name]
34-
except KeyError:
32+
f = color.FILTER_MAP.get(name)
33+
if not f:
3534
raise ValueError("'{}' filter is not supported".format(name))
3635

3736
if space is None:

lib/coloraide/gamut/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from ..channels import FLG_ANGLE
44
from abc import ABCMeta, abstractmethod
55
from ..types import Plugin
6-
from typing import TYPE_CHECKING, Optional, Any
6+
from typing import TYPE_CHECKING, Any
77

88
if TYPE_CHECKING: # pragma: no cover
99
from ..color import Color
@@ -38,8 +38,8 @@ def verify(color: 'Color', tolerance: float) -> bool:
3838
if chan.flags & FLG_ANGLE or not chan.bound or alg.is_nan(value):
3939
continue
4040

41-
a = chan.low # type: Optional[float]
42-
b = chan.high # type: Optional[float]
41+
a = chan.low
42+
b = chan.high
4343

4444
# Check if bounded values are in bounds
4545
if (a is not None and value < (a - tolerance)) or (b is not None and value > (b + tolerance)):

0 commit comments

Comments
 (0)