1 """Color Database. 2 3 This file contains one class, called ColorDB, and several utility functions. 4 The class must be instantiated by the get_colordb() function in this file, 5 passing it a filename to read a database out of. 6 7 The get_colordb() function will try to examine the file to figure out what the 8 format of the file is. If it can't figure out the file format, or it has 9 trouble reading the file, None is returned. You can pass get_colordb() an 10 optional filetype argument. 11 12 Supporte file types are: 13 14 X_RGB_TXT -- X Consortium rgb.txt format files. Three columns of numbers 15 from 0 .. 255 separated by whitespace. Arbitrary trailing 16 columns used as the color name. 17 18 The utility functions are useful for converting between the various expected 19 color formats, and for calculating other color values. 20 21 """ 22 23 import sys 24 import re 25 from types import * 26 import operator 27 28 class BadColor(Exception): 29 pass 30 31 DEFAULT_DB = None 32 SPACE = ' ' 33 COMMASPACE = ', ' 34 35 36 38 # generic class 39 class ColorDB: 40 def __init__(self, fp): 41 lineno = 2 42 self.__name = fp.name 43 # Maintain several dictionaries for indexing into the color database. 44 # Note that while Tk supports RGB intensities of 4, 8, 12, or 16 bits, 45 # for now we only support 8 bit intensities. At least on OpenWindows, 46 # all intensities in the /usr/openwin/lib/rgb.txt file are 8-bit 47 # 48 # key is (red, green, blue) tuple, value is (name, [aliases]) 49 self.__byrgb = {} 50 # key is name, value is (red, green, blue) 51 self.__byname = {} 52 # all unique names (non-aliases). built-on demand 53 self.__allnames = None 54 for line in fp: 55 # get this compiled regular expression from derived class 56 mo = self._re.match(line) 57 if not mo: 58 print >> sys.stderr, 'Error in', fp.name, ' line', lineno 59 lineno += 1 60 continue 61 # extract the red, green, blue, and name 62 red, green, blue = self._extractrgb(mo) 63 name = self._extractname(mo) 64 keyname = name.lower() 65 # BAW: for now the `name' is just the first named color with the 66 # rgb values we find. Later, we might want to make the two word 67 # version the `name', or the CapitalizedVersion, etc. 68 key = (red, green, blue) 69 foundname, aliases = self.__byrgb.get(key, (name, [])) 70 if foundname <> name and foundname not in aliases: 71 aliases.append(name) 72 self.__byrgb[key] = (foundname, aliases) 73 # add to byname lookup 74 self.__byname[keyname] = key 75 lineno = lineno + 1 76 77 # override in derived classes 78 def _extractrgb(self, mo): 79 return [int(x) for x in mo.group('red', 'green', 'blue')] 80 81 def _extractname(self, mo): 82 return mo.group('name') 83 84 def filename(self): 85 return self.__name 86 87 def find_byrgb(self, rgbtuple): 88 """Return name for rgbtuple""" 89 try: 90 return self.__byrgb[rgbtuple] 91 except KeyError: 92 raise BadColor(rgbtuple) 93 94 def find_byname(self, name): 95 """Return (red, green, blue) for name""" 96 name = name.lower() 97 try: 98 return self.__byname[name] 99 except KeyError: 100 raise BadColor(name) 101 102 def nearest(self, red, green, blue): 103 """Return the name of color nearest (red, green, blue)""" 104 # BAW: should we use Voronoi diagrams, Delaunay triangulation, or 105 # octree for speeding up the locating of nearest point? Exhaustive 106 # search is inefficient, but seems fast enough. 107 nearest = -1 108 nearest_name = '' 109 for name, aliases in self.__byrgb.values(): 110 r, g, b = self.__byname[name.lower()] 111 rdelta = red - r 112 gdelta = green - g 113 bdelta = blue - b 114 distance = rdelta * rdelta + gdelta * gdelta + bdelta * bdelta 115 if nearest == -1 or distance < nearest: 116 nearest = distance 117 nearest_name = name 118 return nearest_name 119 120 def unique_names(self): 121 # sorted 122 if not self.__allnames: 123 self.__allnames = [] 124 for name, aliases in self.__byrgb.values(): 125 self.__allnames.append(name) 126 # sort irregardless of case 127 def nocase_cmp(n1, n2): 128 return cmp(n1.lower(), n2.lower()) 129 self.__allnames.sort(nocase_cmp) 130 return self.__allnames 131 132 def aliases_of(self, red, green, blue): 133 try: 134 name, aliases = self.__byrgb[(red, green, blue)] 135 except KeyError: 136 raise BadColor((red, green, blue)) 137 return [name] + aliases 138 139 141 class RGBColorDB(ColorDB): 142 _re = re.compile( 143 '\s*(?P<red>\d+)\s+(?P<green>\d+)\s+(?P<blue>\d+)\s+(?P<name>.*)') 144 145 146 class HTML40DB(ColorDB): 147 _re = re.compile('(?P<name>\S+)\s+(?P<hexrgb>#[0-9a-fA-F]{6})') 148 149 def _extractrgb(self, mo): 150 return rrggbb_to_triplet(mo.group('hexrgb')) 151 152 class LightlinkDB(HTML40DB): 153 _re = re.compile('(?P<name>(.+))\s+(?P<hexrgb>#[0-9a-fA-F]{6})') 154 155 def _extractname(self, mo): 156 return mo.group('name').strip() 157 158 class WebsafeDB(ColorDB): 159 _re = re.compile('(?P<hexrgb>#[0-9a-fA-F]{6})') 160 161 def _extractrgb(self, mo): 162 return rrggbb_to_triplet(mo.group('hexrgb')) 163 164 def _extractname(self, mo): 165 return mo.group('hexrgb').upper() 166 167 168 170 # format is a tuple (RE, SCANLINES, CLASS) where RE is a compiled regular 171 # expression, SCANLINES is the number of header lines to scan, and CLASS is 172 # the class to instantiate if a match is found 173 174 FILETYPES = [ 175 (re.compile('Xorg'), RGBColorDB), 176 (re.compile('XConsortium'), RGBColorDB), 177 (re.compile('HTML'), HTML40DB), 178 (re.compile('lightlink'), LightlinkDB), 179 (re.compile('Websafe'), WebsafeDB), 180 ] 181 182 def get_colordb(file, filetype=None): 183 colordb = None 184 fp = open(file) 185 try: 186 line = fp.readline() 187 if not line: 188 return None 189 # try to determine the type of RGB file it is 190 if filetype is None: 191 filetypes = FILETYPES 192 else: 193 filetypes = [filetype] 194 for typere, class_ in filetypes: 195 mo = typere.search(line) 196 if mo: 197 break 198 else: 199 # no matching type 200 return None 201 # we know the type and the class to grok the type, so suck it in 202 colordb = class_(fp) 203 finally: 204 fp.close() 205 # save a global copy 206 global DEFAULT_DB 207 DEFAULT_DB = colordb 208 return colordb 209 210 211 213 _namedict = {} 214 215 def rrggbb_to_triplet(color): 216 """Converts a #rrggbb color to the tuple (red, green, blue).""" 217 rgbtuple = _namedict.get(color) 218 if rgbtuple is None: 219 if color[0] <> '#': 220 raise BadColor(color) 221 red = color[1:3] 222 green = color[3:5] 223 blue = color[5:7] 224 rgbtuple = int(red, 16), int(green, 16), int(blue, 16) 225 _namedict[color] = rgbtuple 226 return rgbtuple 227 228 229 _tripdict = {} 230 def triplet_to_rrggbb(rgbtuple): 231 """Converts a (red, green, blue) tuple to #rrggbb.""" 232 global _tripdict 233 hexname = _tripdict.get(rgbtuple) 234 if hexname is None: 235 hexname = '#%02x%02x%02x' % rgbtuple 236 _tripdict[rgbtuple] = hexname 237 return hexname 238 239 240 _maxtuple = (256.0,) * 3 241 def triplet_to_fractional_rgb(rgbtuple): 242 return map(operator.__div__, rgbtuple, _maxtuple) 243 244 245 def triplet_to_brightness(rgbtuple): 246 # return the brightness (grey level) along the scale 0.0==black to 247 # 1.0==white 248 r = 0.299 249 g = 0.587 250 b = 0.114 251 return r*rgbtuple[0] + g*rgbtuple[1] + b*rgbtuple[2] 252 253 254 256 if __name__ == '__main__': 257 colordb = get_colordb('/usr/openwin/lib/rgb.txt') 258 if not colordb: 259 print 'No parseable color database found' 260 sys.exit(1) 261 # on my system, this color matches exactly 262 target = 'navy' 263 red, green, blue = rgbtuple = colordb.find_byname(target) 264 print target, ':', red, green, blue, triplet_to_rrggbb(rgbtuple) 265 name, aliases = colordb.find_byrgb(rgbtuple) 266 print 'name:', name, 'aliases:', COMMASPACE.join(aliases) 267 r, g, b = (1, 1, 128) # nearest to navy 268 r, g, b = (145, 238, 144) # nearest to lightgreen 269 r, g, b = (255, 251, 250) # snow 270 print 'finding nearest to', target, '...' 271 import time 272 t0 = time.time() 273 nearest = colordb.nearest(r, g, b) 274 t1 = time.time() 275 print 'found nearest color', nearest, 'in', t1-t0, 'seconds' 276 # dump the database 277 for n in colordb.unique_names(): 278 r, g, b = colordb.find_byname(n) 279 aliases = colordb.aliases_of(r, g, b) 280 print '%20s: (%3d/%3d/%3d) == %s' % (n, r, g, b, 281 SPACE.join(aliases[1:])) 282