• Facebook
  • Twitter
  • Reddit
  • StumbleUpon
  • Digg
  • email

#!/usr/bin/env python
"""
name_that_color.py -- find names for hex colors
Copyright (c) 2010, Jeremiah Dodds <jeremiah.dodds@gmail.com>
 
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the conditions in LICENSE.txt are met
"""
from collections import namedtuple
import os, re
ColorInfo = namedtuple('ColorInfo',
                       ' '.join(['hex_value', 'name', 'red', 'green', 'blue',
                                 'hue', 'saturation', 'lightness']))
Match = namedtuple('Match', ' '.join(['hex_value', 'name', 'exact',
                                      'original']))
RGB = namedtuple('RGB', ' '.join(['red', 'green', 'blue']))
HSL = namedtuple('HSL', ' '.join(['hue', 'saturation', 'lightness']))
 
 
class NameThatColor(object):
    """Utility for finding the closest "human readable" name for a hex color
    """
    def __init__(self, colorfile=None):
        import csv
 
        self.color_info = []
 
        data = os.path.dirname(__file__) + '/data/colors.csv'
        reader = csv.reader(open(data))
 
        for hex_val, name in reader:
            red, green, blue = self.rgb(hex_val.strip())
            hue, saturation, lightness = self.hsl(hex_val.strip())
            self.color_info.append(ColorInfo(hex_val.strip(), name.strip(),
                                             red, green, blue,
                                             hue, saturation, lightness))
 
    def name(self, color):
        """Return the closest human readable name given a color
        """
        color = color.upper()
 
        if not 3 < len(color) < 8:
            return Match("#000000", "Invalid Color", False, color)
        elif len(color) % 3 == 0:
            color = "#" + color
        elif len(color) == 4:
            color = ''.join(['#',
                             color[1], color[1],
                             color[2], color[2],
                             color[3], color[3]])
 
        red, green, blue = self.rgb(color)
        hue, saturation, lightness = self.hsl(color)
 
        ndf1 = 0
        ndf2 = 0
        ndf = 0
        the_color = Match(None, None, None, None)
        df = -1
 
        for info in self.color_info:
            if color == info.hex_value:
                return Match(info.hex_value, info.name, True, color)
 
            ndf1 = (((red - info.red) ** 2) +
                    ((green - info.green) ** 2) +
                    ((blue - info.blue) ** 2))
            ndf2 = (((hue - info.hue) ** 2) +
                    ((saturation - info.saturation) ** 2) +
                    ((lightness - info.lightness) ** 2))
            ndf = ndf1 + ndf2 * 2
 
            if not 0 < df < ndf:
                df = ndf
                the_color = info
 
        if not the_color.name:
            return Match("#000000", "Invalid Color", False, color)
        else:
            return Match(the_color.hex_value,
                         the_color.name,
                         False,
                         color)
 
    def rgb(self, color):
        """Given a hex string representing a color, return an object with
        values representing red, green, and blue.
        """
        return RGB(int(color[1:3], 16),
                   int(color[3:5], 16),
                   int(color[5:7], 16))
 
    def hsl(self, color):
        """Given a hex string representing a color, return an object with
        attributes representing hue, lightness, and saturation.
        """
 
        red, green, blue = self.rgb(color)
 
        red /= 255.0
        green /= 255.0
        blue /= 255.0
 
        min_color = min(red, min(green, blue))
        max_color = max(red, max(green, blue))
        delta = max_color - min_color
        lightness = (min_color + max_color) / 2
 
        saturation = 0
        sat_mod = ((2 * lightness) if lightness < 0.5 else (2 - 2 * lightness))
 
        if 0 < lightness < 1:
            saturation = (delta / sat_mod)
 
        hue = 0
 
        if delta > 0:
            if max_color == red and max_color != green:
                hue += (green - blue) / delta
            if max_color == green and max_color != blue:
                hue += (2 + (blue - red) / delta)
            if max_color == blue and max_color != red:
                hue += (4 + (red - green) / delta)
            hue /= 6
 
        return HSL(int(hue * 255),
                   int(saturation * 255),
                   int(lightness * 255))
 
def main():
    import json
    import argparse
 
    output_choices = {
        'match_hex': lambda m: m.hex_value,
        'match_name': lambda m: m.name,
        'is_exact': lambda m: m.exact,
        'original_hex': lambda m: m.original
    }
 
    format_choices = {
        'json': lambda r: json.dumps(r),
        'raw' : lambda r: r
    }
 
    parser = argparse.ArgumentParser(
        description="Find the closest known color name for a hex value")
 
    parser.add_argument('-c', '--colors', dest='colors_file',
                        help="a csv file of known color name definitions")
 
    parser.add_argument('target',
                        help="hex value of the color to search for")
 
    parser.add_argument('-o', '--output',
                        dest="output",
                        nargs='*',
                        choices=output_choices.keys(),
                        default=['match_hex', 'match_name'],
                        help="what information about the color match to output")
 
    parser.add_argument('--format',
                        dest="format",
                        choices=format_choices.keys(),
                        default="json",
                        help="what format to return data in")
 
    args = parser.parse_args()
 
    Namer = NameThatColor(args.colors_file)
    match = Namer.name(args.target)
    result = {}
    for choice in args.output:
        result[choice] = output_choices[choice](match)
    print format_choices[args.format](result)
 
if __name__ == '__main__':
    main()