#Copyright ReportLab Europe Ltd. 2000-2004
#see license.txt for license details
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/lib/colors.py
__version__=''' $Id: colors.py 3411 2009-01-29 16:41:52Z rgbecker $ '''
__doc__='''Defines standard colour-handling classes and colour names.

We define standard classes to hold colours in two models:  RGB and CMYK.
These can be constructed from several popular formats.  We also include

- pre-built colour objects for the HTML standard colours

- pre-built colours used in ReportLab's branding

- various conversion and construction functions
'''
import string, math
from types import StringType, ListType, TupleType
from reportlab.lib.utils import fp_str
_SeqTypes = (ListType,TupleType)

class Color:
    """This class is used to represent color.  Components red, green, blue
    are in the range 0 (dark) to 1 (full intensity)."""

    def __init__(self, red=0, green=0, blue=0):
        "Initialize with red, green, blue in range [0-1]."
        self.red, self.green, self.blue = red,green,blue

    def __repr__(self):
        return "Color(%s)" % string.replace(fp_str(self.red, self.green, self.blue),' ',',')

    def __hash__(self):
        return hash( (self.red, self.green, self.blue) )

    def __cmp__(self,other):
        try:
            dsum = 4*self.red-4*other.red + 2*self.green-2*other.green + self.blue-other.blue
        except:
            return -1
        if dsum > 0: return 1
        if dsum < 0: return -1
        return 0

    def rgb(self):
        "Returns a three-tuple of components"
        return (self.red, self.green, self.blue)

    def bitmap_rgb(self):
        return tuple(map(lambda x: int(x*255)&255, self.rgb()))

    def hexval(self):
        return '0x%02x%02x%02x' % self.bitmap_rgb()

class CMYKColor(Color):
    """This represents colors using the CMYK (cyan, magenta, yellow, black)
    model commonly used in professional printing.  This is implemented
    as a derived class so that renderers which only know about RGB "see it"
    as an RGB color through its 'red','green' and 'blue' attributes, according
    to an approximate function.

    The RGB approximation is worked out when the object in constructed, so
    the color attributes should not be changed afterwards.

    Extra attributes may be attached to the class to support specific ink models,
    and renderers may look for these."""

    def __init__(self, cyan=0, magenta=0, yellow=0, black=0,
                spotName=None, density=1, knockout=None):
        """
        Initialize with four colors in range [0-1]. the optional
        spotName, density & knockout may be of use to specific renderers.
        spotName is intended for use as an identifier to the renderer not client programs.
        density is used to modify the overall amount of ink.
        knockout is a renderer dependent option that determines whether the applied colour
        knocksout (removes) existing colour; None means use the global default.
        """
        self.cyan = cyan
        self.magenta = magenta
        self.yellow = yellow
        self.black = black
        self.spotName = spotName
        self.density = max(min(density,1),0)    # force into right range
        self.knockout = knockout

        # now work out the RGB approximation. override
        self.red, self.green, self.blue = cmyk2rgb( (cyan, magenta, yellow, black) )

        if density<1:
            #density adjustment of rgb approximants, effectively mix with white
            r, g, b = self.red, self.green, self.blue
            r = density*(r-1)+1
            g = density*(g-1)+1
            b = density*(b-1)+1
            self.red, self.green, self.blue = (r,g,b)

    def __repr__(self):
        return "CMYKColor(%s%s%s%s)" % (
            string.replace(fp_str(self.cyan, self.magenta, self.yellow, self.black),' ',','),
            (self.spotName and (',spotName='+repr(self.spotName)) or ''),
            (self.density!=1 and (',density='+fp_str(self.density)) or ''),
            (self.knockout is not None and (',knockout=%d' % self.knockout) or ''),
            )

    def __hash__(self):
        return hash( (self.cyan, self.magenta, self.yellow, self.black, self.density, self.spotName) )

    def __cmp__(self,other):
        """Partial ordering of colors according to a notion of distance.

        Comparing across the two color models is of limited use."""
        # why the try-except?  What can go wrong?
        if isinstance(other, CMYKColor):
            dsum = (((( (self.cyan-other.cyan)*2 +
                        (self.magenta-other.magenta))*2+
                        (self.yellow-other.yellow))*2+
                        (self.black-other.black))*2+
                        (self.density-other.density))*2 + cmp(self.spotName or '',other.spotName or '')
        else:  # do the RGB comparison
            try:
                dsum = ((self.red-other.red)*2+(self.green-other.green))*2+(self.blue-other.blue)
            except: # or just return 'not equal' if not a color
                return -1
        if dsum >= 0:
            return dsum>0
        else:
            return -1

    def cmyk(self):
        "Returns a tuple of four color components - syntactic sugar"
        return (self.cyan, self.magenta, self.yellow, self.black)

    def _density_str(self):
        return fp_str(self.density)

