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