#!/usr/bin/env python3
# Copyright 2019 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import re
import textwrap
from style_variable_generator.opacity import Opacity
from abc import ABC, abstractmethod
def split_args(arg_str):
'''Splits a string of args by comma, taking into account brackets.
'''
num_unmatched = 0
prev_index = 0
for i, c in enumerate(arg_str):
if c == '(':
num_unmatched += 1
elif c == ')':
num_unmatched -= 1
if (num_unmatched < 0):
raise ValueError('too many ")"')
elif c == ',' and num_unmatched == 0:
yield arg_str[prev_index:i].strip()
prev_index = i + 1
if num_unmatched > 0:
raise ValueError('too many "("')
yield arg_str[prev_index:].strip()
# Attempts to parse special variables, returns the Color if successful.
def from_white_black(var):
if var == 'white':
return ColorRGB([255, 255, 255])
if var == 'black':
return ColorRGB([0, 0, 0])
return None
def from_rgb_ref(rgb_ref):
match = re.match(r'^\$([a-z0-9_\.\-]+)\.rgb$', rgb_ref)
if not match:
raise ValueError(f'Expected a reference to an RGB variable: {rgb_ref}')
rgb_var = match.group(1)
color = from_white_black(rgb_var)
if color is None:
return ColorRGBVar(rgb_var + '.rgb')
return color
def ParseColor(value):
def ParseHex(value):
match = re.match(r'^#([0-9a-f]*)$', value)
if not match:
return None
value = match.group(1)
if len(value) != 6:
raise ValueError('Expected #RRGGBB')
return ColorRGB([int(x, 16) for x in textwrap.wrap(value, 2)],
Opacity(1))
def ParseRGB(value):
match = re.match(r'^rgb\((.*)\)$', value)
if not match:
return None
values = match.group(1).split(',')
if len(values) == 1:
color = from_rgb_ref(values[0])
color.opacity = Opacity(1)
return color
if len(values) == 3:
return ColorRGB([int(x) for x in values], Opacity(1))
raise ValueError('rgb() expected to have either 1 reference or 3 ints')
def ParseRGBA(value):
match = re.match(r'^rgba\((.*)\)$', value)
if not match:
return None
values = [x.strip() for x in match.group(1).split(',')]
if len(values) == 2:
color = from_rgb_ref(values[0])
color.opacity = Opacity(values[1])
return color
if len(values) == 4:
return ColorRGB([int(x) for x in values[0:3]], Opacity(values[3]))
raise ValueError('rgba() expected to have either'
'1 reference + alpha, or 3 ints + alpha')
def ParseBlend(value):
match = re.match(r'^blend\((.*)\)$', value)
if not match:
return None
values = list(split_args(match.group(1)))
if len(values) == 2:
# blend(color1, color2)
return ColorBlend([ParseColor(values[0]), ParseColor(values[1])])
elif len(values) == 3:
# blend(color1, blendPercentage%, color2)
blendPercentage = int(re.match(r'(\d+)%', values[1]).group(1))
return ColorBlend([ParseColor(values[0]),
ParseColor(values[2])], blendPercentage)
raise ValueError('Unexpected number of arguments for blend()')
def ParseVariableReference(value):
match = re.match(r'^\$([\w\.\-]+)$', value)
if not match:
return None
var = match.group(1)
color = from_white_black(var)
if color is not None:
color.opacity = Opacity(1)
return color
if value.endswith('.rgb'):
raise ValueError(
'color reference cannot resolve to an rgb reference')
return ColorVar(var)
parsers = [
ParseHex,
ParseRGB,
ParseRGBA,
ParseBlend,
ParseVariableReference,
]
value = re.sub(r'_rgb\b', '.rgb', value)
parsed = None
for p in parsers:
parsed = p(value)
if parsed is not None:
break
if parsed is None:
raise ValueError('Malformed color value')
if not isinstance(parsed, Color):
raise ValueError(repr(parsed))
return parsed
class Color:
'''A representation of a single color value.
This color can be of the following formats:
- #rrggbb
- rgb(r, g, b)
- rgba(r, g, b, a)
- rgba(r, g, b, $named_opacity)
- $other_color
- rgb($other_color.rgb)
- rgba($other_color.rgb, a)
- rgba($other_color.rgb, $named_opacity)
- blend(color1, color2)
NB: The color components that refer to other colors' RGB values must end
with '.rgb'.
'''
@abstractmethod
def GetFormula(self):
pass
@abstractmethod
def __repr__(self):
pass
class ColorRGB(Color):
def __init__(self, rgb=None, opacity=None):
super().__init__()
if rgb is None:
(self.r, self.g, self.b) = [-1, -1, -1]
else:
if not all([(0 <= v <= 255) for v in rgb]):
raise ValueError(f'RGB value out of bounds: {rgb}')
(self.r, self.g, self.b) = rgb
self.opacity = opacity
def GetFormula(self):
a = repr(self.opacity)
return 'rgba(%d, %d, %d, %s)' % (self.r, self.g, self.b, a)
def __repr__(self):
a = repr(self.opacity)
return 'rgba(%d, %d, %d, %s)' % (self.r, self.g, self.b, a)
class ColorRGBVar(Color):
def __init__(self, rgb_var=None, opacity=None):
super().__init__()
if not re.match(r'^[\w\.\-]+\.rgb$', rgb_var):
raise ValueError(f'"{rgb_var}" is not a valid RGBVar value')
self.rgb_var = rgb_var
self.opacity = opacity
def ToVar(self):
assert (self.rgb_var)
return self.rgb_var.replace('.rgb', '')
def GetFormula(self):
a = self.opacity.GetReadableStr()
return '%s @ %s' % (self.rgb_var, a)
def __repr__(self):
a = repr(self.opacity)
return 'rgba(var(--%s), %s)' % (self.rgb_var, a)
class ColorVar(Color):
def __init__(self, var=None):
super().__init__()
if not re.match(r'^[\w\.\-]+$', var):
raise ValueError(f'{var} is not a valid var value')
self.var = var
def GetFormula(self):
return self.var
def __repr__(self):
return 'var(--%s)' % self.var
class ColorBlend(Color):
'''This color is the result of blending two other colors
It uses the "A over B" operation, where A is blended_colors[0] and B is
blended_colors[1]. The mix percentace is `opacity`. If `opacity` is not
provided, the mix percentage may be taken from A's opacity.
'''
def __init__(self, colors=[], blendPercentage=None):
super().__init__()
if len(colors) not in [0, 2]:
raise ValueError(
f'Can only color-mix 2 colors. Found: {len(colors)}')
if not all(isinstance(c, Color) for c in colors):
raise ValueError(f'Non-Color found in {colors}')
self.blended_colors = colors
self.blendPercentage = blendPercentage
def GetFormula(self):
if self.blendPercentage is None:
return 'blend(%s, %s)' % (self.blended_colors[0].GetFormula(),
self.blended_colors[1].GetFormula())
return 'blend(%s, %s, %s)' % (self.blended_colors[0].GetFormula(),
self.blendPercentage,
self.blended_colors[1].GetFormula())
def __repr__(self):
return (
f'blend({repr(self.blended_colors)}, {repr(self.blendPercentage)})'
)