class PCMYKColor(CMYKColor):
    '''100 based CMYKColor with density and a spotName; just like Rimas uses'''
    def __init__(self,cyan,magenta,yellow,black,density=100,spotName=None,knockout=None):
        CMYKColor.__init__(self,cyan/100.,magenta/100.,yellow/100.,black/100.,spotName,density/100.,knockout=knockout)

    def __repr__(self):
        return "PCMYKColor(%s%s%s%s)" % (
            string.replace(fp_str(self.cyan*100, self.magenta*100, self.yellow*100, self.black*100),' ',','),
            (self.spotName and (',spotName='+repr(self.spotName)) or ''),
            (self.density!=1 and (',density='+fp_str(self.density*100)) or ''),
            (self.knockout is not None and (',knockout=%d' % self.knockout) or ''),
            )

def cmyk2rgb((c,m,y,k),density=1):
    "Convert from a CMYK color tuple to an RGB color tuple"
    # From the Adobe Postscript Ref. Manual 2nd ed.
    r = 1.0 - min(1.0, c + k)
    g = 1.0 - min(1.0, m + k)
    b = 1.0 - min(1.0, y + k)
    return (r,g,b)

def rgb2cmyk(r,g,b):
    '''one way to get cmyk from rgb'''
    c = 1 - r
    m = 1 - g
    y = 1 - b
    k = min(c,m,y)
    c = min(1,max(0,c-k))
    m = min(1,max(0,m-k))
    y = min(1,max(0,y-k))
    k = min(1,max(0,k))
    return (c,m,y,k)

def color2bw(colorRGB):
    "Transform an RGB color to a black and white equivalent."

    col = colorRGB
    r, g, b = col.red, col.green, col.blue
    n = (r + g + b) / 3.0
    bwColorRGB = Color(n, n, n)
    return bwColorRGB

def HexColor(val, htmlOnly=False):
    """This function converts a hex string, or an actual integer number,
    into the corresponding color.  E.g., in "#AABBCC" or 0xAABBCC,
    AA is the red, BB is the green, and CC is the blue (00-FF).

    For completeness I assume that #aabbcc or 0xaabbcc are hex numbers
    otherwise a pure integer is converted as decimal rgb.  If htmlOnly is true,
    only the #aabbcc form is allowed.

    >>> HexColor('#ffffff')
    Color(1,1,1)
    >>> HexColor('#FFFFFF')
    Color(1,1,1)
    >>> HexColor('0xffffff')
    Color(1,1,1)
    >>> HexColor('16777215')
    Color(1,1,1)

    An '0x' or '#' prefix is required for hex (as opposed to decimal):

    >>> HexColor('ffffff')
    Traceback (most recent call last):
    ValueError: invalid literal for int(): ffffff

    >>> HexColor('#FFFFFF', htmlOnly=True)
    Color(1,1,1)
    >>> HexColor('0xffffff', htmlOnly=True)
    Traceback (most recent call last):
    ValueError: not a hex string
    >>> HexColor('16777215', htmlOnly=True)
    Traceback (most recent call last):
    ValueError: not a hex string

    """ #" for emacs

    if type(val) == StringType:
        b = 10
        if val[:1] == '#':
            val = val[1:]
            b = 16
        else:
            if htmlOnly:
                raise ValueError('not a hex string')
            if string.lower(val[:2]) == '0x':
                b = 16
                val = val[2:]
        val = string.atoi(val,b)
    return Color(((val>>16)&0xFF)/255.0,((val>>8)&0xFF)/255.0,(val&0xFF)/255.0)

