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