1import math
2
3try:
4 import numpy as np
5 import pwkit.colormaps
6except ImportError as e:
7 raise ImportError(
8 'Optional dependencies not installed. '
9 'Install with `python3 -m pip install "drawsvg[color]"'
10 ) from e
11
12
13# Most calculations from http://www.chilliant.com/rgb2hsv.html
14
15
16def limit(v, low=0, high=1):
17 return max(min(v, high), low)
18
19class Srgb:
20 LUMA_WEIGHTS = (0.299, 0.587, 0.114)
21 def __init__(self, r, g, b):
22 self.r = float(r)
23 self.g = float(g)
24 self.b = float(b)
25 def __iter__(self):
26 return iter((self.r, self.g, self.b))
27 def __repr__(self):
28 return 'RGB({}, {}, {})'.format(self.r, self.g, self.b)
29 def __str__(self):
30 return 'rgb({}%,{}%,{}%)'.format(self.r*100, self.g*100, self.b*100)
31 def luma(self, wts=None):
32 if wts is None: wts = self.LUMA_WEIGHTS
33 rw, gw, bw = wts
34 return rw*self.r + gw*self.g + bw*self.b
35 def to_srgb(self):
36 return self
37 @staticmethod
38 def from_hue(h):
39 h = h % 1
40 r = abs(h * 6 - 3) - 1
41 g = 2 - abs(h * 6 - 2)
42 b = 2 - abs(h * 6 - 4)
43 return Srgb(limit(r), limit(g), limit(b))
44
45class Hsl:
46 def __init__(self, h, s, l):
47 self.h = float(h) % 1
48 self.s = float(s)
49 self.l = float(l)
50 def __iter__(self):
51 return iter((self.h, self.s, self.l))
52 def __repr__(self):
53 return 'HSL({}, {}, {})'.format(self.h, self.s, self.l)
54 def __str__(self):
55 r, g, b = self.to_srgb()
56 return 'rgb({}%,{}%,{}%)'.format(
57 round(r*100, 2), round(g*100, 2), round(b*100, 2))
58 def to_srgb(self):
59 hs = Srgb.from_hue(self.h)
60 c = (1 - abs(2 * self.l - 1)) * self.s
61 return Srgb(
62 (hs.r - 0.5) * c + self.l,
63 (hs.g - 0.5) * c + self.l,
64 (hs.b - 0.5) * c + self.l
65 )
66
67class Hsv:
68 def __init__(self, h, s, v):
69 self.h = float(h) % 1
70 self.s = float(s)
71 self.v = float(v)
72 def __iter__(self):
73 return iter((self.h, self.s, self.v))
74 def __repr__(self):
75 return 'HSV({}, {}, {})'.format(self.h, self.s, self.v)
76 def __str__(self):
77 r, g, b = self.to_srgb()
78 return 'rgb({}%,{}%,{}%)'.format(
79 round(r*100, 2), round(g*100, 2), round(b*100, 2))
80 def to_srgb(self):
81 hs = Srgb.from_hue(self.h)
82 c = self.v * self.s
83 hp = self.h * 6
84 x = c * (1 - abs(hp % 2 - 1))
85 if hp < 1:
86 r1, g1, b1 = c, x, 0
87 elif hp < 2:
88 r1, g1, b1 = x, c, 0
89 elif hp < 3:
90 r1, g1, b1 = 0, c, x
91 elif hp < 4:
92 r1, g1, b1 = 0, x, c
93 elif hp < 5:
94 r1, g1, b1 = x, 0, c
95 else:
96 r1, g1, b1 = c, 0, x
97 m = self.v - c
98 return Srgb(r1+m, g1+m, b1+m)
99
100class Sin:
101 def __init__(self, h, s, l):
102 self.h = float(h) % 1
103 self.s = float(s)
104 self.l = float(l)
105 def __iter__(self):
106 return iter((self.h, self.s, self.l))
107 def __repr__(self):
108 return 'Sin({}, {}, {})'.format(self.h, self.s, self.l)
109 def __str__(self):
110 r, g, b = self.to_srgb()
111 return 'rgb({}%,{}%,{}%)'.format(
112 round(r*100, 2), round(g*100, 2), round(b*100, 2))
113 def to_srgb(self):
114 h = self.h
115 scale = self.s / 2
116 shift = self.l #* (1-2*scale)
117 return Srgb(
118 shift + scale * math.cos(math.pi*2 * (h - 0/6)),
119 shift + scale * math.cos(math.pi*2 * (h - 2/6)),
120 shift + scale * math.cos(math.pi*2 * (h - 4/6)),
121 )
122
123class Hcy:
124 HCY_WEIGHTS = Srgb.LUMA_WEIGHTS
125 def __init__(self, h, c, y):
126 self.h = float(h) % 1
127 self.c = float(c)
128 self.y = float(y)
129 def __iter__(self):
130 return iter((self.h, self.c, self.y))
131 def __repr__(self):
132 return 'HCY({}, {}, {})'.format(self.h, self.c, self.y)
133 def __str__(self):
134 r, g, b = self.to_srgb()
135 return 'rgb({}%,{}%,{}%)'.format(r*100, g*100, b*100)
136 def to_srgb(self):
137 hs = Srgb.from_hue(self.h)
138 y = hs.luma(wts=self.HCY_WEIGHTS)
139 c = self.c
140 if self.y < y:
141 c *= self.y / y
142 elif y < 1:
143 c *= (1 - self.y) / (1 - y)
144 return Srgb(
145 (hs.r - y) * c + self.y,
146 (hs.g - y) * c + self.y,
147 (hs.b - y) * c + self.y,
148 )
149 @staticmethod
150 def _rgb_to_hcv(srgb):
151 if srgb.g < srgb.b:
152 p = (srgb.b, srgb.g, -1., 2./3.)
153 else:
154 p = (srgb.g, srgb.b, 0., -1./3.)
155 if srgb.r < p[0]:
156 q = (p[0], p[1], p[3], srgb.r)
157 else:
158 q = (srgb.r, p[1], p[2], p[0])
159 c = q[0] - min(q[3], q[1])
160 h = abs((q[3] - q[1]) / (6*c + 1e-10) + q[2])
161 return (h, c, q[0])
162 @classmethod
163 def from_srgb(cls, srgb):
164 hcv = list(cls._rgb_to_hcv(srgb))
165 rw, gw, bw = cls.HCY_WEIGHTS
166 y = rw*srgb.r + gw*srgb.g + bw*srgb.b
167 hs = Srgb.from_hue(hcv[0])
168 z = rw*hs.r + gw*hs.g + bw*hs.b
169 if y < z:
170 hcv[1] *= z / (y + 1e-10)
171 else:
172 hcv[1] *= (1 - z) / (1 - y + 1e-10)
173 return Hcy(hcv[0], hcv[1], y)
174
175class Cielab:
176 REF_WHITE = (0.95047, 1., 1.08883)
177 def __init__(self, l, a, b):
178 self.l = float(l)
179 self.a = float(a)
180 self.b = float(b)
181 def __iter__(self):
182 return iter((self.l, self.a, self.b))
183 def __repr__(self):
184 return 'CIELAB({}, {}, {})'.format(self.l, self.a, self.b)
185 def __str__(self):
186 r, g, b = self.to_srgb()
187 return 'rgb({}%,{}%,{}%)'.format(
188 round(r*100, 2), round(g*100, 2), round(b*100, 2))
189 def to_srgb(self):
190 in_arr = np.array((self.l, self.a, self.b))
191 xyz = pwkit.colormaps.cielab_to_xyz(in_arr, self.REF_WHITE)
192 lin_srgb = pwkit.colormaps.xyz_to_linsrgb(xyz)
193 r, g, b = pwkit.colormaps.linsrgb_to_srgb(lin_srgb)
194 return Srgb(r, g, b)
195 @classmethod
196 def from_srgb(cls, srgb, ref_white=None):
197 if ref_white is None: ref_white = cls.REF_WHITE
198 in_arr = np.array((*srgb,), dtype=float)
199 lin_srgb = pwkit.colormaps.srgb_to_linsrgb(in_arr)
200 xyz = pwkit.colormaps.linsrgb_to_xyz(lin_srgb)
201 l, a, b = pwkit.colormaps.xyz_to_cielab(xyz, ref_white)
202 return Cielab(l, a, b)