Skip to content

Commit f23c315

Browse files
committed
Upgrade to ColorAide 7.0
1 parent 2b5ad97 commit f23c315

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+1048
-408
lines changed

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# ColorHelper
22

3+
## 6.6.0
4+
5+
- **NEW**: Upgrade to ColorAide 7.0.
6+
37
## 6.5.1
48

59
- **NEW**: Fix issues related to ColorAide update.

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License (MIT)
22

3-
Copyright (c) 2015 - 2025 Isaac Muse
3+
Copyright (c) 2015 - 2026 Isaac Muse
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy of
66
this software and associated documentation files (the "Software"), to deal in

color_helper.sublime-settings

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@
148148
// "ColorHelper.lib.coloraide.spaces.zcam.ZCAMJMh",
149149
// "ColorHelper.lib.coloraide.spaces.hellwig.HellwigJMh",
150150
// "ColorHelper.lib.coloraide.spaces.hellwig.HellwigHKJMh",
151+
// "ColorHelper.lib.coloraide.spaces.scam.sCAMJMh",
152+
// "ColorHelper.lib.coloraide.spaces.sucs.sUCS",
153+
// "ColorHelper.lib.coloraide.spaces.msh.Msh",
151154
// "ColorHelper.lib.coloraide.spaces.cmy.CMY",
152155
// "ColorHelper.lib.coloraide.spaces.cmyk.CMYK",
153156
// "ColorHelper.lib.coloraide.spaces.din99o.DIN99o",

