aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Touhey <thomas@touhey.fr>2019-05-07 15:27:45 +0200
committerThomas Touhey <thomas@touhey.fr>2019-05-07 15:27:45 +0200
commit4c9a490e10b342bd8d9400ec72cfdca323fab9ad (patch)
tree23379d5e2eb66445bb7c052c78de5d6ad8944c99
parent1c056419f6425916bc02a7dd8e20d785f1bbec68 (diff)
Remade the whole using generic function utilities.
-rw-r--r--.gitignore2
-rwxr-xr-xtests/test_rgba.py1
-rwxr-xr-xthcolor/__init__.py13
-rwxr-xr-xthcolor/_builtin/__init__.py8
-rwxr-xr-xthcolor/_builtin/_css.py209
-rwxr-xr-xthcolor/_color.py497
-rw-r--r--thcolor/_exc.py133
-rw-r--r--thcolor/_named.py31
-rw-r--r--thcolor/_ref.py295
-rwxr-xr-xthcolor/_sys.py50
10 files changed, 839 insertions, 400 deletions
diff --git a/.gitignore b/.gitignore
index 8f69dec..11aeb50 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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.