blob: ed7f6b1aee7908bda5088db6df5b46d54f035d56 [file] [log] [blame]
Barry Warsaw2406b1d1998-01-31 00:29:41 +00001"""Color Database.
2
3To create a class that contains color lookup methods, use the module global
4function `get_colordb(file)'. This function will try to examine the file to
5figure out what the format of the file is. If it can't figure out the file
6format, or it has trouble reading the file, None is returned. You can pass
7get_colordb() an optional filetype argument.
8
9Supporte file types are:
10
11 X_RGB_TXT -- X Consortium rgb.txt format files. Three columns of numbers
12 from 0 .. 255 separated by whitespace. Arbitrary trailing
13 columns used as the color name.
14"""
15
16import sys
Barry Warsaweb9b8af1998-02-11 18:55:37 +000017import string
Barry Warsaw2406b1d1998-01-31 00:29:41 +000018import re
Barry Warsawf4562a71998-02-11 17:19:23 +000019from types import *
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +000020import operator
Barry Warsaw2406b1d1998-01-31 00:29:41 +000021
Barry Warsaw9f4d73a1998-01-31 23:38:48 +000022class BadColor(Exception):
23 pass
24
Barry Warsawf4562a71998-02-11 17:19:23 +000025DEFAULT_DB = None
26
Barry Warsawa5a018f1998-09-25 22:51:36 +000027
Barry Warsaw2406b1d1998-01-31 00:29:41 +000028
29# generic class
30class ColorDB:
31 def __init__(self, fp, lineno):
32 # Maintain several dictionaries for indexing into the color database.
33 # Note that while Tk supports RGB intensities of 4, 8, 12, or 16 bits,
34 # for now we only support 8 bit intensities. At least on OpenWindows,
35 # all intensities in the /usr/openwin/lib/rgb.txt file are 8-bit
36 #
Barry Warsaweb9b8af1998-02-11 18:55:37 +000037 # key is (red, green, blue) tuple, value is (name, [aliases])
38 self.__byrgb = {}
Barry Warsaw2406b1d1998-01-31 00:29:41 +000039 #
Barry Warsaweb9b8af1998-02-11 18:55:37 +000040 # key is name, value is (red, green, blue)
Barry Warsaw2406b1d1998-01-31 00:29:41 +000041 self.__byname = {}
42 #
Barry Warsaw8be25941998-10-02 14:43:30 +000043 # all unique names (non-aliases). built-on demand
44 self.__allnames = None
Barry Warsaw2406b1d1998-01-31 00:29:41 +000045 while 1:
46 line = fp.readline()
47 if not line:
48 break
49 # get this compiled regular expression from derived class
50 mo = self._re.match(line)
51 if not mo:
52 sys.stderr.write('Error in %s, line %d\n' % (fp.name, lineno))
53 lineno = lineno + 1
54 continue
55 #
56 # extract the red, green, blue, and name
Barry Warsaweb9b8af1998-02-11 18:55:37 +000057 #
Barry Warsaw2406b1d1998-01-31 00:29:41 +000058 red, green, blue = map(int, mo.group('red', 'green', 'blue'))
59 name = mo.group('name')
Barry Warsaweb9b8af1998-02-11 18:55:37 +000060 keyname = string.lower(name)
Barry Warsaw2406b1d1998-01-31 00:29:41 +000061 #
62 # TBD: for now the `name' is just the first named color with the
63 # rgb values we find. Later, we might want to make the two word
64 # version the `name', or the CapitalizedVersion, etc.
Barry Warsaweb9b8af1998-02-11 18:55:37 +000065 #
66 key = (red, green, blue)
67 foundname, aliases = self.__byrgb.get(key, (name, []))
Barry Warsaw2406b1d1998-01-31 00:29:41 +000068 if foundname <> name and foundname not in aliases:
69 aliases.append(name)
Barry Warsaweb9b8af1998-02-11 18:55:37 +000070 self.__byrgb[key] = (foundname, aliases)
Barry Warsaw2406b1d1998-01-31 00:29:41 +000071 #
72 # add to byname lookup
Barry Warsaweb9b8af1998-02-11 18:55:37 +000073 #
74 self.__byname[keyname] = key
Barry Warsaw2406b1d1998-01-31 00:29:41 +000075 lineno = lineno + 1
76
Barry Warsaweb9b8af1998-02-11 18:55:37 +000077 def find_byrgb(self, rgbtuple):
Barry Warsaw9f4d73a1998-01-31 23:38:48 +000078 try:
Barry Warsaweb9b8af1998-02-11 18:55:37 +000079 return self.__byrgb[rgbtuple]
Barry Warsaw9f4d73a1998-01-31 23:38:48 +000080 except KeyError:
Barry Warsaweb9b8af1998-02-11 18:55:37 +000081 raise BadColor(rgbtuple)
Barry Warsaw2406b1d1998-01-31 00:29:41 +000082
83 def find_byname(self, name):
Barry Warsaweb9b8af1998-02-11 18:55:37 +000084 name = string.lower(name)
Barry Warsaw9f4d73a1998-01-31 23:38:48 +000085 try:
86 return self.__byname[name]
87 except KeyError:
88 raise BadColor(name)
Barry Warsaw2406b1d1998-01-31 00:29:41 +000089
Barry Warsaw7a134181998-09-29 20:03:15 +000090 def nearest(self, red, green, blue):
Barry Warsaw2406b1d1998-01-31 00:29:41 +000091 # TBD: use Voronoi diagrams, Delaunay triangulation, or octree for
Barry Warsaweb9b8af1998-02-11 18:55:37 +000092 # speeding up the locating of nearest point. Exhaustive search is
93 # inefficient, but may be fast enough.
Barry Warsaw2406b1d1998-01-31 00:29:41 +000094 nearest = -1
95 nearest_name = ''
Barry Warsaweb9b8af1998-02-11 18:55:37 +000096 for name, aliases in self.__byrgb.values():
97 r, g, b = self.__byname[string.lower(name)]
Barry Warsaw2406b1d1998-01-31 00:29:41 +000098 rdelta = red - r
99 gdelta = green - g
100 bdelta = blue - b
101 distance = rdelta * rdelta + gdelta * gdelta + bdelta * bdelta
102 if nearest == -1 or distance < nearest:
103 nearest = distance
104 nearest_name = name
105 return nearest_name
Barry Warsaw8be25941998-10-02 14:43:30 +0000106
Barry Warsaw9f3ea211998-10-02 15:59:20 +0000107 def unique_names(self):
Barry Warsaw8be25941998-10-02 14:43:30 +0000108 # sorted
109 if not self.__allnames:
110 self.__allnames = []
111 for name, aliases in self.__byrgb.values():
112 self.__allnames.append(name)
Barry Warsaw9f3ea211998-10-02 15:59:20 +0000113 # sort irregardless of case
114 def nocase_cmp(n1, n2):
115 return cmp(string.lower(n1), string.lower(n2))
116 self.__allnames.sort(nocase_cmp)
Barry Warsaw8be25941998-10-02 14:43:30 +0000117 return self.__allnames
Barry Warsaw9f3ea211998-10-02 15:59:20 +0000118
119 def aliases_of(self, red, green, blue):
120 try:
121 name, aliases = self.__byrgb[(red, green, blue)]
122 except KeyError:
123 raise BadColor((red, green, blue))
124 return [name] + aliases
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000125
126
127class RGBColorDB(ColorDB):
128 _re = re.compile(
129 '\s*(?P<red>\d+)\s+(?P<green>\d+)\s+(?P<blue>\d+)\s+(?P<name>.*)')
130
131
132
133# format is a tuple (RE, SCANLINES, CLASS) where RE is a compiled regular
134# expression, SCANLINES is the number of header lines to scan, and CLASS is
135# the class to instantiate if a match is found
136
137X_RGB_TXT = re.compile('XConsortium'), 1, RGBColorDB
138
139def get_colordb(file, filetype=X_RGB_TXT):
140 colordb = None
141 fp = None
142 typere, scanlines, class_ = filetype
143 try:
144 try:
145 lineno = 0
146 fp = open(file)
147 while lineno < scanlines:
148 line = fp.readline()
149 if not line:
150 break
151 mo = typere.search(line)
152 if mo:
153 colordb = class_(fp, lineno)
154 break
155 lineno = lineno + 1
156 except IOError:
157 pass
158 finally:
159 if fp:
160 fp.close()
Barry Warsawf4562a71998-02-11 17:19:23 +0000161 # save a global copy
162 global DEFAULT_DB
163 DEFAULT_DB = colordb
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000164 return colordb
165
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000166
167
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000168_namedict = {}
Barry Warsaw2e7a3201998-02-18 17:01:12 +0000169def rrggbb_to_triplet(color, atoi=string.atoi):
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000170 """Converts a #rrggbb color to the tuple (red, green, blue)."""
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000171 rgbtuple = _namedict.get(color)
172 if rgbtuple is None:
173 assert color[0] == '#'
174 red = color[1:3]
175 green = color[3:5]
176 blue = color[5:7]
Barry Warsaw2e7a3201998-02-18 17:01:12 +0000177 rgbtuple = (atoi(red, 16), atoi(green, 16), atoi(blue, 16))
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000178 _namedict[color] = rgbtuple
179 return rgbtuple
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000180
Barry Warsawf4562a71998-02-11 17:19:23 +0000181
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000182_tripdict = {}
Barry Warsawf4562a71998-02-11 17:19:23 +0000183def triplet_to_rrggbb(rgbtuple):
184 """Converts a (red, green, blue) tuple to #rrggbb."""
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000185 hexname = _tripdict.get(rgbtuple)
186 if hexname is None:
Barry Warsaw2e7a3201998-02-18 17:01:12 +0000187 hexname = '#%02x%02x%02x' % rgbtuple
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000188 _tripdict[rgbtuple] = hexname
189 return hexname
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000190
191
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000192_maxtuple = (256.0,) * 3
Barry Warsawa5a018f1998-09-25 22:51:36 +0000193def triplet_to_fractional_rgb(rgbtuple):
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000194 return map(operator.__div__, rgbtuple, _maxtuple)
Barry Warsaw2662e151998-02-13 21:27:56 +0000195
196
Barry Warsaw0e3e6991998-09-28 23:39:18 +0000197def triplet_to_brightness(rgbtuple):
198 # return the brightness (grey level) along the scale 0.0==black to
199 # 1.0==white
200 r = 0.299
201 g = 0.587
202 b = 0.114
203 return r*rgbtuple[0] + g*rgbtuple[1] + b*rgbtuple[2]
204
205
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000206
207if __name__ == '__main__':
208 import string
209
210 colordb = get_colordb('/usr/openwin/lib/rgb.txt')
211 if not colordb:
212 print 'No parseable color database found'
213 sys.exit(1)
214 # on my system, this color matches exactly
215 target = 'navy'
Barry Warsawa5a018f1998-09-25 22:51:36 +0000216 red, green, blue = rgbtuple = colordb.find_byname(target)
217 print target, ':', red, green, blue, triplet_to_rrggbb(rgbtuple)
218 name, aliases = colordb.find_byrgb(rgbtuple)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000219 print 'name:', name, 'aliases:', string.join(aliases, ", ")
220 target = (1, 1, 128) # nearest to navy
221 target = (145, 238, 144) # nearest to lightgreen
222 target = (255, 251, 250) # snow
223 print 'finding nearest to', target, '...'
224 import time
225 t0 = time.time()
Barry Warsawa5a018f1998-09-25 22:51:36 +0000226 nearest = colordb.nearest(target)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000227 t1 = time.time()
228 print 'found nearest color', nearest, 'in', t1-t0, 'seconds'
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000229