Coverage for lib/lottie/utils/color.py: 40%
202 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-20 16:17 +0100
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-20 16:17 +0100
1import enum
2import math
3import colorsys
4from ..nvector import NVector
7def from_uint8(r, g, b, a=255):
8 return Color(r, g, b, a) / 255
11class ColorMode(enum.Enum):
12 ## sRGB, Components in [0, 1]
13 RGB = enum.auto()
14 ## HSV, components in [0, 1]
15 HSV = enum.auto()
16 ## HSL, components in [0, 1]
17 HSL = enum.auto()
18 ## CIE XYZ with Illuminant D65. Components in [0, 1]
19 XYZ = enum.auto()
20 ## CIE L*u*v*
21 LUV = enum.auto()
22 ## CIE Lch(uv), polar version of LUV where C is the radius and H an angle in radians
23 LCH_uv = enum.auto()
24 ## CIE L*a*b*
25 LAB = enum.auto()
26 ## CIE LCh(ab), polar version of LAB where C is the radius and H an angle in radians
27 #LCH_ab = enum.auto()
30def _clamp(x):
31 return max(0, min(1, x))
34class Conversion:
35 _conv_paths = {
36 (ColorMode.RGB, ColorMode.RGB): [],
37 (ColorMode.RGB, ColorMode.HSV): [],
38 (ColorMode.RGB, ColorMode.HSL): [],
39 (ColorMode.RGB, ColorMode.XYZ): [],
40 (ColorMode.RGB, ColorMode.LUV): [ColorMode.XYZ],
41 (ColorMode.RGB, ColorMode.LAB): [ColorMode.XYZ],
42 (ColorMode.RGB, ColorMode.LCH_uv): [ColorMode.XYZ, ColorMode.LUV],
43 #(ColorMode.RGB, ColorMode.LCH_ab): [ColorMode.XYZ, ColorMode.LAB],
45 (ColorMode.HSV, ColorMode.RGB): [],
46 (ColorMode.HSV, ColorMode.HSV): [],
47 (ColorMode.HSV, ColorMode.HSL): [],
48 (ColorMode.HSV, ColorMode.XYZ): [ColorMode.RGB],
49 (ColorMode.HSV, ColorMode.LUV): [ColorMode.RGB, ColorMode.XYZ],
50 (ColorMode.HSV, ColorMode.LAB): [ColorMode.RGB, ColorMode.XYZ],
51 (ColorMode.HSV, ColorMode.LCH_uv): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LUV],
52 #(ColorMode.HSV, ColorMode.LCH_ab): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LAB],
54 (ColorMode.HSL, ColorMode.RGB): [],
55 (ColorMode.HSL, ColorMode.HSV): [],
56 (ColorMode.HSL, ColorMode.HSL): [],
57 (ColorMode.HSL, ColorMode.XYZ): [ColorMode.RGB],
58 (ColorMode.HSL, ColorMode.LUV): [ColorMode.RGB, ColorMode.XYZ],
59 (ColorMode.HSL, ColorMode.LAB): [ColorMode.RGB, ColorMode.XYZ],
60 (ColorMode.HSL, ColorMode.LCH_uv): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LUV],
61 #(ColorMode.HSL, ColorMode.LCH_ab): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LAB],
63 (ColorMode.XYZ, ColorMode.RGB): [],
64 (ColorMode.XYZ, ColorMode.HSV): [ColorMode.RGB],
65 (ColorMode.XYZ, ColorMode.HSL): [ColorMode.RGB],
66 (ColorMode.XYZ, ColorMode.XYZ): [],
67 (ColorMode.XYZ, ColorMode.LUV): [],
68 (ColorMode.XYZ, ColorMode.LAB): [],
69 (ColorMode.XYZ, ColorMode.LCH_uv): [ColorMode.LUV],
70 #(ColorMode.XYZ, ColorMode.LCH_ab): [ColorMode.LAB],
72 (ColorMode.LCH_uv, ColorMode.RGB): [ColorMode.LUV, ColorMode.XYZ],
73 (ColorMode.LCH_uv, ColorMode.HSV): [ColorMode.LUV, ColorMode.XYZ, ColorMode.RGB],
74 (ColorMode.LCH_uv, ColorMode.HSL): [ColorMode.LUV, ColorMode.XYZ, ColorMode.RGB],
75 (ColorMode.LCH_uv, ColorMode.XYZ): [ColorMode.LUV],
76 (ColorMode.LCH_uv, ColorMode.LUV): [],
77 (ColorMode.LCH_uv, ColorMode.LAB): [ColorMode.LUV, ColorMode.XYZ],
78 (ColorMode.LCH_uv, ColorMode.LCH_uv): [],
79 #(ColorMode.LCH_uv, ColorMode.LCH_ab): [ColorMode.LUV, ColorMode.XYZ, ColorMode.LAB],
81 (ColorMode.LUV, ColorMode.RGB): [ColorMode.XYZ],
82 (ColorMode.LUV, ColorMode.HSV): [ColorMode.XYZ, ColorMode.RGB],
83 (ColorMode.LUV, ColorMode.HSL): [ColorMode.XYZ, ColorMode.RGB],
84 (ColorMode.LUV, ColorMode.XYZ): [],
85 (ColorMode.LUV, ColorMode.LUV): [],
86 (ColorMode.LUV, ColorMode.LAB): [ColorMode.XYZ],
87 (ColorMode.LUV, ColorMode.LCH_uv): [],
88 #(ColorMode.LUV, ColorMode.LCH_ab): [ColorMode.XYZ, ColorMode.LAB],
90 (ColorMode.LAB, ColorMode.RGB): [ColorMode.XYZ],
91 (ColorMode.LAB, ColorMode.HSV): [ColorMode.XYZ, ColorMode.RGB],
92 (ColorMode.LAB, ColorMode.HSL): [ColorMode.XYZ, ColorMode.RGB],
93 (ColorMode.LAB, ColorMode.XYZ): [],
94 (ColorMode.LAB, ColorMode.LUV): [ColorMode.XYZ],
95 (ColorMode.LAB, ColorMode.LAB): [],
96 (ColorMode.LAB, ColorMode.LCH_uv): [ColorMode.XYZ, ColorMode.LUV],
97 #(ColorMode.LAB, ColorMode.LCH_ab): [],
99 #(ColorMode.LCH_ab, ColorMode.RGB): [ColorMode.LAB, ColorMode.XYZ],
100 #(ColorMode.LCH_ab, ColorMode.HSV): [ColorMode.LAB, ColorMode.XYZ, ColorMode.RGB],
101 #(ColorMode.LCH_ab, ColorMode.HSL): [ColorMode.LAB, ColorMode.XYZ, ColorMode.RGB],
102 #(ColorMode.LCH_ab, ColorMode.XYZ): [ColorMode.LAB],
103 #(ColorMode.LCH_ab, ColorMode.LUV): [ColorMode.LAB, ColorMode.XYZ],
104 #(ColorMode.LCH_ab, ColorMode.LAB): [],
105 #(ColorMode.LCH_ab, ColorMode.LCH_uv): [ColorMode.LAB, ColorMode.XYZ, ColorMode.LUV],
106 #(ColorMode.LCH_ab, ColorMode.LCH_ab): [],
107 }
109 @staticmethod
110 def rgb_to_hsv(r, g, b):
111 return colorsys.rgb_to_hsv(r, g, b)
113 @staticmethod
114 def hsv_to_rgb(h, s, v):
115 return colorsys.hsv_to_rgb(h, s, v)
117 @staticmethod
118 def hsl_to_hsv(h, s_hsl, l):
119 v = l + s_hsl * min(l, 1 - l)
120 s_hsv = 0 if v == 0 else 2 - 2 * l / v
121 return (h, s_hsv, v)
123 @staticmethod
124 def hsv_to_hsl(h, s_hsv, v):
125 l = v - v * s_hsv / 2
126 s_hsl = 0 if l in (0, 1) else (v - l) / min(l, 1 - l)
127 return (h, s_hsl, l)
129 @staticmethod
130 def rgb_to_hsl(r, g, b):
131 h, l, s = colorsys.rgb_to_hls(r, g, b)
132 return (h, s, l)
134 @staticmethod
135 def hsl_to_rgb(h, s, l):
136 return colorsys.hls_to_rgb(h, l, s)
138 # http://w3.uqo.ca/missaoui/Publications/TRColorSpace.zip
139 #@staticmethod
140 #def rgb_to_hcl(r, g, b, gamma=3, y0=100):
141 #maxc = max(r, g, b)
142 #minc = min(r, g, b)
143 #if maxc > 0:
144 #alpha = 1/y0 * minc / maxc
145 #else:
146 #alpha = 0
147 #q = math.e ** (alpha * gamma)
148 #h = math.atan2(g - b, r - g)
149 #if h < 0:
150 #h += 2*math.pi
151 #h /= 2*math.pi
152 #c = q / 3 * (abs(r-g) + abs(g-b) + abs(b-r))
153 #l = (q * maxc + (q-1) * minc) / 2
154 #return (h, c, l)
156 #@staticmethod
157 #def hcl_to_rgb(h, c, l, gamma=3, y0=100):
158 #h *= 2*math.pi
160 #q = math.e ** ((1 - 2*c / 4*l) * gamma / y0)
161 #minc = (4*l - 3*c) / (4*q - 2)
162 #maxc = minc + 3*c / 2*q
164 #if h <= math.pi * 1 / 3:
165 #tan = math.tan(3/2*h)
166 #r = maxc
167 #b = minc
168 #g = (r * tan + b) / (1 + tan)
169 #elif h <= math.pi * 2 / 3:
170 #tan = math.tan(3/4*(h-math.pi))
171 #g = maxc
172 #b = minc
173 #r = (g * (1+tan) - b) / tan
174 #elif h <= math.pi * 3 / 3:
175 #tan = math.tan(3/4*(h-math.pi))
176 #g = maxc
177 #r = minc
178 #b = g * (1+tan) - r * tan
179 #elif h <= math.pi * 4 / 3:
180 #tan = math.tan(3/2*(h+math.pi))
181 #b = maxc
182 #r = minc
183 #g = (r * tan + b) / (1 + tan)
184 #elif h <= math.pi * 5 / 3:
185 #tan = math.tan(3/4*h)
186 #b = maxc
187 #g = minc
188 #r = (g * (1+tan) - b) / tan
189 #else:
190 #tan = math.tan(3/4*h)
191 #r = maxc
192 #g = minc
193 #b = g * (1+tan) - r * tan
195 #return _clamp(r), _clamp(g), _clamp(b)
197 @staticmethod
198 def rgb_to_xyz(r, g, b):
199 def _gamma(v):
200 return v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4
201 rgb = (_gamma(r), _gamma(g), _gamma(b))
202 matrix = [
203 [0.4124564, 0.3575761, 0.1804375],
204 [0.2126729, 0.7151522, 0.0721750],
205 [0.0193339, 0.1191920, 0.9503041],
206 ]
207 return tuple(
208 sum(rgb[i] * c for i, c in enumerate(row))
209 for row in matrix
210 )
212 @staticmethod
213 def xyz_to_rgb(x, y, z):
214 def _gamma1(v):
215 return _clamp(v * 12.92 if v <= 0.0031308 else v ** (1/2.4) * 1.055 - 0.055)
216 matrix = [
217 [+3.2404542, -1.5371385, -0.4985314],
218 [-0.9692660, +1.8760108, +0.0415560],
219 [+0.0556434, -0.2040259, +1.0572252],
220 ]
221 xyz = (x, y, z)
222 return tuple(map(_gamma1, (
223 sum(xyz[i] * c for i, c in enumerate(row))
224 for row in matrix
225 )))
227 @staticmethod
228 def xyz_to_luv(x, y, z):
229 u1r = 0.2009
230 v1r = 0.4610
231 yr = 100
233 kap = (29/3)**3
234 eps = (6/29)**3
236 try:
237 u1 = 4*x / (x + 15*y + 3*z)
238 v1 = 9*y / (x + 15*y + 3*z)
239 except ZeroDivisionError:
240 return 0, 0, 0
242 y_r = y/yr
243 l = 166 * y_r ** (1/3) - 16 if y_r > eps else kap * y_r
244 u = 13 * l * (u1 - u1r)
245 v = 13 * l * (v1 - v1r)
246 return l, u, v
248 @staticmethod
249 def luv_to_xyz(l, u, v):
250 u1r = 0.2009
251 v1r = 0.4610
252 yr = 100
254 kap = (29/3)**3
256 if l == 0:
257 u1 = u1r
258 v1 = v1r
259 else:
260 u1 = u / (13 * l) + u1r
261 v1 = v / (13 * l) + v1r
263 y = yr * l / kap if l <= 8 else yr * ((l + 16) / 116) ** 3
264 x = y * 9*u1 / (4*v1)
265 z = y * (12 - 3*u1 - 20*v1) / (4*v1)
266 return x, y, z
268 @staticmethod
269 def luv_to_lch_uv(l, u, v):
270 c = math.hypot(u, v)
271 h = math.atan2(v, u)
272 if h < 0:
273 h += math.tau
274 return l, c, h
276 @staticmethod
277 def lch_uv_to_luv(l, c, h):
278 u = math.cos(h) * c
279 v = math.sin(h) * c
280 return l, u, v
282 @staticmethod
283 def xyz_to_lab(x, y, z):
284 # D65 Illuminant aka sRGB(1,1,1)
285 xn = 0.950489
286 yn = 1
287 zn = 108.8840
289 delta = 6 / 29
291 def f(t):
292 return t ** (1/3) if t > delta ** 3 else t / (3*delta**2) + 4/29
294 fy = f(y/yn)
295 l = 116 * fy - 16
296 a = 500 * (f(x/xn) - fy)
297 b = 200 * (fy - f(z/zn))
299 return l, a, b
301 @staticmethod
302 def lab_to_xyz(l, a, b):
303 # D65 Illuminant aka sRGB(1,1,1)
304 xn = 0.950489
305 yn = 1
306 zn = 108.8840
308 delta = 6 / 29
310 def f1(t):
311 return t**3 if t > delta else 3*delta**2*(t-4/29)
313 l1 = (l+16) / 116
314 x = xn * f1(l1+a/500)
315 y = yn * f1(l1)
316 z = zn * f1(l1-b/200)
318 return x, y, z
320 #@staticmethod
321 #def lab_to_lch_ab(l, a, b):
322 #c = math.hypot(a, b)
323 #h = math.atan2(b, a)
324 #if h < 0:
325 #h += math.tau
326 #return l, c, h
328 #@staticmethod
329 #def lch_ab_to_lab(l, c, h):
330 #a = math.cos(h) * c
331 #b = math.sin(h) * c
332 #return l, a, b
334 @staticmethod
335 def conv_func(mode_from, mode_to):
336 return getattr(Conversion, "%s_to_%s" % (mode_from.name.lower(), mode_to.name.lower()), None)
338 @staticmethod
339 def convert(tuple, mode_from, mode_to):
340 if mode_from == mode_to: 340 ↛ 341line 340 didn't jump to line 341, because the condition on line 340 was never true
341 return tuple
343 if len(tuple) == 4:
344 alpha = tuple[3]
345 tuple = tuple[:3]
346 else:
347 alpha = None
349 func = Conversion.conv_func(mode_from, mode_to)
350 if func: 350 ↛ 353line 350 didn't jump to line 353, because the condition on line 350 was never false
351 return func(*tuple)
353 if (mode_from, mode_to) in Conversion._conv_paths:
354 steps = Conversion._conv_paths[(mode_from, mode_to)] + [mode_to]
355 for step in steps:
356 func = Conversion.conv_func(mode_from, step)
357 if not func:
358 raise ValueError("Missing definition for conversion from %s to %s" % (mode_from, step))
359 tuple = func(*tuple)
360 mode_from = step
361 if alpha is not None:
362 tuple += (alpha,)
363 return tuple
365 raise ValueError("No conversion path from %s to %s" % (mode_from, mode_to))
368class Color(NVector):
369 Mode = ColorMode
371 def __init__(self, c1=0, c2=0, c3=0, a=1, *, mode=ColorMode.RGB):
372 if isinstance(a, ColorMode): 372 ↛ 373line 372 didn't jump to line 373, because the condition on line 372 was never true
373 raise TypeError("Please update the Color constructor")
374 super().__init__(c1, c2, c3, a)
375 self._mode = mode
377 @property
378 def mode(self):
379 return self._mode
381 def convert(self, v):
382 if v == self._mode:
383 return self
385 self.components = list(Conversion.convert(self.components, self._mode, v))
387 self._mode = v
388 return self
390 def clone(self):
391 return Color(*self.components, mode=self._mode)
393 def converted(self, mode):
394 return self.clone().convert(mode)
396 def to_rgb(self):
397 return self.converted(ColorMode.RGB)
399 def __repr__(self):
400 return "<%s %s [%.3f, %.3f, %.3f, %.3f]>" % (
401 (self.__class__.__name__, self.mode.name) + tuple(self.components)
402 )
404 def component_names(self):
405 comps = None
407 if self._mode == ColorMode.RGB:
408 comps = [{"r", "red"}, {"g", "green"}, {"b", "blue"}]
409 elif self._mode == ColorMode.HSV:
410 comps = [{"h", "hue"}, {"s", "saturation"}, {"v", "value"}]
411 elif self._mode == ColorMode.HSL:
412 comps = [{"h", "hue"}, {"s", "saturation"}, {"l", "lightness"}]
413 elif self._mode == ColorMode.LCH_uv: # in (ColorMode.LCH_uv, ColorMode.LCH_ab):
414 comps = [{"l", "luma", "luminance"}, {"c", "choma"}, {"h", "hue"}]
415 elif self._mode == ColorMode.XYZ:
416 comps = ["x", "y", "z"]
417 elif self._mode == ColorMode.LUV:
418 comps = ["l", "u", "v"]
419 elif self._mode == ColorMode.LAB:
420 comps = ["l", "a", "b"]
422 comps.append({"a", "alpha"})
424 return comps
426 def _attrindex(self, name):
427 comps = self.component_names()
429 if comps:
430 for i, vals in enumerate(comps):
431 if name in vals:
432 return i
434 return None
436 def __getattr__(self, name):
437 if name not in vars(self) and name not in {"_mode", "components"}:
438 i = self._attrindex(name)
439 if i is not None:
440 return self.components[i]
441 raise AttributeError(name)
443 def __setattr__(self, name, value):
444 if name not in vars(self) and name not in {"_mode", "components"}: 444 ↛ 445line 444 didn't jump to line 445, because the condition on line 444 was never true
445 i = self._attrindex(name)
446 if i is not None:
447 self.components[i] = value
448 return
449 return super().__setattr__(name, value)
452def color_to_hex(color: Color):
453 conv = color.to_rgb()
454 return "#%02x%02x%02x" % tuple(map(int, (conv * 255).components[:3]))
457def color_from_hex(hex: str):
458 from ..parsers.svg.importer import parse_color
459 return parse_color(hex)