1# coding=utf-8
2#
3# Copyright (C) 2006 Jos Hirth, kaioa.com
4# Copyright (C) 2007 Aaron C. Spike
5# Copyright (C) 2009 Monash University
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
20#
21"""
22Basic color controls
23"""
24
25from .tween import interpcoord
26
27# All the names that get added to the inkex API itself.
28__all__ = ('Color', 'ColorError', 'ColorIdError')
29
30SVG_COLOR = {
31    'aliceblue': '#f0f8ff',
32    'antiquewhite': '#faebd7',
33    'aqua': '#00ffff',
34    'aquamarine': '#7fffd4',
35    'azure': '#f0ffff',
36    'beige': '#f5f5dc',
37    'bisque': '#ffe4c4',
38    'black': '#000000',
39    'blanchedalmond': '#ffebcd',
40    'blue': '#0000ff',
41    'blueviolet': '#8a2be2',
42    'brown': '#a52a2a',
43    'burlywood': '#deb887',
44    'cadetblue': '#5f9ea0',
45    'chartreuse': '#7fff00',
46    'chocolate': '#d2691e',
47    'coral': '#ff7f50',
48    'cornflowerblue': '#6495ed',
49    'cornsilk': '#fff8dc',
50    'crimson': '#dc143c',
51    'cyan': '#00ffff',
52    'darkblue': '#00008b',
53    'darkcyan': '#008b8b',
54    'darkgoldenrod': '#b8860b',
55    'darkgray': '#a9a9a9',
56    'darkgreen': '#006400',
57    'darkgrey': '#a9a9a9',
58    'darkkhaki': '#bdb76b',
59    'darkmagenta': '#8b008b',
60    'darkolivegreen': '#556b2f',
61    'darkorange': '#ff8c00',
62    'darkorchid': '#9932cc',
63    'darkred': '#8b0000',
64    'darksalmon': '#e9967a',
65    'darkseagreen': '#8fbc8f',
66    'darkslateblue': '#483d8b',
67    'darkslategray': '#2f4f4f',
68    'darkslategrey': '#2f4f4f',
69    'darkturquoise': '#00ced1',
70    'darkviolet': '#9400d3',
71    'deeppink': '#ff1493',
72    'deepskyblue': '#00bfff',
73    'dimgray': '#696969',
74    'dimgrey': '#696969',
75    'dodgerblue': '#1e90ff',
76    'firebrick': '#b22222',
77    'floralwhite': '#fffaf0',
78    'forestgreen': '#228b22',
79    'fuchsia': '#ff00ff',
80    'gainsboro': '#dcdcdc',
81    'ghostwhite': '#f8f8ff',
82    'gold': '#ffd700',
83    'goldenrod': '#daa520',
84    'gray': '#808080',
85    'grey': '#808080',
86    'green': '#008000',
87    'greenyellow': '#adff2f',
88    'honeydew': '#f0fff0',
89    'hotpink': '#ff69b4',
90    'indianred': '#cd5c5c',
91    'indigo': '#4b0082',
92    'ivory': '#fffff0',
93    'khaki': '#f0e68c',
94    'lavender': '#e6e6fa',
95    'lavenderblush': '#fff0f5',
96    'lawngreen': '#7cfc00',
97    'lemonchiffon': '#fffacd',
98    'lightblue': '#add8e6',
99    'lightcoral': '#f08080',
100    'lightcyan': '#e0ffff',
101    'lightgoldenrodyellow': '#fafad2',
102    'lightgray': '#d3d3d3',
103    'lightgreen': '#90ee90',
104    'lightgrey': '#d3d3d3',
105    'lightpink': '#ffb6c1',
106    'lightsalmon': '#ffa07a',
107    'lightseagreen': '#20b2aa',
108    'lightskyblue': '#87cefa',
109    'lightslategray': '#778899',
110    'lightslategrey': '#778899',
111    'lightsteelblue': '#b0c4de',
112    'lightyellow': '#ffffe0',
113    'lime': '#00ff00',
114    'limegreen': '#32cd32',
115    'linen': '#faf0e6',
116    'magenta': '#ff00ff',
117    'maroon': '#800000',
118    'mediumaquamarine': '#66cdaa',
119    'mediumblue': '#0000cd',
120    'mediumorchid': '#ba55d3',
121    'mediumpurple': '#9370db',
122    'mediumseagreen': '#3cb371',
123    'mediumslateblue': '#7b68ee',
124    'mediumspringgreen': '#00fa9a',
125    'mediumturquoise': '#48d1cc',
126    'mediumvioletred': '#c71585',
127    'midnightblue': '#191970',
128    'mintcream': '#f5fffa',
129    'mistyrose': '#ffe4e1',
130    'moccasin': '#ffe4b5',
131    'navajowhite': '#ffdead',
132    'navy': '#000080',
133    'oldlace': '#fdf5e6',
134    'olive': '#808000',
135    'olivedrab': '#6b8e23',
136    'orange': '#ffa500',
137    'orangered': '#ff4500',
138    'orchid': '#da70d6',
139    'palegoldenrod': '#eee8aa',
140    'palegreen': '#98fb98',
141    'paleturquoise': '#afeeee',
142    'palevioletred': '#db7093',
143    'papayawhip': '#ffefd5',
144    'peachpuff': '#ffdab9',
145    'peru': '#cd853f',
146    'pink': '#ffc0cb',
147    'plum': '#dda0dd',
148    'powderblue': '#b0e0e6',
149    'purple': '#800080',
150    'rebeccapurple': '#663399',
151    'red': '#ff0000',
152    'rosybrown': '#bc8f8f',
153    'royalblue': '#4169e1',
154    'saddlebrown': '#8b4513',
155    'salmon': '#fa8072',
156    'sandybrown': '#f4a460',
157    'seagreen': '#2e8b57',
158    'seashell': '#fff5ee',
159    'sienna': '#a0522d',
160    'silver': '#c0c0c0',
161    'skyblue': '#87ceeb',
162    'slateblue': '#6a5acd',
163    'slategray': '#708090',
164    'slategrey': '#708090',
165    'snow': '#fffafa',
166    'springgreen': '#00ff7f',
167    'steelblue': '#4682b4',
168    'tan': '#d2b48c',
169    'teal': '#008080',
170    'thistle': '#d8bfd8',
171    'tomato': '#ff6347',
172    'turquoise': '#40e0d0',
173    'violet': '#ee82ee',
174    'wheat': '#f5deb3',
175    'white': '#ffffff',
176    'whitesmoke': '#f5f5f5',
177    'yellow': '#ffff00',
178    'yellowgreen': '#9acd32',
179    'none': None,
180}
181COLOR_SVG = dict([(value, name) for name, value in SVG_COLOR.items()])
182
183def is_color(color):
184    """Determine if it is a color that we can use. If not, leave it unchanged."""
185    try:
186        return bool(Color(color))
187    except ColorError:
188        return False
189
190def constrain(minim, value, maxim, channel):
191    """Returns the value so long as it is between min and max values"""
192    if channel == 'h': # Hue
193        return value % maxim # Wrap around hue value
194    return min([maxim, max([minim, value])])
195
196class ColorError(KeyError):
197    """Specific color parsing error"""
198
199class ColorIdError(ColorError):
200    """Special color error for gradient and color stop ids"""
201
202class Color(list):
203    """An RGB array for the color"""
204    red = property(lambda self: self.to_rgb()[0])
205    red = red.setter(lambda self, value: self._set(0, value))
206    green = property(lambda self: self.to_rgb()[1])
207    green = green.setter(lambda self, value: self._set(1, value))
208    blue = property(lambda self: self.to_rgb()[2])
209    blue = blue.setter(lambda self, value: self._set(2, value))
210    alpha = property(lambda self: self.to_rgba()[3])
211    alpha = alpha.setter(lambda self, value: self._set(3, value, ('rgba',)))
212    hue = property(lambda self: self.to_hsl()[0])
213    hue = hue.setter(lambda self, value: self._set(0, value, ('hsl',)))
214    saturation = property(lambda self: self.to_hsl()[1])
215    saturation = saturation.setter(lambda self, value: self._set(1, value, ('hsl',)))
216    lightness = property(lambda self: self.to_hsl()[2])
217    lightness = lightness.setter(lambda self, value: self._set(2, value, ('hsl',)))
218
219    def __init__(self, color=None, space='rgb'):
220        super().__init__()
221        if isinstance(color, Color):
222            space, color = color.space, list(color)
223
224        if isinstance(color, str):
225            # String from xml or css attributes
226            space, color = self.parse_str(color.strip())
227
228        if isinstance(color, int):
229            # Number from arg parser colour value
230            space, color = self.parse_int(color)
231
232        # Empty list means 'none', or no color
233        if color is None:
234            color = []
235
236        if not isinstance(color, (list, tuple)):
237            raise ColorError("Not a known a color value")
238
239        self.space = space
240        try:
241            for val in color:
242                self.append(val)
243        except ValueError:
244            raise ColorError("Bad color list")
245
246    def __hash__(self):
247        """Allow colors to be hashable"""
248        return tuple(self.to_rgba()).__hash__()
249
250    def _set(self, index, value, spaces=('rgb', 'rgba')):
251        """Set the color value in place, limits setter to specific color space"""
252        # Named colors are just rgb, so dump name memory
253        if self.space == 'named':
254            self.space = 'rgb'
255        if not self.space in spaces:
256            if index == 3 and self.space == 'rgb':
257                # Special, add alpha, don't convert back to rgb
258                self.space = 'rgba'
259                self.append(constrain(0.0, float(value), 1.0, 'a'))
260                return
261            # Set in other colour space and convert back and forth
262            target = self.to(spaces[0])
263            target[index] = constrain(0, int(value), 255, spaces[0][index])
264            self[:] = target.to(self.space)
265            return
266        self[index] = constrain(0, int(value), 255, spaces[0][index])
267
268    def append(self, val):
269        """Append a value to the local list"""
270        if len(self) == len(self.space):
271            raise ValueError("Can't add any more values to color.")
272
273        if isinstance(val, str):
274            val = val.strip()
275            if val.endswith('%'):
276                val = float(val.strip('%')) / 100
277            else:
278                val = float(val)
279
280        end_type = int
281        if len(self) == 3: # Alpha value
282            val = min([1.0, val])
283            end_type = float
284        elif isinstance(val, float) and val <= 1.0:
285            val *= 255
286
287        if isinstance(val, (int, float)):
288            super().append(max(end_type(val), 0))
289
290    @staticmethod
291    def parse_str(color):
292        """Creates a rgb int array"""
293        # Handle pre-defined svg color values
294        if color and color.lower() in SVG_COLOR:
295            return 'named', Color.parse_str(SVG_COLOR[color.lower()])[1]
296
297        if color is None:
298            return 'rgb', None
299
300        if color.startswith('url('):
301            raise ColorIdError("Color references other element id, e.g. a gradient")
302
303        # Next handle short colors (css: #abc -> #aabbcc)
304        if color.startswith('#'):
305            # Remove any icc or ilab directives
306            # FUTURE: We could use icc or ilab information
307            col = color.split(' ')[0]
308            if len(col) == 4:
309                col = '#{1}{1}{2}{2}{3}{3}'.format(*col)
310
311            # Convert hex to integers
312            try:
313                return 'rgb', (int(col[1:3], 16), int(col[3:5], 16), int(col[5:], 16))
314            except ValueError:
315                raise ColorError(f"Bad RGB hex color value {col}")
316
317        # Handle other css color values
318        elif '(' in color and ')' in color:
319            space, values = color.lower().strip().strip(')').split('(')
320            return space, values.split(',')
321
322        try:
323            return Color.parse_int(int(color))
324        except ValueError:
325            pass
326
327        raise ColorError(f"Unknown color format: {color}")
328
329    @staticmethod
330    def parse_int(color):
331        """Creates an rgb or rgba from a long int"""
332        space = 'rgb'
333        color = [
334            ((color >> 24) & 255), # red
335            ((color >> 16) & 255), # green
336            ((color >> 8) & 255), # blue
337            ((color & 255) / 255.), # opacity
338        ]
339        if color[-1] == 1.0:
340            color.pop()
341        else:
342            space = 'rgba'
343        return space, color
344
345    def __str__(self):
346        """int array to #rrggbb"""
347        if not self:
348            return 'none'
349        if self.space == 'named':
350            rgbhex = '#{0:02x}{1:02x}{2:02x}'.format(*self)
351            if rgbhex in COLOR_SVG:
352                return COLOR_SVG[rgbhex]
353            self.space = 'rgb'
354        if self.space == 'rgb':
355            return '#{0:02x}{1:02x}{2:02x}'.format(*self)
356        if self.space == 'rgba':
357            if self[3] == 1.0:
358                return 'rgb({:g}, {:g}, {:g})'.format(*self[:3])
359            return 'rgba({:g}, {:g}, {:g}, {:g})'.format(*self)
360        elif self.space == 'hsl':
361            return 'hsl({0:g}, {1:g}, {2:g})'.format(*self)
362        raise ColorError(f"Can't print colour space '{self.space}'")
363
364    def __int__(self):
365        """int array to large integer"""
366        if not self:
367            return -1
368        color = self.to_rgba()
369        return (color[0] << 24) + (color[1] << 16) + (color[2] << 8) + (int(color[3] * 255))
370
371    def to(self, space):
372        """Dynamic caller for to_hsl, to_rgb, etc"""
373        return getattr(self, 'to_' + space)()
374
375    def to_hsl(self):
376        """Turn this color into a Hue/Saturation/Lightness colour space"""
377        if not self and self.space in ('rgb', 'named'):
378            return self.to_rgb().to_hsl()
379        if self.space == 'hsl':
380            return self
381        elif self.space == 'rgb':
382            return Color(rgb_to_hsl(*self.to_floats()), space='hsl')
383        raise ColorError(f"Unknown color conversion {self.space}->hsl")
384
385    def to_rgb(self):
386        """Turn this color into a Red/Green/Blue colour space"""
387        if not self and self.space in ('rgb', 'named'):
388            return Color([0, 0, 0])
389        if self.space == 'rgb':
390            return self
391        if self.space in ('rgba', 'named'):
392            return Color(self[:3], space='rgb')
393        elif self.space == 'hsl':
394            return Color(hsl_to_rgb(*self.to_floats()), space='rgb')
395        raise ColorError(f"Unknown color conversion {self.space}->rgb")
396
397    def to_rgba(self, alpha=1.0):
398        """Turn this color isn't an RGB with Alpha colour space"""
399        if self.space == 'rgba':
400            return self
401        return Color(self.to_rgb() + [alpha], 'rgba')
402
403    def to_floats(self):
404        """Returns the colour values as percentage floats (0.0 - 1.0)"""
405        return [val / 255.0 for val in self]
406
407    def to_named(self):
408        """Convert this color to a named color if possible"""
409        if not self:
410            return Color()
411        return Color(COLOR_SVG.get(str(self), str(self)))
412
413    def interpolate(self, other, fraction):
414        """Iterpolate two colours by the given fraction"""
415        return Color(
416            [interpcoord(c1, c2, fraction)
417             for (c1, c2) in zip(self.to_floats(), other.to_floats())]
418            )
419
420
421def rgb_to_hsl(red, green, blue):
422    """RGB to HSL colour conversion"""
423    rgb_max = max(red, green, blue)
424    rgb_min = min(red, green, blue)
425    delta = rgb_max - rgb_min
426    hsl = [0.0, 0.0, (rgb_max + rgb_min) / 2.0]
427    if delta != 0:
428        if hsl[2] <= 0.5:
429            hsl[1] = delta / (rgb_max + rgb_min)
430        else:
431            hsl[1] = delta / (2 - rgb_max - rgb_min)
432
433        if red == rgb_max:
434            hsl[0] = (green - blue) / delta
435        elif green == rgb_max:
436            hsl[0] = 2.0 + (blue - red) / delta
437        elif blue == rgb_max:
438            hsl[0] = 4.0 + (red - green) / delta
439
440        hsl[0] /= 6.0
441        if hsl[0] < 0:
442            hsl[0] += 1
443        if hsl[0] > 1:
444            hsl[0] -= 1
445    return hsl
446
447
448def hsl_to_rgb(hue, sat, light):
449    """HSL to RGB Color Conversion"""
450    if sat == 0:
451        return [light, light, light]  # Gray
452
453    if light < 0.5:
454        val2 = light * (1 + sat)
455    else:
456        val2 = light + sat - light * sat
457    val1 = 2 * light - val2
458    return [_hue_to_rgb(val1, val2, hue * 6 + 2.0),
459            _hue_to_rgb(val1, val2, hue * 6),
460            _hue_to_rgb(val1, val2, hue * 6 - 2.0)]
461
462
463def _hue_to_rgb(val1, val2, hue):
464    if hue < 0:
465        hue += 6.0
466    if hue > 6:
467        hue -= 6.0
468    if hue < 1:
469        return val1 + (val2 - val1) * hue
470    if hue < 3:
471        return val2
472    if hue < 4:
473        return val1 + (val2 - val1) * (4 - hue)
474    return val1
475