def linearlyInterpolatedColor(c0, c1, x0, x1, x):
    """
    Linearly interpolates colors. Can handle RGB, CMYK and PCMYK
    colors - give ValueError if colours aren't the same.
    Doesn't currently handle 'Spot Color Interpolation'.
    """

    if c0.__class__ != c1.__class__:
        raise ValueError, "Color classes must be the same for interpolation!"
    if x1<x0:
        x0,x1,c0,c1 = x1,x0,c1,c0 # normalized so x1>x0
    if x<x0-1e-8 or x>x1+1e-8: # fudge factor for numerical problems
        raise ValueError, "Can't interpolate: x=%f is not between %f and %f!" % (x,x0,x1)
    if x<=x0:
        return c0
    elif x>=x1:
        return c1

    cname = c0.__class__.__name__
    dx = float(x1-x0)
    x = x-x0

    if cname == 'Color': # RGB
        r = c0.red+x*(c1.red - c0.red)/dx
        g = c0.green+x*(c1.green- c0.green)/dx
        b = c0.blue+x*(c1.blue - c0.blue)/dx
        return Color(r,g,b)
    elif cname == 'CMYKColor':
        c = c0.cyan+x*(c1.cyan - c0.cyan)/dx
        m = c0.magenta+x*(c1.magenta - c0.magenta)/dx
        y = c0.yellow+x*(c1.yellow - c0.yellow)/dx
        k = c0.black+x*(c1.black - c0.black)/dx
        d = c0.density+x*(c1.density - c0.density)/dx
        return CMYKColor(c,m,y,k, density=d)
    elif cname == 'PCMYKColor':
        if cmykDistance(c0,c1)<1e-8:
            #colors same do density and preserve spotName if any
            assert c0.spotName == c1.spotName, "Identical cmyk, but different spotName"
            c = c0.cyan
            m = c0.magenta
            y = c0.yellow
            k = c0.black
            d = c0.density+x*(c1.density - c0.density)/dx
            return PCMYKColor(c*100,m*100,y*100,k*100, density=d*100, spotName=c0.spotName)
        elif cmykDistance(c0,_CMYK_white)<1e-8:
            #special c0 is white
            c = c1.cyan
            m = c1.magenta
            y = c1.yellow
            k = c1.black
            d = x*c1.density/dx
            return PCMYKColor(c*100,m*100,y*100,k*100, density=d*100, spotName=c1.spotName)
        elif cmykDistance(c1,_CMYK_white)<1e-8:
            #special c1 is white
            c = c0.cyan
            m = c0.magenta
            y = c0.yellow
            k = c0.black
            d = x*c0.density/dx
            d = c0.density*(1-x/dx)
            return PCMYKColor(c*100,m*100,y*100,k*100, density=d*100, spotName=c0.spotName)
        else:
            c = c0.cyan+x*(c1.cyan - c0.cyan)/dx
            m = c0.magenta+x*(c1.magenta - c0.magenta)/dx
            y = c0.yellow+x*(c1.yellow - c0.yellow)/dx
            k = c0.black+x*(c1.black - c0.black)/dx
            d = c0.density+x*(c1.density - c0.density)/dx
            return PCMYKColor(c*100,m*100,y*100,k*100, density=d*100)
    else:
        raise ValueError, "Can't interpolate: Unknown color class %s!" % cname

# special case -- indicates no drawing should be done
# this is a hangover from PIDDLE - suggest we ditch it since it is not used anywhere
#transparent = Color(-1, -1, -1)

_CMYK_white=CMYKColor(0,0,0,0)
_PCMYK_white=PCMYKColor(0,0,0,0)
_CMYK_black=CMYKColor(0,0,0,1)
_PCMYK_black=PCMYKColor(0,0,0,100)

