diff options
author | Thomas Touhey <thomas@touhey.fr> | 2019-05-07 15:27:45 +0200 |
---|---|---|
committer | Thomas Touhey <thomas@touhey.fr> | 2019-05-07 15:27:45 +0200 |
commit | 4c9a490e10b342bd8d9400ec72cfdca323fab9ad (patch) | |
tree | 23379d5e2eb66445bb7c052c78de5d6ad8944c99 | |
parent | 1c056419f6425916bc02a7dd8e20d785f1bbec68 (diff) |
Remade the whole using generic function utilities.
-rw-r--r-- | .gitignore | 2 | ||||
-rwxr-xr-x | tests/test_rgba.py | 1 | ||||
-rwxr-xr-x | thcolor/__init__.py | 13 | ||||
-rwxr-xr-x | thcolor/_builtin/__init__.py | 8 | ||||
-rwxr-xr-x | thcolor/_builtin/_css.py | 209 | ||||
-rwxr-xr-x | thcolor/_color.py | 497 | ||||
-rw-r--r-- | thcolor/_exc.py | 133 | ||||
-rw-r--r-- | thcolor/_named.py | 31 | ||||
-rw-r--r-- | thcolor/_ref.py | 295 | ||||
-rwxr-xr-x | thcolor/_sys.py | 50 |
10 files changed, 839 insertions, 400 deletions
@@ -1,5 +1,5 @@ __pycache__ -/test.py +/test*.py /*.egg-info /dist /.spyproject diff --git a/tests/test_rgba.py b/tests/test_rgba.py index 6781aa8..f6af1f9 100755 --- a/tests/test_rgba.py +++ b/tests/test_rgba.py @@ -13,6 +13,7 @@ from thcolor import Color ('#12345F', ( 18, 52, 95, 1.0)), ('#123', ( 17, 34, 51, 1.0)), ('123', ( 1, 2, 3, 1.0)), + ('123.0', ( 18, 48, 0, 1.0)), ('chucknorris', (192, 0, 0, 1.0)), ('rgb(1, 22,242)', ( 1, 22, 242, 1.0)), (' rgb (1,22, 242 , 50.0% )', ( 1, 22, 242, 0.5)), diff --git a/thcolor/__init__.py b/thcolor/__init__.py index 959b647..7113573 100755 --- a/thcolor/__init__.py +++ b/thcolor/__init__.py @@ -11,13 +11,14 @@ """ from ._color import Color -from ._named import NamedColors -from ._builtin import (CSS1NamedColors, CSS2NamedColors, CSS3NamedColors, - CSS4NamedColors) +from ._ref import Reference +from ._exc import ColorExpressionDecodingError +from ._builtin import (CSS1Reference, CSS2Reference, CSS3Reference, + CSS4Reference) -__all__ = ["version", "Color", "NamedColors", - "CSS1NamedColors", "CSS2NamedColors", "CSS3NamedColors", - "CSS4NamedColors"] +__all__ = ["version", "Color", "Reference", "ColorExpressionDecodingError", + "CSS1Reference", "CSS2Reference", "CSS3Reference", + "CSS4Reference"] version = "0.1" diff --git a/thcolor/_builtin/__init__.py b/thcolor/_builtin/__init__.py index b34966a..3b77cf3 100755 --- a/thcolor/_builtin/__init__.py +++ b/thcolor/_builtin/__init__.py @@ -5,10 +5,10 @@ #****************************************************************************** """ Named colors references, using various sources. """ -from ._css import (CSS1NamedColors, CSS2NamedColors, CSS3NamedColors, - CSS4NamedColors) +from ._css import (CSS1Reference, CSS2Reference, CSS3Reference, + CSS4Reference) -__all__ = ["CSS1NamedColors", "CSS2NamedColors", "CSS3NamedColors", - "CSS4NamedColors"] +__all__ = ["CSS1Reference", "CSS2Reference", "CSS3Reference", + "CSS4Reference"] # End of file. diff --git a/thcolor/_builtin/_css.py b/thcolor/_builtin/_css.py index 5cea361..2f075be 100755 --- a/thcolor/_builtin/_css.py +++ b/thcolor/_builtin/_css.py @@ -3,22 +3,30 @@ # Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> # This file is part of the textoutpc project, which is MIT-licensed. #****************************************************************************** -""" Named colors definitions. Color names are case-insensitive. +""" Named colors and function definitions. Color names are case-insensitive. Taken from: https://www.w3schools.com/cssref/css_colors.asp """ from .._color import Color as _Color -from .._named import NamedColors as _NamedColors +from .._ref import Reference as _Reference +from .._exc import InvalidArgumentValueError as _InvalidArgumentValueError -__all__ = ["CSS1NamedColors", "CSS2NamedColors", "CSS3NamedColors", - "CSS4NamedColors"] +__all__ = ["CSS1Reference", "CSS2Reference", "CSS3Reference", + "CSS4Reference"] def _rgb(hex): - return _Color.from_text(hex, _NamedColors()) + return _Color.from_text(hex, _Reference()) -class CSS1NamedColors(_NamedColors): +class CSS1Reference(_Reference): """ Named colors from CSS Level 1: https://www.w3.org/TR/CSS1/ """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # --- + # Named colors. + # --- + __colors = { 'black': _rgb('#000000'), 'silver': _rgb('#c0c0c0'), @@ -38,32 +46,46 @@ class CSS1NamedColors(_NamedColors): 'teal': _rgb('#008080'), 'aqua': _rgb('#00ffff')} - def get(self, name): + def _color(self, name): if name == 'transparent': return _Color(_Color.Type.RGB, 0, 0, 0, 0) try: return self.__colors[name] except: - return super().get(name) + return super()._color(name) -class CSS2NamedColors(CSS1NamedColors): +class CSS2Reference(CSS1Reference): """ Named colors from CSS Level 2 (Revision 1): https://www.w3.org/TR/CSS2/ """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # --- + # Named colors. + # --- + __colors = { 'orange': _rgb('#ffa500')} - def get(self, name): + def _color(self, name): try: return self.__colors[name] except: - return super().get(name) + return super()._color(name) -class CSS3NamedColors(CSS2NamedColors): +class CSS3Reference(CSS2Reference): """ Named colors from CSS Color Module Level 3: https://drafts.csswg.org/css-color-3/ """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # --- + # Named colors. + # --- + __colors = { 'darkblue': _rgb('#00008B'), 'mediumblue': _rgb('#0000CD'), @@ -196,23 +218,176 @@ class CSS3NamedColors(CSS2NamedColors): 'lightyellow': _rgb('#FFFFE0'), 'ivory': _rgb('#FFFFF0')} - def get(self, name): + def _color(self, name): try: return self.__colors[name] except: - return super().get(name) + return super()._color(name) -class CSS4NamedColors(CSS3NamedColors): +class CSS4Reference(CSS3Reference): """ Named colors from CSS Color Module Level 4: https://drafts.csswg.org/css-color/ """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + number = _Reference.number + percentage = _Reference.percentage + angle = _Reference.angle + color = _Reference.color + + # --- + # Named colors. + # --- + __colors = { 'rebeccapurple': _rgb('#663399')} - def get(self, name): + def _color(self, name): try: return self.__colors[name] except: - return super().get(name) + return super()._color(name) + + # --- + # Utilities. + # --- + + def _rgb(self, rgba, rgb_indexes): + r, g, b, alpha = rgba + ri, gi, bi = rgb_indexes + + try: + r = r.to_byte() + except ValueError as e: + raise _InvalidArgumentValueError(ri, str(e)) + + try: + g = g.to_byte() + except ValueError as e: + raise _InvalidArgumentValueError(gi, str(e)) + + try: + b = b.to_byte() + except ValueError as e: + raise _InvalidArgumentValueError(bi, str(e)) + + try: + alpha = alpha.to_factor() + except ValueError as e: + raise _InvalidArgumentValueError(3, str(e)) + + return _Reference.color(_Color(_Color.Type.RGB, r, g, b, alpha)) + + def _hsl(self, hsla, hsl_indexes): + h, s, l, alpha = hsla + hi, si, li = hsl_indexes + + raise NotImplementedError + + def _hwb(self, hwba, hwb_indexes): + h, w, b, alpha = hwba + hi, wi, bi = hwb_indexes + + raise NotImplementedError + + # --- + # Functions. + # --- + + def rgb(self, r: number | percentage, + g: number | percentage = number(0), b: number | percentage = number(0), + alpha: number | percentage = number(1.0)): + return self._rgb((r, g, b, alpha), (0, 1, 2)) + + def rgba(self, r: number | percentage, + g: number | percentage = number(0), b: number | percentage = number(0), + alpha: number | percentage = number(1.0)): + return self._rgb((r, g, b, alpha), (0, 1, 2)) + + def rbg(self, r: number | percentage, + b: number | percentage = number(0), g: number | percentage = number(0), + alpha: number | percentage = number(1.0)): + return self._rgb((r, g, b, alpha), (0, 2, 1)) + + def rbga(self, r: number | percentage, + b: number | percentage = number(0), g: number | percentage = number(0), + alpha: number | percentage = number(1.0)): + return self._rgb((r, g, b, alpha), (0, 2, 1)) + + def brg(self, b: number | percentage, + r: number | percentage = number(0), g: number | percentage = number(0), + alpha: number | percentage = number(1.0)): + return self._rgb((r, g, b, alpha), (1, 2, 0)) + + def brga(self, b: number | percentage, + r: number | percentage = number(0), g: number | percentage = number(0), + alpha: number | percentage = number(1.0)): + return self._rgb((r, g, b, alpha), (1, 2, 0)) + + def bgr(self, b: number | percentage, + g: number | percentage = number(0), r: number | percentage = number(0), + alpha: number | percentage = number(1.0)): + return self._rgb((r, g, b, alpha), (2, 1, 0)) + + def bgra(self, b: number | percentage, + g: number | percentage = number(0), r: number | percentage = number(0), + alpha: number | percentage = number(1.0)): + return self._rgb((r, g, b, alpha), (2, 1, 0)) + + def gbr(self, g: number | percentage, + b: number | percentage = number(0), r: number | percentage = number(0), + alpha: number | percentage = number(1.0)): + return self._rgb((r, g, b, alpha), (2, 0, 1)) + + def gbra(self, g: number | percentage, + b: number | percentage = number(0), r: number | percentage = number(0), + alpha: number | percentage = number(1.0)): + return self._rgb((r, g, b, alpha), (2, 0, 1)) + + def grb(self, g: number | percentage, + r: number | percentage = number(0), b: number | percentage = number(0), + alpha: number | percentage = number(1.0)): + return self._rgb((r, g, b, alpha), (1, 0, 2)) + + def grba(self, g: number | percentage, + r: number | percentage = number(0), b: number | percentage = number(0), + alpha: number | percentage = number(1.0)): + return self._rgb((r, g, b, alpha), (1, 0, 2)) + + def hsl(self, h: number | angle, s: number | percentage, + l: number | percentage, alpha: number | percentage = number(1.0)): + return self._hsl((h, s, l, alpha), (0, 1, 2)) + + def hsla(self, h: number | angle, s: number | percentage, + l: number | percentage, alpha: number | percentage = number(1.0)): + return self._hsl((h, s, l, alpha), (0, 1, 2)) + + def hls(self, h: number | angle, l: number | percentage, + s: number | percentage, alpha: number | percentage = number(1.0)): + return self._hsl((h, s, l, alpha), (0, 2, 1)) + + def hlsa(self, h: number | angle, l: number | percentage, + s: number | percentage, alpha: number | percentage = number(1.0)): + return self._hsl((h, s, l, alpha), (0, 2, 1)) + + def hwb(self, h: number | angle, w: number | percentage, + b: number | percentage, alpha: number | percentage = number(1.0)): + return self._hwb((h, w, b, alpha), (0, 1, 2)) + + def hwba(self, h: number | angle, w: number | percentage, + b: number | percentage, alpha: number | percentage = number(1.0)): + return self._hwb((h, w, b, alpha), (0, 1, 2)) + + def lab(self, l: number, a: number, b: number, + alpha: percentage = number(1.0)): + raise NotImplementedError + + def lch(self, l: number, c: number, h: number | angle, + alpha: percentage = number(1.0)): + raise NotImplementedError + + def gray(self, g: number, alpha: percentage = number(1.0)): + raise NotImplementedError # End of file. diff --git a/thcolor/_color.py b/thcolor/_color.py index e10a719..a300608 100755 --- a/thcolor/_color.py +++ b/thcolor/_color.py @@ -7,150 +7,42 @@ Defines the `get_color()` function which returns an rgba value. """ -import re as _re -import math as _math +import regex as _re from enum import Enum as _Enum -from ._named import NamedColors as _NamedColors -from ._sys import hls_to_rgb as _hls_to_rgb, hwb_to_rgb as _hwb_to_rgb +from ._ref import Reference as _Reference +from ._sys import (hls_to_rgb as _hls_to_rgb, hwb_to_rgb as _hwb_to_rgb, + netscape_color as _netscape_color) +from ._exc import (\ + ColorExpressionDecodingError as _ColorExpressionDecodingError, + NotEnoughArgumentsError as _NotEnoughArgumentsError, + TooManyArgumentsError as _TooManyArgumentsError, + InvalidArgumentTypeError as _InvalidArgumentTypeError, + InvalidArgumentValueError as _InvalidArgumentValueError) __all__ = ["Color"] # --- -# Color decoding elements. +# Decoding utilities. # --- -_cr = _re.compile(r""" - rgba?\s*\( - \s* ((?P<rgb_r>[0-9]{1,3}) - |(?P<rgb_r_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s] - \s* ((?P<rgb_g>[0-9]{1,3}) - |(?P<rgb_g_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s] - \s* ((?P<rgb_b>[0-9]{1,3}) - |(?P<rgb_b_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s/] - \s* ((?P<rgb_a_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*% - |(?P<rgb_a_flt> (0*[01]\.?|0*\.[0-9]+) )) \s* - )?)?)? - \)| - rbga?\s*\( - \s* ((?P<rbg_r>[0-9]{1,3}) - |(?P<rbg_r_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s] - \s* ((?P<rbg_b>[0-9]{1,3}) - |(?P<rbg_b_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s] - \s* ((?P<rbg_g>[0-9]{1,3}) - |(?P<rbg_g_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s/] - \s* ((?P<rbg_a_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*% - |(?P<rbg_a_flt> (0*[01]\.?|0*\.[0-9]+) )) \s* - )?)?)? - \)| - brga?\s*\( - \s* ((?P<brg_b>[0-9]{1,3}) - |(?P<brg_b_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s] - \s* ((?P<brg_r>[0-9]{1,3}) - |(?P<brg_r_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s] - \s* ((?P<brg_g>[0-9]{1,3}) - |(?P<brg_g_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s/] - \s* ((?P<brg_a_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*% - |(?P<brg_a_flt> (0*[01]\.?|0*\.[0-9]+) )) \s* - )?)?)? - \)| - bgra?\s*\( - \s* ((?P<bgr_b>[0-9]{1,3}) - |(?P<bgr_b_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s] - \s* ((?P<bgr_g>[0-9]{1,3}) - |(?P<bgr_g_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s] - \s* ((?P<bgr_r>[0-9]{1,3}) - |(?P<bgr_r_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s/] - \s* ((?P<bgr_a_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*% - |(?P<bgr_a_flt> (0*[01]\.?|0*\.[0-9]+) )) \s* - )?)?)? - \)| - grba?\s*\( - \s* ((?P<grb_g>[0-9]{1,3}) - |(?P<grb_g_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s] - \s* ((?P<grb_r>[0-9]{1,3}) - |(?P<grb_r_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s] - \s* ((?P<grb_b>[0-9]{1,3}) - |(?P<grb_b_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s/] - \s* ((?P<grb_a_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*% - |(?P<grb_a_flt> (0*[01]\.?|0*\.[0-9]+) )) \s* - )?)?)? - \)| - gbra?\s*\( - \s* ((?P<gbr_g>[0-9]{1,3}) - |(?P<gbr_g_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s] - \s* ((?P<gbr_b>[0-9]{1,3}) - |(?P<gbr_b_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s] - \s* ((?P<gbr_r>[0-9]{1,3}) - |(?P<gbr_r_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s/] - \s* ((?P<gbr_a_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*% - |(?P<gbr_a_flt> (0*[01]\.?|0*\.[0-9]+) )) \s* - )?)?)? - \)| - lab\s*\( - \s* (?P<lab_l>-?[0-9]{1,3}) \s* [,\\s] - \s* (?P<lab_a>-?[0-9]{1,3}) \s* [,\\s] - \s* (?P<lab_b>-?[0-9]{1,3}) \s* ([,\\s/] - \s* ((?P<lab_a_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*% - |(?P<lab_a_flt> (0*[01]\.?|0*\.[0-9]+) )) \s* - )? - \)| - lch\s*\( - \s* (?P<lch_l>-?[0-9]{1,3}) \s* [,\\s] - \s* (?P<lch_ch>-?[0-9]{1,3}) \s* [,\\s] - \s* (?P<lch_hue>-? ([0-9]+\.?|[0-9]*\.[0-9]+) ) - (?P<lch_agl>deg|grad|rad|turn|) \s* ([,\\s/] - \s* ((?P<lch_a_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*% - |(?P<lch_a_flt> (0*[01]\.?|0*\.[0-9]+) )) \s* - )? - \)| - gray\s*\( - \s* (?P<gray_l>-?[0-9]{1,3}) \s* ([,\\s/] - \s* ((?P<gray_a_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*% - |(?P<gray_a_flt> (0*[01]\.?|0*\.[0-9]+) )) \s* - )? - \)| - hsla?\s*\( - \s* (?P<hsl_hue>-? ([0-9]+\.?|[0-9]*\.[0-9]+) ) - (?P<hsl_agl>deg|grad|rad|turn|) \s*[,\\s] - \s* ((?P<hsl_sat_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*% - |(?P<hsl_sat_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*[,\\s] - \s* ((?P<hsl_lgt_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*% - |(?P<hsl_lgt_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*([,\\s/] - \s* ((?P<hsl_aph_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*% - |(?P<hsl_aph_flt> (0*[01]\.?|0*\.[0-9]+) )) \s* - )? - \)| - hlsa?\s*\( - \s* (?P<hls_hue>-? ([0-9]+\.?|[0-9]*\.[0-9]+) ) - (?P<hls_agl>deg|grad|rad|turn|) \s*[,\\s] - \s* ((?P<hls_lgt_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*% - |(?P<hls_lgt_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*[,\\s] - \s* ((?P<hls_sat_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*% - |(?P<hls_sat_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*([,\\s/] - \s* ((?P<hls_aph_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*% - |(?P<hls_aph_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*)? - \)| - hwb\s*\( - \s* (?P<hwb_hue>-? ([0-9]+\.?|[0-9]*\.[0-9]+) ) - (?P<hwb_agl>deg|grad|rad|turn|) \s*[,\\s] - \s* ((?P<hwb_wht_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*% - |(?P<hwb_wht_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*[,\\s] - \s* ((?P<hwb_blk_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*% - |(?P<hwb_blk_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*([,\\s/] - \s* ((?P<hwb_aph_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*% - |(?P<hwb_aph_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*)? - \)| - \# (?P<hex_digits> [0-9a-f]+) - | - (?P<legacy_chars> [0-9a-z]+) -""", _re.VERBOSE | _re.I | _re.M) - -_rgb = ('rgb', 'rbg', 'brg', 'bgr', 'grb', 'gbr') +_ColorPattern = _re.compile(r""" + ( + ((?P<agl_val>-? ([0-9]+\.?|[0-9]*\.[0-9]+)) \s* + (?P<agl_typ>deg|grad|rad|turn)) + | ((?P<per>[0-9]+(\.[0-9]*)? | \.[0-9]+) \s* \%) + | (?P<num>[0-9]+(\.[0-9]*)? | \.[0-9]+) + | (\# (?P<hex>[0-9a-f]{3} | [0-9a-f]{4} | [0-9a-f]{6} | [0-9a-f]{8})) + | ((?P<name>[a-z]([a-z0-9\s_-]*[a-z0-9_-])?) + ( \s* \( \s* (?P<arg> (?0)? ) \s* \) )?) + ) + + \s* ((?P<sep>[,/\s]) \s* (?P<nextargs> (?0))?)? +""", _re.VERBOSE | _re.I | _re.M | _re.V1) # --- -# Formats. +# Color initialization varargs utilities. # --- def _byte(name, value): @@ -224,6 +116,8 @@ class Color: args += (('hue', self._hue), ('whiteness', self._wht), ('blackness', self._blk)) + args += (('alpha', self._alpha),) + argtext = ', '.join(f'{key}: {repr(value)}' for key, value in args) return f"{self.__class__.__name__}({argtext})" @@ -424,227 +318,150 @@ class Color: return Color.from_text(value) - def from_text(value, named = None): + def from_text(expr, ref = None): """ Get a color from a string. """ - if named is None: - named = _NamedColors.default() - if not isinstance(named, _NamedColors): - raise ValueError("named is not a NamedColors instance") - - # Check if is a named color. - - value = value.strip() - - try: - return named.get(value) - except: - pass - - # Initialize the alpha. - - alpha = 1.0 - - # Get the match. - - match = _cr.fullmatch(value) - if not match: - raise ValueError("invalid color string") - - match = match.groupdict() - - if match['hex_digits'] or match['legacy_chars']: - # Imitate the Netscape behaviour. Find more about this here: - # https://stackoverflow.com/a/8333464 - # - # I've also extended the thing as I could to introduce more - # modern syntaxes described on the dedicated MDN page: - # https://developer.mozilla.org/en-US/docs/Web/CSS/color_value - # - # First of all, depending on our source, we will act differently: - # - if we are using the `hex_digits` source, then we use the modern - # behaviour and do the fancy things such as `#ABC -> #AABBCC` - # management and possible alpha decoding; - # - if we are using the `legacy_chars` source, then we sanitize our - # input by replacing invalid characters by '0' characters (the - # 0xFFFF limit is due to how UTF-16 was managed at the time). - # We shall also truncate our input to 128 characters. - # - # After these sanitization options, we will keep the same method as - # for legacy color decoding. It should work and be tolerant enough… - - members = 3 - if match['hex_digits']: - hx = match['hex_digits'].lower() - - # RGB and RGBA (3 and 4 char.) notations. - - if len(hx) in (3, 4): - hx = hx[0:1] * 2 + hx[1:2] * 2 + hx[2:3] * 2 + hx[3:4] * 2 - - # Check if there is transparency or not. - - if len(hx) % 3 != 0 and len(hx) % 4 == 0: - members = 4 + if ref is None: + ref = _Reference.default() + if not isinstance(ref, _Reference): + raise ValueError("ref is expected to be a subclass of Reference") + + class argument: + def __init__(self, column, value): + self._column = column + self._value = value + + def __repr__(self): + return f"{self.__class__.__name__}(column = {self._column}, " \ + f"value = {repr(self._value)})" + + @property + def column(self): + return self._column + + @property + def value(self): + return self._value + + def recurse(column, match): + if not match: + return () + + if match['agl_val'] is not None: + # The matched value is an angle. + + value = _Reference.angle(float(match['agl_val']), + match['agl_typ']) + elif match['per'] is not None: + # The matched value is a percentage. + + value = float(match['per']) + value = _Reference.percentage(value) + elif match['num'] is not None: + # The matched value is a number. + + value = _Reference.number(match['num']) + elif match['hex'] is not None: + # The matched value is a hex color. + + name = match['hex'] + + if len(name) <= 4: + name = ''.join(map(lambda x: x + x, name)) + + r = int(name[0:2], 16) + g = int(name[2:4], 16) + b = int(name[4:6], 16) + a = int(name[6:8], 16) / 255.0 if len(name) == 8 else 1.0 + + value = _Reference.color(Color(Color.Type.RGB, r, g, b, a)) + elif match['arg'] is not None: + # The matched value is a function. + + name = match['name'] + + # Get the arguments. + + args = recurse(column + match.start('arg'), + _ColorPattern.fullmatch(match['arg'])) + + # Get the function and call it with the arguments. + + try: + func = ref.functions[name] + except KeyError: + raise _ColorExpressionDecodingError("no such function " \ + f"{repr(name)}", column = column) + + try: + value = func(*map(lambda x: x.value, args)) + except _NotEnoughArgumentsError as e: + raise _ColorExpressionDecodingError("not enough " \ + f"arguments (expected at least {e.count} arguments)", + column = column, func = name) + except _TooManyArgumentsError as e: + raise _ColorExpressionDecodingError("extraneous " \ + f"argument (expected {e.count} arguments at most)", + column = args[e.count].column, func = name) + except _InvalidArgumentTypeError as e: + raise _ColorExpressionDecodingError("type mismatch for " \ + f"argument {e.index + 1}: expected {e.expected}, " \ + f"got {e.got}", column = args[e.index].column, + func = name) + except _InvalidArgumentValueError as e: + raise _ColorExpressionDecodingError("erroneous value " \ + f"for argument {e.index + 1}: {e.text}", + column = args[e.index].column, func = name) + except NotImplementedError: + raise _ColorExpressionDecodingError("not implemented", + column = column, func = name) + else: + # The matched value is a named color. - else: # our source is `legacy_chars` - hx = match['legacy_chars'].lower() - hx = ''.join(c if c in '0123456789abcdef' \ - else ('0', '00')[ord(c) > 0xFFFF] for c in hx[:128])[:128] + name = match['name'] - # First, calculate some values we're going to need. - # `iv` is the size of the zone for a member. - # `sz` is the size of the digits slice to take in that zone - # (max. 8). - # `of` is the offset in the zone of the slice to take. + try: + # Get the named color (e.g. 'blue'). - iv = _math.ceil(len(hx) / members) - of = iv - 8 if iv > 8 else 0 - sz = iv - of + value = ref.colors[name] + assert value != None + except: + r, g, b = _netscape_color(name) + value = Color(Color.Type.RGB, r, g, b, 1.0) - # Then isolate the slices using the values calculated above. - # `gr` will be an array of 3 or 4 digit strings (depending on the - # number of members). + value = _Reference.color(value) - gr = list(map(lambda i: hx[i * iv + of:i * iv + iv] \ - .ljust(sz, '0'), range(members))) + return (argument(column, value),) \ + + recurse(column + match.start('nextargs'), + _ColorPattern.fullmatch(match['nextargs'] or "")) - # Check how many digits we can skip at the beginning of each slice. + # Strip the expression. - pre = min(map(lambda x: len(x) - len(x.lstrip('0')), gr)) - pre = min(pre, sz - 2) + lexpr = expr.strip() + column = (len(expr) - len(lexpr)) + expr = lexpr + del lexpr - # Then extract the values. + # Match the expression (and check it as a whole directly). - it = map(lambda x: int('0' + x[pre:pre + 2], 16), gr) - if members == 3: - r, g, b = it - else: - r, g, b, alpha = it - alpha /= 255.0 - - return Color(Color.Type.RGB, r, g, b, alpha) - elif any(match[key + '_r'] or match[key + '_r_per'] for key in _rgb): - # Extract the values. - - for key in _rgb: - if not match[key + '_r'] and not match[key + '_r_per']: - continue - - r = match[f'{key}_r'] - rp = match[f'{key}_r_per'] - g = match[f'{key}_g'] - gp = match[f'{key}_g_per'] - b = match[f'{key}_b'] - bp = match[f'{key}_b_per'] - ap = match[f'{key}_a_per'] - af = match[f'{key}_a_flt'] - break - - r = int(r) if r else int(int(rp) * 255 / 100) - g = int(g) if g else int(int(gp) * 255 / 100) if gp else 0 - b = int(b) if b else int(int(bp) * 255 / 100) if bp else 0 - - if ap: - alpha = float(ap) / 100.0 - elif af: - alpha = float(af) - - return Color(Color.Type.RGB, r, g, b, alpha) - elif match['hsl_hue'] or match['hls_hue']: - # Extract the values. - - if match['hsl_hue']: - hue = float(match['hsl_hue']) - agl = match['hsl_agl'] - - # Saturation. - if match['hsl_sat_per']: - sat = float(match['hsl_sat_per']) / 100.0 - else: - sat = float(match['hsl_sat_flt']) - if sat > 1.0: - sat /= 100.0 + match = _ColorPattern.fullmatch(expr) + if match is None: + raise _ColorExpressionDecodingError("expression parsing failed") - # Light. - if match['hsl_lgt_per']: - lgt = float(match['hsl_lgt_per']) / 100.0 - else: - lgt = float(match['hsl_lgt_flt']) - if lgt > 1.0: - lgt /= 100.0 - - # Alpha value. - if match['hsl_aph_per']: - alpha = float(match['hsl_aph_per']) / 100.0 - elif match['hsl_aph_flt']: - alpha = float(match['hsl_aph_flt']) - else: - hue = float(match['hls_hue']) - agl = match['hls_agl'] + # Get the result and check its type. - # Saturation. - if match['hls_sat_per']: - sat = float(match['hls_sat_per']) / 100.0 - else: - sat = float(match['hls_sat_flt']) + results = recurse(column, match) + if len(results) > 1: + raise _ColorExpressionDecodingError("extraneous value", + column = results[1].column) - # Light. - if match['hls_lgt_per']: - lgt = float(match['hls_lgt_per']) / 100.0 - else: - lgt = float(match['hls_lgt_flt']) - - # Alpha value. - if match['hls_aph_per']: - alpha = float(match['hls_aph_per']) / 100.0 - elif match['hls_aph_flt']: - alpha = float(match['hls_aph_flt']) - - # Prepare the angle. - if agl == 'grad': - hue = hue * 400.0 - elif agl == 'rad': - hue = hue / (2 * _math.pi) - elif not agl or agl == 'deg': - hue = hue / 360.0 - hue = hue % 1.0 - - if sat > 1 or lgt > 1: - raise Exception - - return Color(Color.Type.HSL, hue = hue, - saturation = sat, lightness = lgt) - elif match['hwb_hue']: - hue = float(match['hwb_hue']) - agl = match['hwb_agl'] - - # Prepare the angle. - if agl == 'grad': - hue = hue * 400.0 - elif agl == 'rad': - hue = hue / (2 * _math.pi) - elif not agl or agl == 'deg': - hue = hue / 360.0 - hue = hue % 1.0 - - # Saturation. - if match['hwb_wht_per']: - wht = float(match['hwb_wht_per']) / 100.0 - else: - wht = float(match['hwb_wht_flt']) - - # Light. - if match['hwb_blk_per']: - blk = float(match['hwb_blk_per']) / 100.0 - else: - blk = float(match['hwb_blk_flt']) - - if wht > 1 or blk > 1: - raise Exception - - return Color(Color.Type.HWB, hue, wht, blk) + result = results[0].value + try: + result = result.to_color() + except AttributeError: + raise _ColorExpressionDecodingError("expected a color", + column = column) - raise ValueError("unsupported format yet") + return result # End of file. diff --git a/thcolor/_exc.py b/thcolor/_exc.py new file mode 100644 index 0000000..1d858f2 --- /dev/null +++ b/thcolor/_exc.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +#****************************************************************************** +# Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> +# This file is part of the thcolor project, which is MIT-licensed. +#****************************************************************************** +""" Exception definitions, with internal and external exceptions defined. """ + +__all__ = ["ColorExpressionDecodingError", + "NotEnoughArgumentsError", "TooManyArgumentsError", + "InvalidArgumentTypeError", "InvalidArgumentValueError"] + +# --- +# External exceptions. +# --- + +class ColorExpressionDecodingError(Exception): + """ A color decoding error has occurred on the text. """ + + def __init__(self, text, column = None, func = None): + self._column = column + self._func = func + self._text = text + + def __str__(self): + msg = "" + + if self._column is not None: + msg += f"at column {self._column}" + if self._func is not None: + msg += ", " + if self._func is not None: + msg += f"in function {repr(self._func)}" + if msg: + msg += ": " + + return msg + self._text + +# --- +# Internal exceptions. +# --- + +class NotEnoughArgumentsError(Exception): + """ Not enough arguments. """ + + def __init__(self, count, name = None): + self._name = name + self._count = count + + def __str__(self): + msg = "not enough arguments" + if self._name is not None: + msg += f" for function {repr(self._name)}" + msg += f", expected {self._count} arguments at least" + + return msg + + @property + def count(self): + return self._count + +class TooManyArgumentsError(Exception): + """ Too many arguments. """ + + def __init__(self, count, name = None): + self._name = name + self._count = count + + def __str__(self): + msg = "too many arguments" + if self._name is not None: + msg += f" for function {repr(self._name)}" + msg += f", expected {self._count} arguments at most" + + return msg + + @property + def count(self): + return self._count + +class InvalidArgumentTypeError(Exception): + """ Invalid argument type. """ + + def __init__(self, index, expected, got, name = None): + self._name = name + self._index = index + self._expected = expected + self._got = got + + def __str__(self): + msg = f"wrong type for argument {self._index + 1}" + if self._name: + msg += f" of function {repr(self._name)}" + msg += f": expected {self._expected}, got {self._got}" + + return msg + + @property + def index(self): + return self._index + + @property + def expected(self): + return self._expected + + @property + def got(self): + return self._got + +class InvalidArgumentValueError(Exception): + """ Invalid argument value. """ + + def __init__(self, index, text, name = None): + self._name = name + self._index = index + self._text = text + + def __str__(self): + msg = f"erroneous value for argument {self._index + 1}" + if self._name: + msg += f" of function {repr(self._name)}" + msg += f": {self._text}" + + return msg + + @property + def index(self): + return self._index + + @property + def text(self): + return self._text + +# End of file. diff --git a/thcolor/_named.py b/thcolor/_named.py deleted file mode 100644 index 48150fc..0000000 --- a/thcolor/_named.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python3 -#****************************************************************************** -# Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> -# This file is part of the textoutpc project, which is MIT-licensed. -#****************************************************************************** -""" Named color reference, parent class. """ - -__all__ = ["NamedColors"] - -_default_named_colors = None - -class NamedColors: - """ Color reference for named colors. """ - - def __init__(self): - pass - - def get(self, name): - raise KeyError(f"{name}: no such color") from None - - def default(): - global _default_named_colors - - if _default_named_colors is not None: - return _default_named_colors - - from ._builtin import CSS4NamedColors - _default_named_colors = CSS4NamedColors() - return _default_named_colors - -# End of file. diff --git a/thcolor/_ref.py b/thcolor/_ref.py new file mode 100644 index 0000000..b224b52 --- /dev/null +++ b/thcolor/_ref.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 +#****************************************************************************** +# Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> +# This file is part of the thcolor project, which is MIT-licensed. +#****************************************************************************** +""" Named color reference, parent class. """ + +from inspect import (getfullargspec as _getfullargspec, + getmembers as _getmembers, ismethod as _ismethod) +from itertools import count as _count + +from ._sys import netscape_color as _netscape_color +from ._exc import (NotEnoughArgumentsError as _NotEnoughArgumentsError, + TooManyArgumentsError as _TooManyArgumentsError, + InvalidArgumentTypeError as _InvalidArgumentTypeError) + +__all__ = ["Reference"] + +_default_reference = None +_color_cls = None + +def _get_color_class(): + global _color_cls + + if _color_cls is not None: + return _color_cls + + from ._color import Color + _color_cls = Color + return _color_cls + +class _type_or: + """ A type or another. """ + + def __init__(self, type1, type2): + self._type1 = type1 + self._type2 = type2 + + @property + def type1(self): + return self._type1 + + @property + def type2(self): + return self._type2 + + def __repr__(self): + return f"{repr(self._type1)} | {repr(self._type2)}" + + def __str__(self): + return f"{self._type1} or {self._type2}" + + def __or__(self, other): + return type_or(self, other) + + def __contains__(self, other): + return other in self._type1 or other in self._type2 + +# --- +# Main reference definition. +# --- + +class Reference: + """ Function reference for color parsing and operations. """ + + def __init__(self): + pass + + # --- + # Base type and function definitions for parsing. + # --- + + class base_type(type): + """ The metaclass for all types used below. """ + + def __new__(mcls, name, bases, attrs): + return super().__new__(mcls, name, bases, attrs) + + def __init__(self, name, bases, attrs): + self.__name = name + + def __contains__(self, other): + return self if other == self else None + + def __or__(self, other): + return _type_or(self, other) + + def __repr__(self): + return f"<class {repr(self.__name)}>" + + def __str__(self): + return self.__name + + class number(metaclass = base_type): + """ The number type. """ + + def __init__(self, value): + if type(value) == str: + self._strvalue = value + self._value = float(value) + else: + self._value = float(value) + if self._value == int(self._value): + self._strvalue = str(int(self._value)) + else: + self._strvalue = str(value) + + def to_byte(self): + try: + value = int(self._value) + + assert value == self._value + assert 0 <= value < 256 + except: + raise ValueError("unsuitable value for byte conversion: " \ + f"{repr(self._value)}") + + return value + + def to_factor(self): + try: + assert 0.0 <= self._value <= 1.0 + except: + raise ValueError("expected a value between 0.0 and 1.0, got " \ + f"{self._value}") + + return self._value + + def to_color(self): + r, g, b = _netscape_color(self._strvalue) + Color = _get_color_class() + return Color(Color.Type.RGB, r, g, b, 1.0) + + class percentage(metaclass = base_type): + def __init__(self, value): + self._value = value + + assert 0 <= value <= 100 + + def __repr__(self): + return f"{self._value} %" + + def to_byte(self): + return int(self._value / 100 * 255) + + def to_factor(self): + try: + assert 0 <= self._value <= 100 + except: + raise ValueError("expected a value between 0.0 and 1.0, got " \ + f"{self._value}") + + return self._value / 100 + + class angle(metaclass = base_type): + def __init__(self, value, unit): + self._value = value % 360.0 + + self._unit = unit + if not self._unit: + self._unit = 'deg' + + assert self._unit in ('deg', 'grad', 'rad', 'turn') + + def __repr__(self): + return f"{self._value} {self._unit}" + + class color(metaclass = base_type): + def __init__(self, value): + if not isinstance(value, _get_color_class()): + raise ValueError("expected a Color instance") + + self._value = value + + def __repr__(self): + return repr(self._value) + + def to_color(self): + return self._value + + # --- + # Function and named color getters. + # --- + + def _get_functions(self): + """ The functions getter, for getting a function using its + name. """ + + class _FunctionGetter: + def __init__(self, ref): + self._fref = ref + + def __getitem__(self, name): + fref = self._fref + + # Find the method. + + if name[0:1] == '_' or name in ('functions', 'named', + 'default'): + raise KeyError(repr(name)) + + members = dict(_getmembers(fref, predicate = _ismethod)) + + try: + method = members[name] + except (KeyError, AssertionError): + raise KeyError(repr(name)) + + # Make a function separated from the class, copy the + # annotations and add the type check on each argument. + + class _MethodCaller: + def __init__(self, fref, name, func): + self._fref = fref + self._name = name + self._func = func + + self.__annotations__ = func.__annotations__ + try: + del self.__annotations__['self'] + except: + pass + + spec = _getfullargspec(func) + + def annotate(arg_name): + try: + return spec.annotations[arg_name] + except: + return None + + self._args = list(map(annotate, spec.args[1:])) + self._optargs = self._args[-len(spec.defaults):] \ + if spec.defaults else [] + self._args = self._args[:-len(self._optargs)] + + def __call__(self, *args): + if len(args) < len(self._args): + raise _NotEnoughArgumentsError(len(self._args), + self._name) + if len(args) > len(self._args) + len(self._optargs): + raise _TooManyArgumentsError(len(self._args), + self._name) + + arg_types = self._args \ + + self._optargs[:len(args) - len(self._args)] + + for index, arg, exp in zip(_count(), args, arg_types): + if exp is not None and type(arg) not in exp: + raise _InvalidArgumentTypeError(index, + exp, type(arg), self._name) + + return self._func(*args) + + return _MethodCaller(self, name, method) + + return _FunctionGetter(self) + + def _get_colors(self): + """ The colors getter, for getting a named color. """ + + class _ColorGetter: + def __init__(self, ref): + self._cref = ref + + def __getitem__(self, name): + return self._cref._color(name) + + return _ColorGetter(self) + + functions = property(_get_functions) + colors = property(_get_colors) + + # --- + # Default methods. + # --- + + def _color(self, name): + """ Get a named color. """ + + raise KeyError(f'{name}: no such color') + + def default(): + """ Get the default reference. """ + + global _default_reference + + if _default_reference is not None: + return _default_reference + + from ._builtin import CSS4Reference + _default_reference = CSS4Reference() + return _default_reference + +# End of file. diff --git a/thcolor/_sys.py b/thcolor/_sys.py index 686c61f..7b05637 100755 --- a/thcolor/_sys.py +++ b/thcolor/_sys.py @@ -5,9 +5,10 @@ #****************************************************************************** """ Conversions between color systems. """ +from math import ceil as _ceil from colorsys import hls_to_rgb -__all__ = ["hls_to_rgb", "hwb_to_rgb"] +__all__ = ["hls_to_rgb", "hwb_to_rgb", "netscape_color"] def hwb_to_rgb(hue, w, b): """ Convert HWB to RGB color. @@ -19,4 +20,51 @@ def hwb_to_rgb(hue, w, b): return r, g, b +def netscape_color(name): + """ Produce a color from a name (which can be all-text, all-digits or + both), using the Netscape behaviour. """ + + # Find more about this here: https://stackoverflow.com/a/8333464 + # + # First of all: + # - we sanitize our input by replacing invalid characters + # by '0' characters (the 0xFFFF limit is due to how + # UTF-16 was managed at the time). + # - we truncate our input to 128 characters. + + name = name.lower() + name = ''.join(c if c in '0123456789abcdef' \ + else ('0', '00')[ord(c) > 0xFFFF] \ + for c in name[:128])[:128] + + # Then we calculate some values we're going to need. + # `iv` is the size of the zone for a member. + # `sz` is the size of the digits slice to take in that zone + # (max. 8). + # `of` is the offset in the zone of the slice to take. + + iv = _ceil(len(name) / 3) + of = iv - 8 if iv > 8 else 0 + sz = iv - of + + # Then we isolate the slices using the values calculated + # above. `gr` will be an array of 3 or 4 digit strings + # (depending on the number of members). + + gr = list(map(lambda i: name[i * iv + of:i * iv + iv] \ + .ljust(sz, '0'), range(3))) + + # Check how many digits we can skip at the beginning of + # each slice. + + pre = min(map(lambda x: len(x) - len(x.lstrip('0')), gr)) + pre = min(pre, sz - 2) + + # Then we extract the values. + + it = map(lambda x: int('0' + x[pre:pre + 2], 16), gr) + r, g, b = it + + return (r, g, b) + # End of file. |