Home | History | Annotate | Download | only in pynche
      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