# coding: utf-8 """ tinycss.colors3 --------------- Parser for CSS 3 color values http://www.w3.org/TR/css3-color/ This module does not provide anything that integrates in a parser class, only functions that parse single tokens from (eg.) a property value. :copyright: (c) 2012 by Simon Sapin. :license: BSD, see LICENSE for more details. """ from __future__ import division, unicode_literals import collections import itertools import re from .tokenizer import tokenize_grouped class RGBA(collections.namedtuple('RGBA', ['red', 'green', 'blue', 'alpha'])): """An RGBA color. A tuple of four floats in the 0..1 range: ``(r, g, b, a)``. Also has ``red``, ``green``, ``blue`` and ``alpha`` attributes to access the same values. """ def parse_color_string(css_string): """Parse a CSS string as a color value. This is a convenience wrapper around :func:`parse_color` in case you have a string that is not from a CSS stylesheet. :param css_string: An unicode string in CSS syntax. :returns: Same as :func:`parse_color`. """ tokens = list(tokenize_grouped(css_string.strip())) if len(tokens) == 1: return parse_color(tokens[0]) def parse_color(token): """Parse single token as a color value. :param token: A single :class:`~.token_data.Token` or :class:`~.token_data.ContainerToken`, as found eg. in a property value. :returns: * ``None``, if the token is not a valid CSS 3 color value. (No exception is raised.) * For the *currentColor* keyword: the string ``'currentColor'`` * Every other values (including keywords, HSL and HSLA) is converted to RGBA and returned as an :class:`RGBA` object (a 4-tuple with attribute access). The alpha channel is clipped to [0, 1], but R, G, or B can be out of range (eg. ``rgb(-51, 306, 0)`` is represented as ``(-.2, 1.2, 0, 1)``.) """ if token.type == 'IDENT': return COLOR_KEYWORDS.get(token.value.lower()) elif token.type == 'HASH': for multiplier, regexp in HASH_REGEXPS: match = regexp(token.value) if match: r, g, b = [int(group * multiplier, 16) / 255 for group in match.groups()] return RGBA(r, g, b, 1.) elif token.type == 'FUNCTION': args = parse_comma_separated(token.content) if args: name = token.function_name.lower() if name == 'rgb': return parse_rgb(args, alpha=1.) elif name == 'rgba': alpha = parse_alpha(args[3:]) if alpha is not None: return parse_rgb(args[:3], alpha) elif name == 'hsl': return parse_hsl(args, alpha=1.) elif name == 'hsla': alpha = parse_alpha(args[3:]) if alpha is not None: return parse_hsl(args[:3], alpha) def parse_alpha(args): """ If args is a list of a single INTEGER or NUMBER token, retur its value clipped to the 0..1 range Otherwise, return None. """ if len(args) == 1 and args[0].type in ('NUMBER', 'INTEGER'): return min(1, max(0, args[0].value)) def parse_rgb(args, alpha): """ If args is a list of 3 INTEGER tokens or 3 PERCENTAGE tokens, return RGB values as a tuple of 3 floats in 0..1. Otherwise, return None. """ types = [arg.type for arg in args] if types == ['INTEGER', 'INTEGER', 'INTEGER']: r, g, b = [arg.value / 255 for arg in args[:3]] return RGBA(r, g, b, alpha) elif types == ['PERCENTAGE', 'PERCENTAGE', 'PERCENTAGE']: r, g, b = [arg.value / 100 for arg in args[:3]] return RGBA(r, g, b, alpha) def parse_hsl(args, alpha): """ If args is a list of 1 INTEGER token and 2 PERCENTAGE tokens, return RGB values as a tuple of 3 floats in 0..1. Otherwise, return None. """ types = [arg.type for arg in args] if types == ['INTEGER', 'PERCENTAGE', 'PERCENTAGE']: hsl = [arg.value for arg in args[:3]] r, g, b = hsl_to_rgb(*hsl) return RGBA(r, g, b, alpha) def hsl_to_rgb(hue, saturation, lightness): """ :param hue: degrees :param saturation: percentage :param lightness: percentage :returns: (r, g, b) as floats in the 0..1 range """ hue = (hue / 360) % 1 saturation = min(1, max(0, saturation / 100)) lightness = min(1, max(0, lightness / 100)) # Translated from ABC: http://www.w3.org/TR/css3-color/#hsl-color def hue_to_rgb(m1, m2, h): if h < 0: h += 1 if h > 1: h -= 1 if h * 6 < 1: return m1 + (m2 - m1) * h * 6 if h * 2 < 1: return m2 if h * 3 < 2: return m1 + (m2 - m1) * (2 / 3 - h) * 6 return m1 if lightness <= 0.5: m2 = lightness * (saturation + 1) else: m2 = lightness + saturation - lightness * saturation m1 = lightness * 2 - m2 return ( hue_to_rgb(m1, m2, hue + 1 / 3), hue_to_rgb(m1, m2, hue), hue_to_rgb(m1, m2, hue - 1 / 3), ) def parse_comma_separated(tokens): """Parse a list of tokens (typically the content of a function token) as arguments made of a single token each, separated by mandatory commas, with optional white space around each argument. return the argument list without commas or white space; or None if the function token content do not match the description above. """ tokens = [token for token in tokens if token.type != 'S'] if not tokens: return [] if len(tokens) % 2 == 1 and all( token.type == 'DELIM' and token.value == ',' for token in tokens[1::2]): return tokens[::2] HASH_REGEXPS = ( (2, re.compile('^#([\da-f])([\da-f])([\da-f])$', re.I).match), (1, re.compile('^#([\da-f]{2})([\da-f]{2})([\da-f]{2})$', re.I).match), ) # (r, g, b) in 0..255 BASIC_COLOR_KEYWORDS = [ ('black', (0, 0, 0)), ('silver', (192, 192, 192)), ('gray', (128, 128, 128)), ('white', (255, 255, 255)), ('maroon', (128, 0, 0)), ('red', (255, 0, 0)), ('purple', (128, 0, 128)), ('fuchsia', (255, 0, 255)), ('green', (0, 128, 0)), ('lime', (0, 255, 0)), ('olive', (128, 128, 0)), ('yellow', (255, 255, 0)), ('navy', (0, 0, 128)), ('blue', (0, 0, 255)), ('teal', (0, 128, 128)), ('aqua', (0, 255, 255)), ] # (r, g, b) in 0..255 EXTENDED_COLOR_KEYWORDS = [ ('aliceblue', (240, 248, 255)), ('antiquewhite', (250, 235, 215)), ('aqua', (0, 255, 255)), ('aquamarine', (127, 255, 212)), ('azure', (240, 255, 255)), ('beige', (245, 245, 220)), ('bisque', (255, 228, 196)), ('black', (0, 0, 0)), ('blanchedalmond', (255, 235, 205)), ('blue', (0, 0, 255)), ('blueviolet', (138, 43, 226)), ('brown', (165, 42, 42)), ('burlywood', (222, 184, 135)), ('cadetblue', (95, 158, 160)), ('chartreuse', (127, 255, 0)), ('chocolate', (210, 105, 30)), ('coral', (255, 127, 80)), ('cornflowerblue', (100, 149, 237)), ('cornsilk', (255, 248, 220)), ('crimson', (220, 20, 60)), ('cyan', (0, 255, 255)), ('darkblue', (0, 0, 139)), ('darkcyan', (0, 139, 139)), ('darkgoldenrod', (184, 134, 11)), ('darkgray', (169, 169, 169)), ('darkgreen', (0, 100, 0)), ('darkgrey', (169, 169, 169)), ('darkkhaki', (189, 183, 107)), ('darkmagenta', (139, 0, 139)), ('darkolivegreen', (85, 107, 47)), ('darkorange', (255, 140, 0)), ('darkorchid', (153, 50, 204)), ('darkred', (139, 0, 0)), ('darksalmon', (233, 150, 122)), ('darkseagreen', (143, 188, 143)), ('darkslateblue', (72, 61, 139)), ('darkslategray', (47, 79, 79)), ('darkslategrey', (47, 79, 79)), ('darkturquoise', (0, 206, 209)), ('darkviolet', (148, 0, 211)), ('deeppink', (255, 20, 147)), ('deepskyblue', (0, 191, 255)), ('dimgray', (105, 105, 105)), ('dimgrey', (105, 105, 105)), ('dodgerblue', (30, 144, 255)), ('firebrick', (178, 34, 34)), ('floralwhite', (255, 250, 240)), ('forestgreen', (34, 139, 34)), ('fuchsia', (255, 0, 255)), ('gainsboro', (220, 220, 220)), ('ghostwhite', (248, 248, 255)), ('gold', (255, 215, 0)), ('goldenrod', (218, 165, 32)), ('gray', (128, 128, 128)), ('green', (0, 128, 0)), ('greenyellow', (173, 255, 47)), ('grey', (128, 128, 128)), ('honeydew', (240, 255, 240)), ('hotpink', (255, 105, 180)), ('indianred', (205, 92, 92)), ('indigo', (75, 0, 130)), ('ivory', (255, 255, 240)), ('khaki', (240, 230, 140)), ('lavender', (230, 230, 250)), ('lavenderblush', (255, 240, 245)), ('lawngreen', (124, 252, 0)), ('lemonchiffon', (255, 250, 205)), ('lightblue', (173, 216, 230)), ('lightcoral', (240, 128, 128)), ('lightcyan', (224, 255, 255)), ('lightgoldenrodyellow', (250, 250, 210)), ('lightgray', (211, 211, 211)), ('lightgreen', (144, 238, 144)), ('lightgrey', (211, 211, 211)), ('lightpink', (255, 182, 193)), ('lightsalmon', (255, 160, 122)), ('lightseagreen', (32, 178, 170)), ('lightskyblue', (135, 206, 250)), ('lightslategray', (119, 136, 153)), ('lightslategrey', (119, 136, 153)), ('lightsteelblue', (176, 196, 222)), ('lightyellow', (255, 255, 224)), ('lime', (0, 255, 0)), ('limegreen', (50, 205, 50)), ('linen', (250, 240, 230)), ('magenta', (255, 0, 255)), ('maroon', (128, 0, 0)), ('mediumaquamarine', (102, 205, 170)), ('mediumblue', (0, 0, 205)), ('mediumorchid', (186, 85, 211)), ('mediumpurple', (147, 112, 219)), ('mediumseagreen', (60, 179, 113)), ('mediumslateblue', (123, 104, 238)), ('mediumspringgreen', (0, 250, 154)), ('mediumturquoise', (72, 209, 204)), ('mediumvioletred', (199, 21, 133)), ('midnightblue', (25, 25, 112)), ('mintcream', (245, 255, 250)), ('mistyrose', (255, 228, 225)), ('moccasin', (255, 228, 181)), ('navajowhite', (255, 222, 173)), ('navy', (0, 0, 128)), ('oldlace', (253, 245, 230)), ('olive', (128, 128, 0)), ('olivedrab', (107, 142, 35)), ('orange', (255, 165, 0)), ('orangered', (255, 69, 0)), ('orchid', (218, 112, 214)), ('palegoldenrod', (238, 232, 170)), ('palegreen', (152, 251, 152)), ('paleturquoise', (175, 238, 238)), ('palevioletred', (219, 112, 147)), ('papayawhip', (255, 239, 213)), ('peachpuff', (255, 218, 185)), ('peru', (205, 133, 63)), ('pink', (255, 192, 203)), ('plum', (221, 160, 221)), ('powderblue', (176, 224, 230)), ('purple', (128, 0, 128)), ('red', (255, 0, 0)), ('rosybrown', (188, 143, 143)), ('royalblue', (65, 105, 225)), ('saddlebrown', (139, 69, 19)), ('salmon', (250, 128, 114)), ('sandybrown', (244, 164, 96)), ('seagreen', (46, 139, 87)), ('seashell', (255, 245, 238)), ('sienna', (160, 82, 45)), ('silver', (192, 192, 192)), ('skyblue', (135, 206, 235)), ('slateblue', (106, 90, 205)), ('slategray', (112, 128, 144)), ('slategrey', (112, 128, 144)), ('snow', (255, 250, 250)), ('springgreen', (0, 255, 127)), ('steelblue', (70, 130, 180)), ('tan', (210, 180, 140)), ('teal', (0, 128, 128)), ('thistle', (216, 191, 216)), ('tomato', (255, 99, 71)), ('turquoise', (64, 224, 208)), ('violet', (238, 130, 238)), ('wheat', (245, 222, 179)), ('white', (255, 255, 255)), ('whitesmoke', (245, 245, 245)), ('yellow', (255, 255, 0)), ('yellowgreen', (154, 205, 50)), ] # (r, g, b, a) in 0..1 or a string marker SPECIAL_COLOR_KEYWORDS = { 'currentcolor': 'currentColor', 'transparent': RGBA(0., 0., 0., 0.), } # RGBA namedtuples of (r, g, b, a) in 0..1 or a string marker COLOR_KEYWORDS = SPECIAL_COLOR_KEYWORDS.copy() COLOR_KEYWORDS.update( # 255 maps to 1, 0 to 0, the rest is linear. (keyword, RGBA(r / 255., g / 255., b / 255., 1.)) for keyword, (r, g, b) in itertools.chain( BASIC_COLOR_KEYWORDS, EXTENDED_COLOR_KEYWORDS))