# coding=utf-8
#
# Copyright (C) 2006 Jos Hirth, kaioa.com
# Copyright (C) 2007 Aaron C. Spike
# Copyright (C) 2009 Monash University
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
"""
Basic color controls
"""
# All the names that get added to the inkex API itself.
__all__ = ('Color',)
SVG_COLOR = {
'aliceblue': '#f0f8ff',
'antiquewhite': '#faebd7',
'aqua': '#00ffff',
'aquamarine': '#7fffd4',
'azure': '#f0ffff',
'beige': '#f5f5dc',
'bisque': '#ffe4c4',
'black': '#000000',
'blanchedalmond': '#ffebcd',
'blue': '#0000ff',
'blueviolet': '#8a2be2',
'brown': '#a52a2a',
'burlywood': '#deb887',
'cadetblue': '#5f9ea0',
'chartreuse': '#7fff00',
'chocolate': '#d2691e',
'coral': '#ff7f50',
'cornflowerblue': '#6495ed',
'cornsilk': '#fff8dc',
'crimson': '#dc143c',
'cyan': '#00ffff',
'darkblue': '#00008b',
'darkcyan': '#008b8b',
'darkgoldenrod': '#b8860b',
'darkgray': '#a9a9a9',
'darkgreen': '#006400',
'darkgrey': '#a9a9a9',
'darkkhaki': '#bdb76b',
'darkmagenta': '#8b008b',
'darkolivegreen': '#556b2f',
'darkorange': '#ff8c00',
'darkorchid': '#9932cc',
'darkred': '#8b0000',
'darksalmon': '#e9967a',
'darkseagreen': '#8fbc8f',
'darkslateblue': '#483d8b',
'darkslategray': '#2f4f4f',
'darkslategrey': '#2f4f4f',
'darkturquoise': '#00ced1',
'darkviolet': '#9400d3',
'deeppink': '#ff1493',
'deepskyblue': '#00bfff',
'dimgray': '#696969',
'dimgrey': '#696969',
'dodgerblue': '#1e90ff',
'firebrick': '#b22222',
'floralwhite': '#fffaf0',
'forestgreen': '#228b22',
'fuchsia': '#ff00ff',
'gainsboro': '#dcdcdc',
'ghostwhite': '#f8f8ff',
'gold': '#ffd700',
'goldenrod': '#daa520',
'gray': '#808080',
'grey': '#808080',
'green': '#008000',
'greenyellow': '#adff2f',
'honeydew': '#f0fff0',
'hotpink': '#ff69b4',
'indianred': '#cd5c5c',
'indigo': '#4b0082',
'ivory': '#fffff0',
'khaki': '#f0e68c',
'lavender': '#e6e6fa',
'lavenderblush': '#fff0f5',
'lawngreen': '#7cfc00',
'lemonchiffon': '#fffacd',
'lightblue': '#add8e6',
'lightcoral': '#f08080',
'lightcyan': '#e0ffff',
'lightgoldenrodyellow': '#fafad2',
'lightgray': '#d3d3d3',
'lightgreen': '#90ee90',
'lightgrey': '#d3d3d3',
'lightpink': '#ffb6c1',
'lightsalmon': '#ffa07a',
'lightseagreen': '#20b2aa',
'lightskyblue': '#87cefa',
'lightslategray': '#778899',
'lightslategrey': '#778899',
'lightsteelblue': '#b0c4de',
'lightyellow': '#ffffe0',
'lime': '#00ff00',
'limegreen': '#32cd32',
'linen': '#faf0e6',
'magenta': '#ff00ff',
'maroon': '#800000',
'mediumaquamarine': '#66cdaa',
'mediumblue': '#0000cd',
'mediumorchid': '#ba55d3',
'mediumpurple': '#9370db',
'mediumseagreen': '#3cb371',
'mediumslateblue': '#7b68ee',
'mediumspringgreen': '#00fa9a',
'mediumturquoise': '#48d1cc',
'mediumvioletred': '#c71585',
'midnightblue': '#191970',
'mintcream': '#f5fffa',
'mistyrose': '#ffe4e1',
'moccasin': '#ffe4b5',
'navajowhite': '#ffdead',
'navy': '#000080',
'oldlace': '#fdf5e6',
'olive': '#808000',
'olivedrab': '#6b8e23',
'orange': '#ffa500',
'orangered': '#ff4500',
'orchid': '#da70d6',
'palegoldenrod': '#eee8aa',
'palegreen': '#98fb98',
'paleturquoise': '#afeeee',
'palevioletred': '#db7093',
'papayawhip': '#ffefd5',
'peachpuff': '#ffdab9',
'peru': '#cd853f',
'pink': '#ffc0cb',
'plum': '#dda0dd',
'powderblue': '#b0e0e6',
'purple': '#800080',
'rebeccapurple': '#663399',
'red': '#ff0000',
'rosybrown': '#bc8f8f',
'royalblue': '#4169e1',
'saddlebrown': '#8b4513',
'salmon': '#fa8072',
'sandybrown': '#f4a460',
'seagreen': '#2e8b57',
'seashell': '#fff5ee',
'sienna': '#a0522d',
'silver': '#c0c0c0',
'skyblue': '#87ceeb',
'slateblue': '#6a5acd',
'slategray': '#708090',
'slategrey': '#708090',
'snow': '#fffafa',
'springgreen': '#00ff7f',
'steelblue': '#4682b4',
'tan': '#d2b48c',
'teal': '#008080',
'thistle': '#d8bfd8',
'tomato': '#ff6347',
'turquoise': '#40e0d0',
'violet': '#ee82ee',
'wheat': '#f5deb3',
'white': '#ffffff',
'whitesmoke': '#f5f5f5',
'yellow': '#ffff00',
'yellowgreen': '#9acd32',
'none': None,
}
COLOR_SVG = dict([(value, name) for name, value in SVG_COLOR.items()])
def is_color(color):
"""Determine if its a color we can use. If not, leave it unchanged."""
try:
return bool(Color(color))
except ColorError:
return False
class ColorError(KeyError):
"""Specific color parsing error"""
[docs]class Color(list):
"""An RGB array for the color"""
red = property(lambda self: self.to_rgb()[0])
red = red.setter(lambda self, value: self._set(0, value))
green = property(lambda self: self.to_rgb()[1])
green = green.setter(lambda self, value: self._set(1, value))
blue = property(lambda self: self.to_rgb()[2])
blue = blue.setter(lambda self, value: self._set(2, value))
alpha = property(lambda self: self.to_rgba()[3])
alpha = alpha.setter(lambda self, value: self._set(3, value, ('rgba',)))
hue = property(lambda self: self.to_hsl()[0])
hue = hue.setter(lambda self, value: self._set(0, value, ('hsl',)))
saturation = property(lambda self: self.to_hsl()[1])
saturation = saturation.setter(lambda self, value: self._set(1, value, ('hsl',)))
lightness = property(lambda self: self.to_hsl()[2])
lightness = lightness.setter(lambda self, value: self._set(2, value, ('hsl',)))
def __init__(self, color=None, space='rgb'):
super(Color, self).__init__()
if isinstance(color, str):
space, color = self.parse_str(color)
# Empty list means 'none', or no color
if color is None:
color = []
if not isinstance(color, (list, tuple)):
raise ColorError("Not a known a color value")
self.space = space
for val in color:
self.append(val)
def _set(self, index, value, spaces=('rgb', 'rgba')):
"""Set the color value in place, limits setter to specific color space"""
# Named colors are just rgb, so dump name memory
if self.space == 'named':
self.space = 'rgb'
if not self.space in spaces:
if index == 3 and self.space == 'rgb':
# Special, add alpha, don't convert back to rgb
self.space = 'rgba'
self.append(value)
return
else:
# Set in other colour space and convert back and forth
target = getattr(self, 'to_' + spaces[0])()
target[index] = value
self[:] = getattr(target, 'to_' + self.space)()
self[index] = value
[docs] def append(self, val):
"""Append a value to the local list"""
if len(self) == len(self.space):
raise ValueError("Can't add any more values to color.")
if isinstance(val, str):
val = val.strip()
if val.endswith('%'):
val = float(val.strip('%')) / 100
else:
val = float(val)
end_type = int
if len(self) == 3: # Alpha value
val = min([1.0, val])
end_type = float
elif isinstance(val, float) and val <= 1.0:
val *= 255
if isinstance(val, (int, float)):
super(Color, self).append(max(end_type(val), 0))
[docs] @staticmethod
def parse_str(color):
"""Creates a rgb int array"""
# Handle pre-defined svg color values
if color and color.lower() in SVG_COLOR:
return 'named', Color.parse_str(SVG_COLOR[color.lower()])[1]
if color is None:
return 'rgb', None
# Next handle short colors (css: #abc -> #aabbcc)
if color.startswith('#'):
# Remove any icc or ilab directives
# FUTURE: We could use icc or ilab information
col = color.split(' ')[0]
if len(col) == 4:
col = '#{1}{1}{2}{2}{3}{3}'.format(*col)
# Convert hex to integers
return 'rgb', (int(col[1:3], 16), int(col[3:5], 16), int(col[5:], 16))
# Handle other css color values
elif '(' in color and ')' in color:
space, values = color.lower().strip().strip(')').split('(')
return space, values.split(',')
raise ColorError("Unknown color format: {}".format(color))
def __str__(self):
"""int array to #rrggbb"""
if not self:
return 'none'
if self.space == 'named':
rgbhex = '#{0:02x}{1:02x}{2:02x}'.format(*self)
if rgbhex in COLOR_SVG:
return COLOR_SVG[rgbhex]
self.space = 'rgb'
if self.space == 'rgb':
return '#{0:02x}{1:02x}{2:02x}'.format(*self)
if self.space == 'rgba':
if self[3] == 1.0:
return 'rgb({:g}, {:g}, {:g})'.format(*self[:3])
return 'rgba({:g}, {:g}, {:g}, {:g})'.format(*self)
elif self.space == 'hsl':
return 'hsl({0:g}, {1:g}, {2:g})'.format(*self)
raise ColorError("Can't print colour space '{}'".format(self.space))
[docs] def to_hsl(self):
"""Turn this color into a Hue/Saturation/Lightness colour space"""
if not self and self.space in ('rgb', 'named'):
return self.to_rgb().to_hsl()
if self.space == 'hsl':
return self
elif self.space == 'rgb':
return Color(rgb_to_hsl(*self.to_floats()), space='hsl')
raise ColorError("Unknown color conversion {}->hsl".format(self.space))
[docs] def to_rgb(self):
"""Turn this color into a Red/Green/Blue colour space"""
if not self and self.space in ('rgb', 'named'):
return Color([0, 0, 0])
if self.space == 'rgb':
return self
if self.space in ('rgba', 'named'):
return Color(self[:3], space='rgb')
elif self.space == 'hsl':
return Color(hsl_to_rgb(*self.to_floats()), space='rgb')
raise ColorError("Unknown color conversion {}->rgb".format(self.space))
[docs] def to_rgba(self, alpha=1.0):
"""Turn this color isn't an RGB with Alpha colour space"""
if self.space == 'rgba':
return self
return Color(self.to_rgb() + [alpha], 'rgba')
[docs] def to_floats(self):
"""Returns the colour values as percentage floats (0.0 - 1.0)"""
return [val / 255.0 for val in self]
def rgb_to_hsl(red, green, blue):
"""RGB to HSL colour conversion"""
rgb_max = max(red, green, blue)
rgb_min = min(red, green, blue)
delta = rgb_max - rgb_min
hsl = [0.0, 0.0, (rgb_max + rgb_min) / 2.0]
if delta != 0:
if hsl[2] <= 0.5:
hsl[1] = delta / (rgb_max + rgb_min)
else:
hsl[1] = delta / (2 - rgb_max - rgb_min)
if red == rgb_max:
hsl[0] = (green - blue) / delta
elif green == rgb_max:
hsl[0] = 2.0 + (blue - red) / delta
elif blue == rgb_max:
hsl[0] = 4.0 + (red - green) / delta
hsl[0] /= 6.0
if hsl[0] < 0:
hsl[0] += 1
if hsl[0] > 1:
hsl[0] -= 1
return hsl
def hsl_to_rgb(hue, sat, light):
"""HSL to RGB Color Conversion"""
if sat == 0:
return [light, light, light] # Gray
if light < 0.5:
val2 = light * (1 + sat)
else:
val2 = light + sat - light * sat
val1 = 2 * light - val2
return [_hue_to_rgb(val1, val2, hue * 6 + 2.0),
_hue_to_rgb(val1, val2, hue * 6),
_hue_to_rgb(val1, val2, hue * 6 - 2.0)]
def _hue_to_rgb(val1, val2, hue):
if hue < 0:
hue += 6.0
if hue > 6:
hue -= 6.0
if hue < 1:
return val1 + (val2 - val1) * hue
if hue < 3:
return val2
if hue < 4:
return val1 + (val2 - val1) * (4 - hue)
return val1