__all__ = ["Color"] # karxi: Copied from my old Textsub script. Please forgive the mess, and keep # in mind that this may be phased out in the future. import collections import functools import sys # Python 3 checking if sys.version_info[0] == 2: basestr = str else: basestr = str # A named tuple for containing CIE L*a*b* (CIELAB) information. # NOTE TO THOSE MAINTAINING: If you don't know what that means, you're going to # hate yourself *and* me if you try to edit this. I know I did when I wrote it. LabTuple = collections.namedtuple("LabTuple", ["L", "a", "b"]) class Color: # The threshold at which to consider two colors noticeably different, even # if only barely jnd = 2.3 # TODO: Either subclass (this is probably best) or add a .native_type; in # the case of the former, just make sure each type is geared towards using # a certain kind of color space as a starting point, e.g. RGB, XYZ, HSV, # CIELAB, etc... # TODO: color_for_name() # TODO: Split __init__, partly using __new__, so the former just has to do # conversions def __init__(self, *args, **kwargs): self.ccode = "" self.closest_name = self.name = None nargs = len(args) if nargs == 1: # Make this a little easier to type out by reducing what we need to # work with arg = args[0] if isinstance(arg, int): # Assume we were passed a raw hexadecimal value # Again, handle this the easy way arg = "#%06X" % arg # Using 'if' instead of 'elif' here allows us to fall through from # the above, which is, of course, quite useful in this situation if isinstance(arg, basestr): # If it's a string, we've probably got a hex code, but check # anyway just in case if arg.startswith("#"): self.hexstr = self.sanitize_hex(arg) rgb = self.hexstr_to_rgb(self.hexstr) self.red, self.green, self.blue = rgb ##return # TODO: This. elif ( arg.startswith("\003") and len(arg) > 1 or len(arg) < 3 and arg.isdigit() ): # We have an IRC-style color code arg = arg.lstrip("\003") # Just in case arg = arg.split(",")[0] cnum = int(arg) try: color = _irc_colors[cnum] except LookupError: raise ValueError("No color for ccode %r found" % cnum) # We found a color; fall through and so on arg = color else: # Presumably we have a color name name = arg.lower() try: color = _svg_colors[name] except LookupError: raise ValueError("No color with name %r found" % name) # We found a color; fall through so we make this one a copy arg = color if isinstance(arg, Color): # We were passed a Color object - just duplicate it. # For now, we'll do things the cheap way.... self.red, self.green, self.blue = arg.to_rgb_tuple() self.hexstr = arg.hexstr self.closest_name = arg.closest_name self.name = arg.name self.ccode = arg.ccode elif nargs == 3: # Assume we've got RGB # Map to abs() so we can't get messed up results due to negatives args = list(map(abs, args)) self.red, self.green, self.blue = args # Convert for the hex code self.hexstr = self.rgb_to_hexstr(*args) ##return else: # We don't know how to handle the value we recieved.... raise ValueError # Otherwise, calculate XYZ for this self.x, self.y, self.z = self.rgb_to_xyz(*self.to_rgb_tuple()) # Calculate the LAB color self.cielab = LabTuple(*self.xyz_to_cielab(*self.to_xyz_tuple())) if not self.closest_name: self.closest_name = self.get_svg_name() if not self.ccode: self.ccode = self.get_ccode() def __eq__(self, other): return hash(self) == hash(other) def __ne__(self, other): return not self.__eq__(other) def __sub__(self, other): if not isinstance(other, Color): raise TypeError return self.distance(other) def __hash__(self): ##result = map(hash, [ ## str(self).upper(), ## self.red, self.green, self.blue ## ]) # 2012-12-08T13:34-07:00: This should improve accuracy result = list(map(hash, self.cielab)) result = functools.reduce(lambda x, y: x ^ y, result) return result # Before the change on 2012-12-08, the above was equivalent to the old # code, which was this: ##result = hash(str(self).upper()) ##result ^= self.red ##result ^= self.green ##result ^= self.blue ##return result def __repr__(self): ##return "%s(%r)" % (type(self).__name__, str(self)) return "{}({!r})".format(type(self).__name__, self.reduce_hexstr(self.hexstr)) def __str__(self): ##return self.reduce_hexstr(self.hexstr) return self.name() # Builtins # These were yanked from Hostmask and changed around a bit def __getitem__(self, ind): return (self.red, self.green, self.blue)[ind] def __iter__(self): targs = (self.red, self.green, self.blue) yield from targs # If we got here, we're out of attributes to provide raise StopIteration ##def __len__(self): ## # Acceptable so long as we're returning RGB like we currently (at TOW) ## # are ## return 3 @classmethod def from_ccode(cls, ccode): if isinstance(ccode, basestr): # We were passed a string ccode = ccode.lstrip("\003") ccode = ccode.split(",") if len(ccode) < 2: fg = ccode[0] bg = None else: fg, bg = ccode try: fg = int(fg) except ValueError: # We started with a string to the effect of ",00" fg = -1 try: bg = int(bg) except (ValueError, TypeError): # We started with a string to the effect of "00,", or it didn't # have a comma bg = -1 else: fg = ccode bg = -1 try: fg = _irc_colors[fg] except LookupError: # We had a string to the effect of ",00", or the color code # couldn't be found # TODO: Consider making a ValueError return a different value? fg = None else: fg = Color(fg) try: bg = _irc_colors[bg] except LookupError: # See above note. bg = None else: bg = Color(bg) ##if bg: return fg, bg return fg, bg def get_ccode(self): closest, cldist = None, None targs = _irc_colors for code, other in list(targs.items()): dist = self - other ##if (not strict and dist > self.jnd) or dist == 0: if dist == 0: # We have a perfect match! # Just return the relevant color code right now return "%02d" % code if cldist is None or cldist > dist: closest, cldist = "%02d" % code, dist # We've found the closest matching color code; return it return closest def get_svg_name(self, strict=False): closest, cldist = None, None targs = _svg_colors for name, other in list(targs.items()): dist = self - other if (not strict and dist > self.jnd) or dist == 0: # The difference is below the Just-Noticeable Difference # threshold, or we have a perfect match; consider them roughly # the same return name if cldist is None or cldist > dist: closest, cldist = name, dist # We've found the closest matching color name; return it return closest ##def name(self): return self.closest_name def distance(self, other): # CIELAB distance, adapted from distance() and checked vs. Wikipedia: # http://en.wikipedia.org/wiki/Color_difference slab, olab = self.to_cielab_tuple(), other.to_cielab_tuple() # Calculate the distance between the points for each dist = list(map(lambda p1, p2: (p2 - p1) ** 2, slab, olab)) # Add the results up, and sqrt to compensate for earlier squaring dist = sum(dist) ** 0.5 return dist def rgb_distance(self, other): # The older version of distance(). ##r1, r2 = self.red, other.red ##g1, g2 = self.green, other.green ##b1, b2 = self.blue, other.blue srgb, orgb = self.to_rgb_tuple(), other.to_rgb_tuple() ### Subtract the RGBs from each other (i.e. r1 - r2, g1 - g2, b1 - b2) ##dist = map(operator.sub, srgb, orgb) ### Square the results from the above ##dist = [x**2 for x in dist] # Do what we WOULD have done in those two lines with a single one dist = list(map(lambda x1, x2: (x1 - x2) ** 2, srgb, orgb)) # Add the results up dist = sum(dist) # Fetch the square root to compensate for the earlier squaring dist **= 0.5 return dist @classmethod def hexstr_to_rgb(cls, hexstr): hexstr = cls.sanitize_hex(hexstr) hexstr = hexstr.lstrip("#") if len(hexstr) == 3: # NOTE: This will presently never happen, due to the way # sanitize_hex works. # We have something like '#FEF', which means '#FFEEFF'. Expand it # first. # Multiplying each element by 17 expands it. Dividing it does the # opposite. result = tuple((int(h, 16) * 17) for h in hexstr) else: # This is ugly, but the purpose is simple and it's accomplished in # a single line...it just runs through the string, picking two # characters at a time and converting them from hex values to ints. result = tuple(int(hexstr[i : i + 2], 16) for i in range(0, len(hexstr), 2)) return result @staticmethod def rgb_to_hexstr(red, green, blue, compress=False): rgb = [red, green, blue] rgb = list(map(abs, rgb)) result = [] for c in rgb: c = "%02X" % c # Append to our result result.append(c) if compress: # Try to compress this down from six characters to three. # Basically the same thing as reduce_hexstr. Might make it use that # later. for h in result: if h[0] != h[1]: # We can't compress this; alas. # Break out so we don't go to the 'else' segment. break else: # All of our codes were doubles; compress them all down. result = [h[0] for h in result] # Join and return the result return "#" + "".join(result) # These next two are from http://www.easyrgb.com/index.php?X=MATH @staticmethod def rgb_to_xyz(red, green, blue): rgb = [red, green, blue] for i, n in enumerate(rgb): n /= 255 if n > 0.04045: n = ((n + 0.055) / 1.055) ** 2.4 else: n /= 12.92 rgb[i] = n * 100 r, g, b = rgb x = r * 0.4124 + g * 0.3576 + b * 0.1805 y = r * 0.2126 + g * 0.7152 + b * 0.0722 z = r * 0.0193 + g * 0.1192 + b * 0.9505 ##x = 0.436052025 * r + 0.385081593 * g + 0.143087414 * b ##y = 0.222491598 * r + 0.71688606 * g + 0.060621486 * b ##z = 0.013929122 * r + 0.097097002 * g + 0.71418547 * b return x, y, z @staticmethod def xyz_to_cielab(x, y, z): # Reference X, Y, and Z refs = [95.047, 100.000, 108.883] ref_x, ref_y, ref_z = refs ##xyz = [x / ref_x, y / ref_y, z / ref_z] xyz = [x, y, z] for i, n in enumerate(xyz): n /= refs[i] if n > 0.008856: n **= 1 / 3 else: n *= 7.787 n += 16 / 116 xyz[i] = n x, y, z = xyz l = (y * 116) - 16 a = (x - y) * 500 b = (y - z) * 200 return l, a, b @staticmethod def reduce_hexstr(hexstr): """Attempt to reduce a six-character hexadecimal color code down to a four-character one.""" orig = hexstr hexstr = hexstr.lstrip("#") strlen = len(hexstr) h = hexstr.upper() for i in range(0, strlen, 2): if h[i] != h[i + 1]: # We found a match that wouldn't work; give back the old value. return orig else: # All of these can be reduced; do so and return. return "#" + hexstr[::2] @staticmethod def sanitize_hex(hexstr): orig = hexstr hexstr = hexstr.upper() # We don't need the leading hash mark for now hexstr = hexstr.lstrip("#") strlen = len(hexstr) if strlen == 6: # We just need to test this for validity. Fall through to the end. pass elif strlen == 3: # We have a short (CSS style) code; duplicate all of the characters hexstr = [c + c for c in hexstr] hexstr = "".join(hexstr) else: raise ValueError("Invalid hexadecimal value provided: %s" % orig) try: # Make sure it works/is readable (no invalid characters). int(hexstr, 16) except ValueError: raise ValueError("Invalid hexadecimal value provided: %s" % orig) return "#" + hexstr def to_cielab_tuple(self): # For now, just return the stored CIELAB tuple return self.cielab def to_rgb_tuple(self): return (self.red, self.green, self.blue) # 2012-12-05T17:40:39-07:00: Changed 'self.blue' to 'self.z' like it SHOULD # have been in the FIRST place. Ugh. How did I fuck THAT one up? def to_xyz_tuple(self): return (self.x, self.y, self.z) # All of these are effectively equivalent to the Qt-provided colors, so they # could be phased out - but there's no need to, yet. _svg_colors = {} _irc_colors = {} _svg_colors.update( { "aliceblue": Color(240, 248, 255), "antiquewhite": Color(250, 235, 215), "aqua": Color(0, 255, 255), "aquamarine": Color(127, 255, 212), "azure": Color(240, 255, 255), "beige": Color(245, 245, 220), "bisque": Color(255, 228, 196), "black": Color(0, 0, 0), "blanchedalmond": Color(255, 235, 205), "blue": Color(0, 0, 255), "blueviolet": Color(138, 43, 226), "brown": Color(165, 42, 42), "burlywood": Color(222, 184, 135), "cadetblue": Color(95, 158, 160), "chartreuse": Color(127, 255, 0), "chocolate": Color(210, 105, 30), "coral": Color(255, 127, 80), "cornflowerblue": Color(100, 149, 237), "cornsilk": Color(255, 248, 220), "crimson": Color(220, 20, 60), "cyan": Color(0, 255, 255), "darkblue": Color(0, 0, 139), "darkcyan": Color(0, 139, 139), "darkgoldenrod": Color(184, 134, 11), "darkgray": Color(169, 169, 169), "darkgreen": Color(0, 100, 0), "darkgrey": Color(169, 169, 169), "darkkhaki": Color(189, 183, 107), "darkmagenta": Color(139, 0, 139), "darkolivegreen": Color(85, 107, 47), "darkorange": Color(255, 140, 0), "darkorchid": Color(153, 50, 204), "darkred": Color(139, 0, 0), "darksalmon": Color(233, 150, 122), "darkseagreen": Color(143, 188, 143), "darkslateblue": Color(72, 61, 139), "darkslategray": Color(47, 79, 79), "darkslategrey": Color(47, 79, 79), "darkturquoise": Color(0, 206, 209), "darkviolet": Color(148, 0, 211), "deeppink": Color(255, 20, 147), "deepskyblue": Color(0, 191, 255), "dimgray": Color(105, 105, 105), "dimgrey": Color(105, 105, 105), "dodgerblue": Color(30, 144, 255), "firebrick": Color(178, 34, 34), "floralwhite": Color(255, 250, 240), "forestgreen": Color(34, 139, 34), "fuchsia": Color(255, 0, 255), "gainsboro": Color(220, 220, 220), "ghostwhite": Color(248, 248, 255), "gold": Color(255, 215, 0), "goldenrod": Color(218, 165, 32), "gray": Color(128, 128, 128), "grey": Color(128, 128, 128), "green": Color(0, 128, 0), "greenyellow": Color(173, 255, 47), "honeydew": Color(240, 255, 240), "hotpink": Color(255, 105, 180), "indianred": Color(205, 92, 92), "indigo": Color(75, 0, 130), "ivory": Color(255, 255, 240), "khaki": Color(240, 230, 140), "lavender": Color(230, 230, 250), "lavenderblush": Color(255, 240, 245), "lawngreen": Color(124, 252, 0), "lemonchiffon": Color(255, 250, 205), "lightblue": Color(173, 216, 230), "lightcoral": Color(240, 128, 128), "lightcyan": Color(224, 255, 255), "lightgoldenrodyellow": Color(250, 250, 210), "lightgray": Color(211, 211, 211), "lightgreen": Color(144, 238, 144), "lightgrey": Color(211, 211, 211), "lightpink": Color(255, 182, 193), "lightsalmon": Color(255, 160, 122), "lightseagreen": Color(32, 178, 170), "lightskyblue": Color(135, 206, 250), "lightslategray": Color(119, 136, 153), "lightslategrey": Color(119, 136, 153), "lightsteelblue": Color(176, 196, 222), "lightyellow": Color(255, 255, 224), "lime": Color(0, 255, 0), "limegreen": Color(50, 205, 50), "linen": Color(250, 240, 230), "magenta": Color(255, 0, 255), "maroon": Color(128, 0, 0), "mediumaquamarine": Color(102, 205, 170), "mediumblue": Color(0, 0, 205), "mediumorchid": Color(186, 85, 211), "mediumpurple": Color(147, 112, 219), "mediumseagreen": Color(60, 179, 113), "mediumslateblue": Color(123, 104, 238), "mediumspringgreen": Color(0, 250, 154), "mediumturquoise": Color(72, 209, 204), "mediumvioletred": Color(199, 21, 133), "midnightblue": Color(25, 25, 112), "mintcream": Color(245, 255, 250), "mistyrose": Color(255, 228, 225), "moccasin": Color(255, 228, 181), "navajowhite": Color(255, 222, 173), "navy": Color(0, 0, 128), "oldlace": Color(253, 245, 230), "olive": Color(128, 128, 0), "olivedrab": Color(107, 142, 35), "orange": Color(255, 165, 0), "orangered": Color(255, 69, 0), "orchid": Color(218, 112, 214), "palegoldenrod": Color(238, 232, 170), "palegreen": Color(152, 251, 152), "paleturquoise": Color(175, 238, 238), "palevioletred": Color(219, 112, 147), "papayawhip": Color(255, 239, 213), "peachpuff": Color(255, 218, 185), "peru": Color(205, 133, 63), "pink": Color(255, 192, 203), "plum": Color(221, 160, 221), "powderblue": Color(176, 224, 230), "purple": Color(128, 0, 128), "red": Color(255, 0, 0), "rosybrown": Color(188, 143, 143), "royalblue": Color(65, 105, 225), "saddlebrown": Color(139, 69, 19), "salmon": Color(250, 128, 114), "sandybrown": Color(244, 164, 96), "seagreen": Color(46, 139, 87), "seashell": Color(255, 245, 238), "sienna": Color(160, 82, 45), "silver": Color(192, 192, 192), "skyblue": Color(135, 206, 235), "slateblue": Color(106, 90, 205), "slategray": Color(112, 128, 144), "slategrey": Color(112, 128, 144), "snow": Color(255, 250, 250), "springgreen": Color(0, 255, 127), "steelblue": Color(70, 130, 180), "tan": Color(210, 180, 140), "teal": Color(0, 128, 128), "thistle": Color(216, 191, 216), "tomato": Color(255, 99, 71), "turquoise": Color(64, 224, 208), "violet": Color(238, 130, 238), "wheat": Color(245, 222, 179), "white": Color(255, 255, 255), "whitesmoke": Color(245, 245, 245), "yellow": Color(255, 255, 0), "yellowgreen": Color(154, 205, 50), } ) for k, v in list(_svg_colors.items()): v.closest_name = v.name = k # 2012-12-08T14:29-07:00: Copied over from Colors.hexstr_for_ccodes in the main # textsub file, and subsequently modified. _irc_colors.update( { # These are all taken from *MY* XChat settings - they aren't guaranteed to # please everyone! 0: Color(0xFFFFFF), 1: Color(0x1F1F1F), 2: Color(0x00007F), 3: Color(0x007F00), 4: Color(0xFF0000), 5: Color(0x7F0000), 6: Color(0x9C009C), 7: Color(0xFC7F00), 8: Color(0xFFFF00), 9: Color(0x00FC00), ##10: Color(0x009393), 10: Color(0x008282), 11: Color(0x00FFFF), 12: Color(0x0000FC), 13: Color(0xFF00FF), 14: Color(0x7F7F7F), 15: Color(0xD2D2D2), # My local colors 16: Color(0xCCCCCC), ##17: Color(0x000000), # Commented out 'til readability checks are in 17: Color(0x1F1F1F), 18: Color(0x000056), 19: Color(0x008141), 20: Color(0xE00707), 21: Color(0xA10000), 22: Color(0x6A006A), 23: Color(0xA15000), 24: Color(0xA1A100), 25: Color(0x416600), ##26: Color(0x008282), 26: Color(0x005682), 27: Color(0x00D5F2), 28: Color(0x0715CD), 29: Color(0x99004D), 30: Color(0x323232), 31: Color(0x929292), 99: Color(0x999999), # Until I think of a better solution to this } ) for k, v in list(_irc_colors.items()): v.ccode = "%02d" % k del k, v # vim: set autoindent ts=4 sts=4 sw=4 textwidth=79 expandtab: