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

1import enum 

2import math 

3import colorsys 

4from ..nvector import NVector 

5 

6 

7def from_uint8(r, g, b, a=255): 

8 return Color(r, g, b, a) / 255 

9 

10 

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() 

28 

29 

30def _clamp(x): 

31 return max(0, min(1, x)) 

32 

33 

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], 

44 

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], 

53 

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], 

62 

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], 

71 

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], 

80 

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], 

89 

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): [], 

98 

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 } 

108 

109 @staticmethod 

110 def rgb_to_hsv(r, g, b): 

111 return colorsys.rgb_to_hsv(r, g, b) 

112 

113 @staticmethod 

114 def hsv_to_rgb(h, s, v): 

115 return colorsys.hsv_to_rgb(h, s, v) 

116 

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) 

122 

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) 

128 

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) 

133 

134 @staticmethod 

135 def hsl_to_rgb(h, s, l): 

136 return colorsys.hls_to_rgb(h, l, s) 

137 

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) 

155 

156 #@staticmethod 

157 #def hcl_to_rgb(h, c, l, gamma=3, y0=100): 

158 #h *= 2*math.pi 

159 

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 

163 

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 

194 

195 #return _clamp(r), _clamp(g), _clamp(b) 

196 

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 ) 

211 

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 ))) 

226 

227 @staticmethod 

228 def xyz_to_luv(x, y, z): 

229 u1r = 0.2009 

230 v1r = 0.4610 

231 yr = 100 

232 

233 kap = (29/3)**3 

234 eps = (6/29)**3 

235 

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 

241 

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 

247 

248 @staticmethod 

249 def luv_to_xyz(l, u, v): 

250 u1r = 0.2009 

251 v1r = 0.4610 

252 yr = 100 

253 

254 kap = (29/3)**3 

255 

256 if l == 0: 

257 u1 = u1r 

258 v1 = v1r 

259 else: 

260 u1 = u / (13 * l) + u1r 

261 v1 = v / (13 * l) + v1r 

262 

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 

267 

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 

275 

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 

281 

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 

288 

289 delta = 6 / 29 

290 

291 def f(t): 

292 return t ** (1/3) if t > delta ** 3 else t / (3*delta**2) + 4/29 

293 

294 fy = f(y/yn) 

295 l = 116 * fy - 16 

296 a = 500 * (f(x/xn) - fy) 

297 b = 200 * (fy - f(z/zn)) 

298 

299 return l, a, b 

300 

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 

307 

308 delta = 6 / 29 

309 

310 def f1(t): 

311 return t**3 if t > delta else 3*delta**2*(t-4/29) 

312 

313 l1 = (l+16) / 116 

314 x = xn * f1(l1+a/500) 

315 y = yn * f1(l1) 

316 z = zn * f1(l1-b/200) 

317 

318 return x, y, z 

319 

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 

327 

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 

333 

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) 

337 

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 

342 

343 if len(tuple) == 4: 

344 alpha = tuple[3] 

345 tuple = tuple[:3] 

346 else: 

347 alpha = None 

348 

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) 

352 

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 

364 

365 raise ValueError("No conversion path from %s to %s" % (mode_from, mode_to)) 

366 

367 

368class Color(NVector): 

369 Mode = ColorMode 

370 

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 

376 

377 @property 

378 def mode(self): 

379 return self._mode 

380 

381 def convert(self, v): 

382 if v == self._mode: 

383 return self 

384 

385 self.components = list(Conversion.convert(self.components, self._mode, v)) 

386 

387 self._mode = v 

388 return self 

389 

390 def clone(self): 

391 return Color(*self.components, mode=self._mode) 

392 

393 def converted(self, mode): 

394 return self.clone().convert(mode) 

395 

396 def to_rgb(self): 

397 return self.converted(ColorMode.RGB) 

398 

399 def __repr__(self): 

400 return "<%s %s [%.3f, %.3f, %.3f, %.3f]>" % ( 

401 (self.__class__.__name__, self.mode.name) + tuple(self.components) 

402 ) 

403 

404 def component_names(self): 

405 comps = None 

406 

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"] 

421 

422 comps.append({"a", "alpha"}) 

423 

424 return comps 

425 

426 def _attrindex(self, name): 

427 comps = self.component_names() 

428 

429 if comps: 

430 for i, vals in enumerate(comps): 

431 if name in vals: 

432 return i 

433 

434 return None 

435 

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) 

442 

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) 

450 

451 

452def color_to_hex(color: Color): 

453 conv = color.to_rgb() 

454 return "#%02x%02x%02x" % tuple(map(int, (conv * 255).components[:3])) 

455 

456 

457def color_from_hex(hex: str): 

458 from ..parsers.svg.importer import parse_color 

459 return parse_color(hex)