# Special colors
ReportLabBlueOLD = HexColor(0x4e5688)
ReportLabBlue = HexColor(0x00337f)
ReportLabBluePCMYK = PCMYKColor(100,65,0,30,spotName='Pantone 288U')
ReportLabLightBlue = HexColor(0xb7b9d3)
ReportLabFidBlue=HexColor(0x3366cc)
ReportLabFidRed=HexColor(0xcc0033)
ReportLabGreen = HexColor(0x336600)
ReportLabLightGreen = HexColor(0x339933)

# color constants -- mostly from HTML standard
aliceblue =     HexColor(0xF0F8FF)
antiquewhite =  HexColor(0xFAEBD7)
aqua =  HexColor(0x00FFFF)
aquamarine =    HexColor(0x7FFFD4)
azure =     HexColor(0xF0FFFF)
beige =     HexColor(0xF5F5DC)
bisque =    HexColor(0xFFE4C4)
black =     HexColor(0x000000)
blanchedalmond =    HexColor(0xFFEBCD)
blue =  HexColor(0x0000FF)
blueviolet =    HexColor(0x8A2BE2)
brown =     HexColor(0xA52A2A)
burlywood =     HexColor(0xDEB887)
cadetblue =     HexColor(0x5F9EA0)
chartreuse =    HexColor(0x7FFF00)
chocolate =     HexColor(0xD2691E)
coral =     HexColor(0xFF7F50)
cornflowerblue = cornflower =   HexColor(0x6495ED)
cornsilk =  HexColor(0xFFF8DC)
crimson =   HexColor(0xDC143C)
cyan =  HexColor(0x00FFFF)
darkblue =  HexColor(0x00008B)
darkcyan =  HexColor(0x008B8B)
darkgoldenrod =     HexColor(0xB8860B)
darkgray =  HexColor(0xA9A9A9)
darkgrey =  darkgray
darkgreen =     HexColor(0x006400)
darkkhaki =     HexColor(0xBDB76B)
darkmagenta =   HexColor(0x8B008B)
darkolivegreen =    HexColor(0x556B2F)
darkorange =    HexColor(0xFF8C00)
darkorchid =    HexColor(0x9932CC)
darkred =   HexColor(0x8B0000)
darksalmon =    HexColor(0xE9967A)
darkseagreen =  HexColor(0x8FBC8B)
darkslateblue =     HexColor(0x483D8B)
darkslategray =     HexColor(0x2F4F4F)
darkslategrey = darkslategray
darkturquoise =     HexColor(0x00CED1)
darkviolet =    HexColor(0x9400D3)
deeppink =  HexColor(0xFF1493)
deepskyblue =   HexColor(0x00BFFF)
dimgray =   HexColor(0x696969)
dimgrey = dimgray
dodgerblue =    HexColor(0x1E90FF)
firebrick =     HexColor(0xB22222)
floralwhite =   HexColor(0xFFFAF0)
forestgreen =   HexColor(0x228B22)
fuchsia =   HexColor(0xFF00FF)
gainsboro =     HexColor(0xDCDCDC)
ghostwhite =    HexColor(0xF8F8FF)
gold =  HexColor(0xFFD700)
goldenrod =     HexColor(0xDAA520)
gray =  HexColor(0x808080)
grey = gray
green =     HexColor(0x008000)
greenyellow =   HexColor(0xADFF2F)
honeydew =  HexColor(0xF0FFF0)
hotpink =   HexColor(0xFF69B4)
indianred =     HexColor(0xCD5C5C)
indigo =    HexColor(0x4B0082)
ivory =     HexColor(0xFFFFF0)
khaki =     HexColor(0xF0E68C)
lavender =  HexColor(0xE6E6FA)
lavenderblush =     HexColor(0xFFF0F5)
lawngreen =     HexColor(0x7CFC00)
lemonchiffon =  HexColor(0xFFFACD)
lightblue =     HexColor(0xADD8E6)
lightcoral =    HexColor(0xF08080)
lightcyan =     HexColor(0xE0FFFF)
lightgoldenrodyellow =  HexColor(0xFAFAD2)
lightgreen =    HexColor(0x90EE90)
lightgrey =     HexColor(0xD3D3D3)
lightpink =     HexColor(0xFFB6C1)
lightsalmon =   HexColor(0xFFA07A)
lightseagreen =     HexColor(0x20B2AA)
lightskyblue =  HexColor(0x87CEFA)
lightslategray =    HexColor(0x778899)
lightslategrey = lightslategray
lightsteelblue =    HexColor(0xB0C4DE)
lightyellow =   HexColor(0xFFFFE0)
lime =  HexColor(0x00FF00)
limegreen =     HexColor(0x32CD32)
linen =     HexColor(0xFAF0E6)
magenta =   HexColor(0xFF00FF)
maroon =    HexColor(0x800000)
mediumaquamarine =  HexColor(0x66CDAA)
mediumblue =    HexColor(0x0000CD)
mediumorchid =  HexColor(0xBA55D3)
mediumpurple =  HexColor(0x9370DB)
mediumseagreen =    HexColor(0x3CB371)
mediumslateblue =   HexColor(0x7B68EE)
mediumspringgreen =     HexColor(0x00FA9A)
mediumturquoise =   HexColor(0x48D1CC)
mediumvioletred =   HexColor(0xC71585)
midnightblue =  HexColor(0x191970)
mintcream =     HexColor(0xF5FFFA)
mistyrose =     HexColor(0xFFE4E1)
moccasin =  HexColor(0xFFE4B5)
navajowhite =   HexColor(0xFFDEAD)
navy =  HexColor(0x000080)
oldlace =   HexColor(0xFDF5E6)
olive =     HexColor(0x808000)
olivedrab =     HexColor(0x6B8E23)
orange =    HexColor(0xFFA500)
orangered =     HexColor(0xFF4500)
orchid =    HexColor(0xDA70D6)
palegoldenrod =     HexColor(0xEEE8AA)
palegreen =     HexColor(0x98FB98)
paleturquoise =     HexColor(0xAFEEEE)
palevioletred =     HexColor(0xDB7093)
papayawhip =    HexColor(0xFFEFD5)
peachpuff =     HexColor(0xFFDAB9)
peru =  HexColor(0xCD853F)
pink =  HexColor(0xFFC0CB)
plum =  HexColor(0xDDA0DD)
powderblue =    HexColor(0xB0E0E6)
purple =    HexColor(0x800080)
red =   HexColor(0xFF0000)
rosybrown =     HexColor(0xBC8F8F)
royalblue =     HexColor(0x4169E1)
saddlebrown =   HexColor(0x8B4513)
salmon =    HexColor(0xFA8072)
sandybrown =    HexColor(0xF4A460)
seagreen =  HexColor(0x2E8B57)
seashell =  HexColor(0xFFF5EE)
sienna =    HexColor(0xA0522D)
silver =    HexColor(0xC0C0C0)
skyblue =   HexColor(0x87CEEB)
slateblue =     HexColor(0x6A5ACD)
slategray =     HexColor(0x708090)
slategrey = slategray
snow =  HexColor(0xFFFAFA)
springgreen =   HexColor(0x00FF7F)
steelblue =     HexColor(0x4682B4)
tan =   HexColor(0xD2B48C)
teal =  HexColor(0x008080)
thistle =   HexColor(0xD8BFD8)
tomato =    HexColor(0xFF6347)
turquoise =     HexColor(0x40E0D0)
violet =    HexColor(0xEE82EE)
wheat =     HexColor(0xF5DEB3)
white =     HexColor(0xFFFFFF)
whitesmoke =    HexColor(0xF5F5F5)
yellow =    HexColor(0xFFFF00)
yellowgreen =   HexColor(0x9ACD32)
fidblue=HexColor(0x3366cc)
fidred=HexColor(0xcc0033)
fidlightblue=HexColor("#d6e0f5")

