from math import sqrt from colorsys import rgb_to_yiq, yiq_to_rgb, rgb_to_hls, hls_to_rgb, rgb_to_hsv, hsv_to_rgb class Color(object): """ Color class, by leonardo maffi, V.1.1, Sep 12 2006. Notes about the rgb, yiq, hls, hsv properties: The colorsys module defines bidirectional conversions of color values between colors expressed in the RGB (Red Green Blue) color space used in computer monitors and three other coordinate systems: YIQ, HLS (Hue Lightness Saturation) and HSV (Hue Saturation Value). Coordinates in all of these color spaces are floating point values. In the YIQ space, the Y coordinate is between 0 and 1, but the I and Q coordinates can be positive or negative. In all other spaces, the coordinates are all between 0 and 1. Doctests: >>> red Color(1.000, 0.000, 0.000) >>> Color() Color(0.000, 0.000, 0.000) >>> Color(blue=0.5) Color(0.000, 0.000, 0.500) >>> c = Color(1, 1, 1) >>> c Color(1.000, 1.000, 1.000) >>> print c <255 255 255> >>> c.r = -5 >>> c Color(0.000, 1.000, 1.000) >>> c.r 0.0 >>> c.tk '#00ffff' >>> c.tk = "#00CC00" >>> c.tk '#00cc00' >>> c Color(0.000, 0.800, 0.000) >>> print c <0 204 0> >>> c = Color(0.5, 0.1, 0.8) >>> c.rgb (0.5, 0.10000000000000001, 0.80000000000000004) >>> c * 2 Color(1.000, 0.200, 1.000) >>> c / 2 Color(0.250, 0.050, 0.400) >>> c / 10.0 Color(0.050, 0.010, 0.080) >>> c.tk '#801acc' >>> not(c) False >>> -c Color(0.500, 0.900, 0.200) >>> c.contrasted Color(0.000, 0.000, 0.000) >>> c = Color(0.1, 0.2, 0.3) >>> c.contrasted Color(0.000, 0.000, 0.000) >>> Color(0.9, 0.8, 0.7).contrasted Color(1.000, 1.000, 1.000) >>> c = black >>> bool(c) False >>> bool(-c) True >>> c = Color(0.001, 0.001, 0.001) >>> print c <0 0 0> >>> bool(c) True >>> g = Color(1, 0.5, 1.1) >>> g Color(1.000, 0.500, 1.000) >>> g.rgb (1.0, 0.5, 1.0) >>> g.rgb255 (255, 128, 255) >>> Color("255 128 255") Color(1.000, 0.502, 1.000) >>> Color("255 127.0 255") Color(1.000, 0.498, 1.000) >>> g = Color("255 127.0 255") >>> g.rgb (1.0, 0.49803921568627452, 1.0) >>> g.rgb255 (255, 127, 255) >>> g.tk '#ff7fff' >>> Color('#ff7fff') Color(1.000, 0.498, 1.000) >>> c1 = Color(1.0, 1, 1) >>> c2 = Color("0 0 0") >>> c1.interpolate(c2) Color(0.500, 0.500, 0.500) >>> c1.interpolate(c2, 0.1) Color(0.900, 0.900, 0.900) >>> c1 = Color(1.0, 1, 1) >>> c2 = Color("0 70 110.5") >>> c2 Color(0.000, 0.275, 0.433) >>> c1.perceptualDist(c2) 5.4081218762014611 >>> c2.perceptualDist(c1) 4.7292329873125718 >>> c3 = Color("#000000") >>> c1.perceptualDist(c3) 9.0 >>> c3.perceptualDist(c1) 9.0 >>> c = Color(0.1, 0.6, 0.9) >>> c.rgb (0.10000000000000001, 0.59999999999999998, 0.90000000000000002) >>> c.yiq (0.48299999999999998, -0.39600000000000002, -0.011999999999999955) >>> c.yiq = (0.48299999999999998, -0.39600000000000007, -0.012000000000000011) >>> c Color(0.100, 0.600, 0.900) >>> c.hsv (0.56250002156250567, 0.88888877185183635, 0.89999988000000009) >>> c.hsv = (0.56250002156250567, 0.88888877185183635, 0.89999988000000009) >>> c Color(0.100, 0.600, 0.900) >>> c.hls (0.56250002156250567, 0.49999998600000001, 0.79999981039999479) >>> c.hls = (0.5625, 0.5, 0.80000000000000004) >>> c Color(0.100, 0.600, 0.900) >>> c1 = Color(0.5, 0.5, 0.50) >>> c1.dist(white) 0.8660254037844386 >>> white.dist(c1) 0.8660254037844386 >>> white.dist(black) 1.7320508075688772 >>> c1 = Color() >>> c1.rgb = (0.5, 0.5, 0.5) >>> c1 Color(0.500, 0.500, 0.500) >>> c1.rgb = (-10, 100, 200.5) >>> c1 Color(0.000, 1.000, 1.000) """ _halfBrightness = (0.3 + 0.59 + 0.11) / 2.0 # For self.get_contrasted _conv = dict( (ch,nu) for nu,ch in enumerate("0123456789abcdef") ) # For self.set_tk def __init__(self, first=0.0, green=0.0, blue=0.0): if isinstance(first, (int, long, float)): self._r = self._constrain(first) self._g = self._constrain(green) self._b = self._constrain(blue) elif isinstance(first, (tuple, list)) and len(first) == 3: self._r = self._constrain(first[0]) self._g = self._constrain(first[1]) self._b = self._constrain(first[2]) elif isinstance(first, basestring): color = str(first).strip() if color[0] == "#": self.set_tk(color) else: # accepts colors in decimal form, in [0, 255], like "100.0 20 15" r, g, b = first.split(" ") self._r = self._constrain(float(r)/255.0) self._g = self._constrain(float(g)/255.0) self._b = self._constrain(float(b)/255.0) else: raise "Color parameter error: " + str((first, green, blue)) def get_r(self): return self._r def set_r(self, red): self._r = self._constrain(red) r = property(fget=get_r, fset=set_r) def get_g(self): return self._g def set_g(self, green): self._g = self._constrain(green) g = property(fget=get_g, fset=set_g) def get_b(self): return self._b def set_b(self, blue): self._b = self._constrain(blue) b = property(fget=get_b, fset=set_b) def get_rgb(self): return (self._r, self._g, self._b) def set_rgb(self, (r,g,b)): self._r = self._constrain(r) self._g = self._constrain(g) self._b = self._constrain(b) rgb = property(fget=get_rgb, fset=set_rgb) def get_rgb255(self): return int(round(self._r*255)), int(round(self._g*255)), int(round(self._b*255)) def set_rgb255(self, (r,g,b)): self._r = self._constrain(r/255.0) self._g = self._constrain(g/255.0) self._b = self._constrain(b/255.0) rgb255 = property(fget=get_rgb255, fset=set_rgb255) def get_tk(self): # For tk property return '#%02x%02x%02x' % (round(self._r*255), round(self._g*255), round(self._b*255)) def set_tk(self, tkcol): # For tk property col_lower = tkcol.strip().lower() assert col_lower[0] == "#" and len(col_lower) == 7 sharp, r1,r0, g1,g0, b1,b0 = col_lower self._r = (self._conv[r1]*16 + self._conv[r0]) / 255.0 self._g = (self._conv[g1]*16 + self._conv[g0]) / 255.0 self._b = (self._conv[b1]*16 + self._conv[b0]) / 255.0 tk = property(fget=get_tk, fset=set_tk) def get_yiq(self): "Convert the color from RGB coordinates to YIQ coordinates." return rgb_to_yiq(self._r, self._g, self._b) def set_yiq(self, (y, i, q)): "Convert the color from YIQ coordinates to RGB coordinates." self._r, self._g, self._b = yiq_to_rgb(y, i, q) yiq = property(fget=get_yiq, fset=set_yiq) def get_hls(self): "Convert the color from RGB coordinates to HLS coordinates." return rgb_to_hls(self._r, self._g, self._b) def set_hls(self, (h, l, s)): "Convert the color from HLS coordinates to RGB coordinates." self._r, self._g, self._b = hls_to_rgb(h, l, s) hls = property(fget=get_hls, fset=set_hls) def get_hsv(self): "Convert the color from RGB coordinates to HSV coordinates." return rgb_to_hsv(self._r, self._g, self._b) def set_hsv(self, (h, s, v)): "Convert the color from HSV coordinates to RGB coordinates." self._r, self._g, self._b = hsv_to_rgb(h, s, v) hsv = property(fget=get_hsv, fset=set_hsv) def _constrain(self, component): return max(0.0, min(1.0, float(component)) ) def __repr__(self): return 'Color(%.3f, %.3f, %.3f)' % (self._r, self._g, self._b) def __str__(self): return "<%d %d %d>" % (round(self._r*255), round(self._g*255), round(self._b*255)) def __add__(self, other): # Color + Color return self.__class__(self._r + other._r, self._g + other._g, self._b + other._b) def __sub__(self, other): # Color + Color return self.__class__(self.r - other.r, self.g - other.g, self.b - other.b) def __mul__(self, n): return self.__class__(self._r * n, self._g * n, self._b * n) def __div__(self, n): return self.__class__(self._r / n, self._g / n, self._b / n) def __neg__(self): return self.__class__(1.0-self._r, 1.0-self._g, 1.0-self._b) def __nonzero__(self): # for bool(acolor) return self._r + self._g + self._b > 1e-4 def interpolate(self, other, t=0.5): """interpolate(self, other, t=0.5): computes the linear interpolating color between self and the other color.""" return self.__class__(self._r + (other._r - self._r)*t, self._g + (other._g - self._g)*t, self._b + (other._b - self._b)*t ) def dist(self, other): return sqrt((self._r - other._r)**2 + (self._g - other._g)**2 + (self._b - other._b)**2) def perceptualDist(self, other): """perceptualDist(self, other): return the approximate distance with the other color, computed perceptually. Not even commutative.""" # Adapted from a C version: http://www.compuphase.com/cmetric.htm rmean = (self._r - other._r) / 2 dr = self._r - other._r dg = self._g - other._g db = self._b - other._b return ((2+rmean)*dr*dr) + 4*dg*dg + ((3-rmean)*db*db) def get_contrasted(self): # For contrasted property """contrasted(red, green, blue): given an rgb color, return white or black to keep a high contrast""" # From code by Andreas Filsinger, andreas@filsinger.de brightness = 0.3*self._r + 0.59*self._g + 0.11*self._b if brightness < self._halfBrightness: return white else: return black contrasted = property(fget=get_contrasted) red = Color(1, 0, 0) green = Color(0, 1, 0) blue = Color(0, 0, 1) black = Color(0, 0, 0) white = Color(1, 1, 1) dark_red = Color(0.5, 0.0, 0.0) dark_green = Color(0.0, 0.4, 0.0) dark_blue = Color(0.0, 0.0, 0.5) dark_grey = Color(0.3, 0.3, 0.3) grey = Color(0.5, 0.5, 0.5) light_grey = Color(0.7, 0.7, 0.7) yellow = Color(0.9, 0.8, 0.0) brown = Color(0.5, 0.35, 0.0) pink = Color(1.0, 0.0, 0.8) purple = Color(0.6, 0.0, 0.7) def _test(): import doctest doctest.testmod() if __name__ == "__main__": _test()