lib/coloraide/__meta__.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Meta related things."""
22
from __future__ import annotations
3-
from collections import namedtuple
3+
from typing import NamedTuple
44
import re
55

66
RE_VER = re.compile(
@@ -37,7 +37,19 @@
3737
PRE_REL_MAP = {"a": 'alpha', "b": 'beta', "rc": 'candidate'}
3838

3939

40-
class Version(namedtuple("Version", ["major", "minor", "micro", "release", "pre", "post", "dev"])):
40+
class VersionSpec(NamedTuple):
41+
"""Version specification."""
42+
43+
major: int
44+
minor: int
45+
micro: int
46+
release: str
47+
pre: int
48+
post: int
49+
dev: int
50+
51+
52+
class Version(VersionSpec):
4153
"""
4254
Get the version (PEP 440).
4355
@@ -77,7 +89,6 @@ class Version(namedtuple("Version", ["major", "minor", "micro", "release", "pre"
7789
Version(1, 2, 3, ".dev") 1.2.3.dev0
7890
Version(1, 2, 3, ".dev", dev=1) 1.2.3.dev1
7991
```
80-
8192
"""
8293

8394
def __new__(
@@ -193,5 +204,5 @@ def parse_version(ver: str) -> Version:
193204
return Version(major, minor, micro, release, pre, post, dev)
194205

195206

196-
__version_info__ = Version(6, 0, 0, "final")
207+
__version_info__ = Version(7, 0, 0, "final")
197208
__version__ = __version_info__._get_canonical()

lib/coloraide/algebra.py

Lines changed: 66 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,12 @@
2626
import operator
2727
import functools
2828
import itertools as it
29-
from .deprecate import deprecated
3029
from .types import (
3130
ArrayLike, MatrixLike, EmptyShape, VectorShape, MatrixShape, TensorShape, ArrayShape, VectorLike,
3231
TensorLike, Array, Matrix, Tensor, Vector, VectorBool, MatrixBool, TensorBool, MatrixInt, ArrayType, VectorInt, # noqa: F401
3332
Shape, DimHints, SupportsFloatOrInt
3433
)
35-
from typing import Callable, Sequence, Iterator, Any, Iterable, overload
34+
from typing import Callable, Sequence, Iterator, Any, Iterable, overload, cast
3635

3736
EPS = sys.float_info.epsilon
3837
RTOL = 4 * EPS
@@ -43,7 +42,6 @@
4342
MIN_FLOAT = sys.float_info.min
4443

4544
# Keeping for backwards compatibility
46-
prod = math.prod
4745
_all = builtins.all
4846
_any = builtins.any
4947

@@ -86,12 +84,12 @@
8684
################################
8785
# General math
8886
################################
89-
def sign(x: float) -> float:
87+
def sgn(x: SupportsFloatOrInt) -> SupportsFloatOrInt:
9088
"""Return the sign of a given value."""
9189

92-
if x and x == x:
93-
return x / abs(x)
94-
return x
90+
if isinstance(x, int):
91+
return 1 if x > 0 else -1 if x < 0 else 0
92+
return 1.0 if x > 0.0 else -1.0 if x < 0 else x
9593

9694

9795
def order(x: float) -> int:
@@ -1118,7 +1116,7 @@ def _cross_pad(a: ArrayLike, s: ArrayShape) -> Array:
11181116
m = acopy(a)
11191117

11201118
# Initialize indexes so we can properly write our data
1121-
total = prod(s[:-1])
1119+
total = math.prod(s[:-1])
11221120
idx = [0] * (len(s) - 1)
11231121

11241122
for c in range(total):
@@ -1783,8 +1781,8 @@ def multi_dot(arrays: Sequence[ArrayLike]) -> Any:
17831781
# We can easily calculate three with less complexity and in less time. Anything
17841782
# greater than three becomes a headache.
17851783
if count == 3:
1786-
pa = prod(shapes[0])
1787-
pc = prod(shapes[2])
1784+
pa = math.prod(shapes[0])
1785+
pc = math.prod(shapes[2])
17881786
cost1 = pa * shapes[2][0] + pc * shapes[0][0]
17891787
cost2 = pc * shapes[0][1] + pa * shapes[2][1] # type: ignore[misc]
17901788
if cost1 < cost2:
@@ -1845,7 +1843,7 @@ def __init__(self, array: ArrayLike | float, old: Shape, new: Shape) -> None:
18451843
elif self.different:
18461844
# Calculate the shape of the data.
18471845
if len(old) > 1:
1848-
self.amount = prod(old[:-1])
1846+
self.amount = math.prod(old[:-1])
18491847
self.length = old[-1]
18501848
else:
18511849
# Vectors have to be handled a bit special as they only have 1-D
@@ -1856,7 +1854,7 @@ def __init__(self, array: ArrayLike | float, old: Shape, new: Shape) -> None:
18561854
# We need to flip them based on whether the original shape has an even or odd number of
18571855
# dimensions.
18581856
diff = [int(x / y) if y else y for x, y in zip(new, old)]
1859-
repeat = prod(diff[:-1]) if len(old) > 1 else 1
1857+
repeat = math.prod(diff[:-1]) if len(old) > 1 else 1
18601858
expand = diff[-1]
18611859
if len(diff) > 1 and diff[-2] > 1:
18621860
self.repeat = expand
@@ -2110,7 +2108,7 @@ def __init__(self, *arrays: ArrayLike | float) -> None:
21102108
# But shouldn't matter for what we do.
21112109
self.shape = common
21122110
self.ndims = max_dims
2113-
self.size = prod(common)
2111+
self.size = math.prod(common)
21142112
self._init()
21152113

21162114
def _init(self) -> None:
@@ -2378,7 +2376,7 @@ def __call__(
23782376
# Apply math to two N-D matrices
23792377
if dims_a == dims_b:
23802378
empty = (not shape_a or 0 in shape_a) and (not shape_b or 0 in shape_b)
2381-
if not empty and prod(shape_a) != prod(shape_b): # pragma: no cover
2379+
if not empty and math.prod(shape_a) != math.prod(shape_b): # pragma: no cover
23822380
raise ValueError(f'Shape {shape_a} does not match the data total of {shape_b}')
23832381
with ArrayBuilder(m, shape_a) as build:
23842382
for x, y in zip(flatiter(a), flatiter(b)):
@@ -2658,13 +2656,6 @@ def vectorize2(
26582656
raise ValueError("'vectorize2' does not support dimensions greater than 2 or less than 1")
26592657

26602658

2661-
@deprecated("'vectorize1' is deprecated, use 'vectorize2(func, doc, params=1)' for the equivalent")
2662-
def vectorize1(pyfunc: Callable[..., Any], doc: str | None = None) -> Callable[..., Any]: # pragma: no cover
2663-
"""An optimized version of vectorize that is hard coded to broadcast only the first input."""
2664-
2665-
return vectorize2(pyfunc, doc, params=1)
2666-
2667-
26682659
@overload
26692660
def linspace(start: float, stop: float, num: int = ..., endpoint: bool = ...) -> Vector:
26702661
...
@@ -2805,6 +2796,38 @@ def isnan(a: TensorLike, *, dims: DimHints = ..., **kwargs: Any) -> TensorBool:
28052796
isnan = vectorize2(math.isnan, doc="Test if a value or values in an array are NaN.", params=1)
28062797

28072798

2799+
@overload # type: ignore[no-overload-impl]
2800+
def sign(a: float, *, dims: DimHints = ..., **kwargs: Any) -> float:
2801+
...
2802+
2803+
2804+
@overload
2805+
def sign(a: VectorLike, *, dims: DimHints = ..., **kwargs: Any) -> Vector:
2806+
...
2807+
2808+
2809+
@overload
2810+
def sign(a: MatrixLike, *, dims: DimHints = ..., **kwargs: Any) -> Matrix:
2811+
...
2812+
2813+
2814+
@overload
2815+
def sign(a: TensorLike, *, dims: DimHints = ..., **kwargs: Any) -> Tensor:
2816+
...
2817+
2818+
2819+
sign = vectorize2(sgn, doc="Return the sign of a number.", params=1)
2820+
2821+
2822+
def prod(a: ArrayLike | float) -> float:
2823+
"""Return the product."""
2824+
2825+
l = len(shape(a))
2826+
if l == 0:
2827+
return float(math.prod([a])) # type: ignore[list-item]
2828+
return float(math.prod(flatiter(a) if l > 1 else a)) # type: ignore[arg-type]
2829+
2830+
28082831
def allclose(a: ArrayType, b: ArrayType, **kwargs: Any) -> bool:
28092832
"""Test if all are close."""
28102833

@@ -3120,14 +3143,14 @@ def full(array_shape: int | Shape, fill_value: float | ArrayLike) -> Array | flo
31203143
if not isinstance(fill_value, Sequence):
31213144
return fill_value
31223145
_s = shape(fill_value)
3123-
if prod(_s) == 1:
3146+
if math.prod(_s) == 1:
31243147
return ravel(fill_value)[0]
31253148

31263149
# Normalize `fill_value` to be an array.
31273150
elif not isinstance(fill_value, Sequence):
31283151
m = [] # type: Array
31293152
with ArrayBuilder(m, s) as build:
3130-
for v in [fill_value] * prod(s):
3153+
for v in [fill_value] * math.prod(s):
31313154
next(build).append(v)
31323155
return m
31333156

@@ -3358,9 +3381,9 @@ def transpose(array: ArrayLike | float) -> float | Array:
33583381
# N x M matrix
33593382
if s and s[0] == 0:
33603383
s = s[1:] + (0,)
3361-
total = prod(s[:-1])
3384+
total = math.prod(s[:-1])
33623385
else:
3363-
total = prod(s)
3386+
total = math.prod(s)
33643387

33653388
# Create the array
33663389
m = [] # type: Array
@@ -3455,8 +3478,8 @@ def reshape(array: ArrayLike | float, new_shape: int | Shape) -> float | Array:
34553478
empty = (not new_shape or 0 in new_shape) and (not current_shape or 0 in current_shape)
34563479

34573480
# Make sure we can actually reshape.
3458-
total = prod(new_shape) if not empty else prod(new_shape[:-1])
3459-
if not empty and total != prod(current_shape):
3481+
total = math.prod(new_shape if not empty else new_shape[:-1])
3482+
if not empty and total != math.prod(current_shape):
34603483
raise ValueError(f'Shape {new_shape} does not match the data total of {shape(array)}')
34613484

34623485
# Create the array
@@ -4290,7 +4313,7 @@ def _qr(a: Matrix, m: int, n: int, mode: str = 'reduced') -> Any:
42904313
for k in range(0, m - 1 if not tall else n):
42914314
# Calculate the householder reflections
42924315
norm = math.sqrt(sum([r[i][k] ** 2 for i in range(k, m)]))
4293-
sig = -sign(r[k][k])
4316+
sig = -sgn(r[k][k])
42944317
u0 = r[k][k] - sig * norm
42954318
w = [[(r[i][k] / u0) if u0 else 1] for i in range(k, m)]
42964319
w[0][0] = 1
@@ -4479,7 +4502,7 @@ def solve(a: MatrixLike | TensorLike, b: ArrayLike) -> Array:
44794502
p, l, u = lu(a, p_indices=True, _shape=s)
44804503

44814504
# If determinant is zero, we can't solve. Really small determinant may give bad results.
4482-
if prod(l[i][i] * u[i][i] for i in range(size)) == 0.0:
4505+
if math.prod(l[i][i] * u[i][i] for i in range(size)) == 0.0:
44834506
raise ValueError('Matrix is singular')
44844507

44854508
# Solve for x using forward substitution on U and back substitution on L
@@ -4521,7 +4544,7 @@ def solve(a: MatrixLike | TensorLike, b: ArrayLike) -> Array:
45214544

45224545
p, l, u = lu(ma, p_indices=True, _shape=m_shape)
45234546

4524-
if prod(l[i][i] * u[i][i] for i in range(size)) == 0.0: # pragma: no cover
4547+
if math.prod(l[i][i] * u[i][i] for i in range(size)) == 0.0: # pragma: no cover
45254548
raise ValueError('Matrix is singular')
45264549

45274550
next(build).append(_back_sub_vector(u, _forward_sub_vector(l, [b[i] for i in p], size), size)) # type: ignore[misc]
@@ -4543,7 +4566,7 @@ def solve(a: MatrixLike | TensorLike, b: ArrayLike) -> Array:
45434566

45444567
p, l, u = lu(ma, p_indices=True, _shape=s[-2:]) # type: ignore[misc]
45454568

4546-
if prod(l[i][i] * u[i][i] for i in range(size)) == 0.0:
4569+
if math.prod(l[i][i] * u[i][i] for i in range(size)) == 0.0:
45474570
raise ValueError('Matrix is singular')
45484571

45494572
bi = [[*mb[i]] for i in p]
@@ -4578,8 +4601,8 @@ def det(array: MatrixLike | TensorLike) -> float | Vector:
45784601
size = s[0]
45794602
p, l, u = lu(array, _shape=s)
45804603
swaps = size - trace(p)
4581-
sign = (-1) ** (swaps - 1) if swaps else 1
4582-
dt = sign * prod(l[i][i] * u[i][i] for i in range(size))
4604+
_sign = (-1) ** (swaps - 1) if swaps else 1
4605+
dt = _sign * math.prod(l[i][i] * u[i][i] for i in range(size))
45834606
return 0.0 if not dt else dt
45844607
else:
45854608
last = s[-2:] # type: ignore[misc]
@@ -4625,7 +4648,7 @@ def inv(matrix: MatrixLike | TensorLike) -> Matrix | Tensor:
46254648
# Floating point math will produce very small, non-zero determinants for singular matrices.
46264649
# This occurs with Numpy as well.
46274650
# Don't bother calculating sign as we only care about how close to zero we are.
4628-
if prod(l[i][i] * u[i][i] for i in range(size)) == 0.0:
4651+
if math.prod(l[i][i] * u[i][i] for i in range(size)) == 0.0:
46294652
raise ValueError('Matrix is singular')
46304653

46314654
# Solve for the identity matrix (will give us inverse)
@@ -4722,7 +4745,7 @@ def vstack(arrays: Sequence[ArrayLike | float]) -> Matrix | Tensor:
47224745
raise ValueError('All the input array dimensions except for the concatenation axis must match exactly')
47234746

47244747
# Stack the arrays
4725-
m.extend(reshape(a, (prod(s[:1 - dims]),) + s[1 - dims:-1] + s[-1:])) # type: ignore[arg-type, misc]
4748+
m.extend(reshape(a, (math.prod(s[:1 - dims]),) + s[1 - dims:-1] + s[-1:])) # type: ignore[arg-type, misc]
47264749

47274750
# Update the last array tracker
47284751
if not last or len(last) > len(s):
@@ -4740,7 +4763,7 @@ def _hstack_extract(a: ArrayLike | float, s: ArrayShape) -> Iterator[Array]:
47404763
"""Extract data from the second axis."""
47414764

47424765
data = flatiter(a)
4743-
length = prod(s[1:])
4766+
length = math.prod(s[1:])
47444767

47454768
for _ in range(s[0]):
47464769
yield [next(data) for _ in range(length)]
@@ -5203,9 +5226,9 @@ def roll(
52035226
if axis is None:
52045227
if not isinstance(shift, int):
52055228
shift = sum(shift)
5206-
p = prod(s)
5207-
sgn = sign(shift)
5208-
shift = int(shift % (p * sgn)) if p and sgn else 0
5229+
p = math.prod(s)
5230+
_sign = sgn(shift)
5231+
shift = shift % (p * _sign) if p and _sign else 0
52095232
flat = ravel(a) if len(s) != 1 else [*a] # type: ignore[misc]
52105233
sh = -shift
52115234
flat[:] = flat[sh:] + flat[:sh]
@@ -5221,11 +5244,12 @@ def roll(
52215244
new_shift = [] # type: VectorInt
52225245
new_axes = [] # type: VectorInt
52235246
for i, j in broadcast(shift, axes):
5247+
i, j = cast(int, i), cast(int, j)
52245248
if j < 0:
52255249
j = l + j
5226-
sgn = sign(i)
5227-
new_shift.append(int(i % (s[j] * sgn)) if s[j] and sgn else 0) # type: ignore[call-overload]
5228-
new_axes.append(j) # type: ignore[arg-type]
5250+
_sign = sgn(i)
5251+
new_shift.append((i % (s[j] * _sign)) if s[j] and _sign else 0)
5252+
new_axes.append(j)
52295253

52305254
# Perform the roll across the specified axes
52315255
for idx in ndindex(s[:-1] + (1,)): # type: ignore[misc]

lib/coloraide/average.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,15 @@ def average(
4040
if cs.is_polar():
4141
hue_index = cs.hue_index() # type: ignore[attr-defined]
4242
is_hwb = isinstance(cs, HWBish)
43+
hue_max = cs.channels[hue_index].high
44+
to_rad = math.tau / hue_max
45+
to_hue = hue_max / math.tau
4346
else:
4447
hue_index = -1
4548
is_hwb = False
49+
hue_max = 0.0
50+
to_rad = 0.0
51+
to_hue = 0.0
4652
channels = cs.channels
4753
chan_count = len(channels)
4854
avgs = [0.0] * chan_count
@@ -104,7 +110,7 @@ def average(
104110
counts[i] += 1
105111
n = counts[i]
106112
if i == hue_index:
107-
rad = math.radians(coord)
113+
rad = coord * to_rad
108114
sin += ((math.sin(rad) * wfactor) - sin) / n
109115
cos += ((math.cos(rad) * wfactor) - cos) / n
110116
else:
@@ -132,8 +138,8 @@ def average(
132138
if abs(sin) < util.ACHROMATIC_THRESHOLD_SM and abs(cos) < util.ACHROMATIC_THRESHOLD_SM:
133139
avgs[i] = math.nan
134140
else:
135-
avg_theta = math.degrees(math.atan2(sin, cos))
136-
avgs[i] = (avg_theta + 360) if avg_theta < 0 else avg_theta
141+
avg_theta = math.atan2(sin, cos) * to_hue
142+
avgs[i] = (avg_theta + hue_max) if avg_theta < 0 else avg_theta
137143
else:
138144
avgs[i] /= factor
139145

0 commit comments

Comments
 (0)