ColorType=type(black)

    ################################################################
    #
    #  Helper functions for dealing with colors.  These tell you
    #  which are predefined, so you can print color charts;
    #  and can give the nearest match to an arbitrary color object
    #
    #################################################################

def colorDistance(col1, col2):
    """Returns a number between 0 and root(3) stating how similar
    two colours are - distance in r,g,b, space.  Only used to find
    names for things."""
    return math.sqrt(
            (col1.red - col2.red)**2 +
            (col1.green - col2.green)**2 +
            (col1.blue - col2.blue)**2
            )

def cmykDistance(col1, col2):
    """Returns a number between 0 and root(4) stating how similar
    two colours are - distance in r,g,b, space.  Only used to find
    names for things."""
    return math.sqrt(
            (col1.cyan - col2.cyan)**2 +
            (col1.magenta - col2.magenta)**2 +
            (col1.yellow - col2.yellow)**2 +
            (col1.black - col2.black)**2
            )

_namedColors = None

def getAllNamedColors():
    #returns a dictionary of all the named ones in the module
    # uses a singleton for efficiency
    global _namedColors
    if _namedColors is not None: return _namedColors
    import colors
    _namedColors = {}
    for (name, value) in colors.__dict__.items():
        if isinstance(value, Color):
            _namedColors[name] = value

    return _namedColors

