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