def describe(aColor,mode=0):
    '''finds nearest colour match to aColor.
    mode=0 print a string desription
    mode=1 return a string description
    mode=2 return (distance, colorName)
    '''
    namedColors = getAllNamedColors()
    closest = (10, None, None)  #big number, name, color
    for (name, color) in namedColors.items():
        distance = colorDistance(aColor, color)
        if distance < closest[0]:
            closest = (distance, name, color)
    if mode<=1:
        s = 'best match is %s, distance %0.4f' % (closest[1], closest[0])
        if mode==0: print s
        else: return s
    elif mode==2:
        return (closest[1], closest[0])
    else:
        raise ValueError, "Illegal value for mode "+str(mode)

def toColor(arg,default=None):
    '''try to map an arbitrary arg to a color instance'''
    if isinstance(arg,Color): return arg
    tArg = type(arg)
    if tArg in _SeqTypes:
        assert 3<=len(arg)<=4, 'Can only convert 3 and 4 sequences to color'
        assert 0<=min(arg) and max(arg)<=1
        return len(arg)==3 and Color(arg[0],arg[1],arg[2]) or CMYKColor(arg[0],arg[1],arg[2],arg[3])
    elif tArg == StringType:
        C = getAllNamedColors()
        s = string.lower(arg)
        if C.has_key(s): return C[s]
        try:
            return toColor(eval(arg))
        except:
            pass

    try:
        return HexColor(arg)
    except:
        if default is None:
            raise ValueError('Invalid color value %r' % arg)
        return default

def toColorOrNone(arg,default=None):
    '''as above but allows None as a legal value'''
    if arg is None:
        return None
    else:
        return toColor(arg, default)

def setColors(**kw):
    UNDEF = []
    progress = 1
    assigned = {}
    while kw and progress:
        progress = 0
        for k, v in kw.items():
            if type(v) in (type(()),type([])):
                c = map(lambda x,UNDEF=UNDEF: toColor(x,UNDEF),v)
                if type(v) is type(()): c = tuple(c)
                ok = UNDEF not in c
            else:
                c = toColor(v,UNDEF)
                ok = c is not UNDEF
            if ok:
                assigned[k] = c
                del kw[k]
                progress = 1

    if kw: raise ValueError("Can't convert\n%s" % str(kw))
    getAllNamedColors()
    for k, c in assigned.items():
        globals()[k] = c
        if isinstance(c,Color): _namedColors[k] = c

def Whiter(c,f):
    '''given a color combine with white as c*f w*(1-f) 0<=f<=1'''
    c = toColor(c)
    if isinstance(c,PCMYKColor):
        w = _PCMYK_white
    elif isinstance(c,CMYKColor): w = _CMYK_white
    else: w = white
    return linearlyInterpolatedColor(w, c, 0, 1, f)

def Blacker(c,f):
    '''given a color combine with black as c*f+b*(1-f) 0<=f<=1'''
    c = toColor(c)
    if isinstance(c,PCMYKColor):
        b = _PCMYK_black
    elif isinstance(c,CMYKColor): b = _CMYK_black
    else: b = black
    return linearlyInterpolatedColor(b, c, 0, 1, f)


if __name__ == "__main__":
    import doctest
    doctest.testmod()
