chromatic-python 0.2.2__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. chromatic/__init__.py +2 -103
  2. chromatic/__init__.pyi +74 -0
  3. chromatic/_typing.py +38 -20
  4. chromatic/_version.py +2 -2
  5. chromatic/color/__init__.py +2 -5
  6. chromatic/color/__init__.pyi +86 -0
  7. chromatic/color/colorconv.py +59 -42
  8. chromatic/color/core.py +987 -1066
  9. chromatic/color/core.pyi +230 -134
  10. chromatic/color/iterators.py +3 -3
  11. chromatic/color/palette.py +465 -458
  12. chromatic/color/palette.pyi +173 -191
  13. chromatic/data/__init__.py +15 -177
  14. chromatic/data/__init__.pyi +9 -15
  15. chromatic/data/_fetchers.py +55 -0
  16. chromatic/data/registry.json +5 -0
  17. chromatic/data/userfont.py +132 -0
  18. chromatic/data/userfont.pyi +28 -0
  19. chromatic/data/userfont.schema.json +28 -0
  20. chromatic/demo.py +153 -132
  21. chromatic/image/__init__.pyi +70 -0
  22. chromatic/{ascii → image}/_array.py +123 -55
  23. chromatic/{ascii → image}/_curses.py +8 -4
  24. chromatic/{ascii → image}/_glyph_proc.py +9 -3
  25. {chromatic_python-0.2.2.dist-info → chromatic_python-0.3.0.dist-info}/METADATA +2 -2
  26. chromatic_python-0.3.0.dist-info/RECORD +35 -0
  27. {chromatic_python-0.2.2.dist-info → chromatic_python-0.3.0.dist-info}/WHEEL +1 -1
  28. chromatic/data/images/hotdog.jpg +0 -0
  29. chromatic_python-0.2.2.dist-info/RECORD +0 -28
  30. /chromatic/data/{images/butterfly.jpg → butterfly.jpg} +0 -0
  31. /chromatic/data/{images/escher.png → escher.png} +0 -0
  32. /chromatic/data/fonts/{IBM_VGA_437_8x16.ttf → vga437.ttf} +0 -0
  33. /chromatic/data/{images/goblin_virus.png → goblin_virus.png} +0 -0
  34. /chromatic/{ascii → image}/__init__.py +0 -0
  35. {chromatic_python-0.2.2.dist-info → chromatic_python-0.3.0.dist-info}/licenses/LICENSE +0 -0
  36. {chromatic_python-0.2.2.dist-info → chromatic_python-0.3.0.dist-info}/top_level.txt +0 -0
chromatic/__init__.py CHANGED
@@ -3,107 +3,6 @@ try:
3
3
  except ImportError:
4
4
  __version__ = "0.0.0"
5
5
 
6
- from . import ascii, color, data
7
- from .ascii import (
8
- AnsiImage,
9
- ansi2img,
10
- ansify,
11
- ansi_quantize,
12
- ascii2img,
13
- ascii_printable,
14
- contrast_stretch,
15
- cp437_printable,
16
- equalize_white_point,
17
- get_font_key,
18
- get_font_object,
19
- img2ansi,
20
- img2ascii,
21
- read_ans,
22
- render_ans,
23
- reshape_ansi,
24
- to_sgr_array,
25
- )
26
- from .ascii._glyph_proc import get_glyph_masks
27
- from .color import (
28
- Back,
29
- Color,
30
- ColorNamespace,
31
- ColorStr,
32
- Fore,
33
- SgrParameter,
34
- Style,
35
- ansicolor24Bit,
36
- ansicolor4Bit,
37
- ansicolor8Bit,
38
- colorbytes,
39
- named_color,
40
- )
41
- from .data import register_user_font
6
+ import lazy_loader as _lazy
42
7
 
43
- __all__ = []
44
-
45
- try:
46
- import os
47
- import sys
48
- from functools import lru_cache, wraps
49
- from types import ModuleType
50
-
51
- def find_modules(path: str):
52
- from setuptools import find_packages
53
- from pkgutil import iter_modules
54
-
55
- tree: dict[str, dict | ModuleType] = {__name__: {'__module__': sys.modules[__name__]}}
56
- children = set()
57
- for pkg in find_packages(path):
58
- children.add(pkg)
59
- pkg_path = path + '/' + pkg.replace('.', '/')
60
- if sys.version_info.major == 2 or (
61
- sys.version_info.major == 3 and sys.version_info.minor < 6
62
- ):
63
- for _, name, ispkg in iter_modules([pkg_path]):
64
- if not ispkg:
65
- children.add(f"{pkg}.{name}")
66
- else:
67
- for info in iter_modules([pkg_path]):
68
- if not info.ispkg:
69
- children.add(f"{pkg}.{info.name}")
70
- for child in children:
71
- name = f"{__name__}.{child}"
72
- depth = tree
73
- for node in name.split('.'):
74
- if node not in depth:
75
- depth[node] = {}
76
- depth = depth[node]
77
- if name in sys.modules:
78
- depth['__module__'] = sys.modules[name]
79
- return tree
80
-
81
- def publicize_modules(modulename: str, tree: dict[str, dict | ModuleType]):
82
- def is_local(obj: object):
83
- if isinstance(obj, ModuleType):
84
- return obj.__spec__.parent == modulename
85
- elif hasattr(obj, '__module__'):
86
- return obj.__module__ == modulename
87
- return obj is not None
88
-
89
- for name, subtree in tree.items():
90
- if name == '__module__' and isinstance(subtree, ModuleType):
91
- submodule = subtree
92
- init_dir = dir(submodule)
93
-
94
- @wraps(submodule.__dir__)
95
- def wrapped():
96
- s = set(attr for attr in init_dir if is_local(getattr(submodule, attr, None)))
97
- if hasattr(submodule, '__all__'):
98
- s.update(submodule.__all__)
99
- return list(s)
100
-
101
- sys.modules[submodule.__name__].__dir__ = wrapped
102
-
103
- else:
104
- publicize_modules(f"{modulename}.{name}", subtree)
105
-
106
- publicize_modules(*find_modules(os.path.split(__file__)[0]).popitem())
107
-
108
- finally:
109
- del find_modules, publicize_modules
8
+ __getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__)
chromatic/__init__.pyi ADDED
@@ -0,0 +1,74 @@
1
+ __all__ = [
2
+ 'AnsiImage',
3
+ 'Back',
4
+ 'Color',
5
+ 'ColorNamespace',
6
+ 'ColorStr',
7
+ 'Fore',
8
+ 'SgrParameter',
9
+ 'Style',
10
+ '__version__',
11
+ 'ansi2img',
12
+ 'ansi_quantize',
13
+ 'ansicolor24Bit',
14
+ 'ansicolor4Bit',
15
+ 'ansicolor8Bit',
16
+ 'ansify',
17
+ 'image',
18
+ 'ascii2img',
19
+ 'ascii_printable',
20
+ 'color',
21
+ 'colorbytes',
22
+ 'contrast_stretch',
23
+ 'cp437_printable',
24
+ 'data',
25
+ 'equalize_white_point',
26
+ 'get_font_key',
27
+ 'get_font_object',
28
+ 'get_glyph_masks',
29
+ 'img2ansi',
30
+ 'img2ascii',
31
+ 'named_color',
32
+ 'read_ans',
33
+ 'register_userfont',
34
+ 'render_ans',
35
+ 'reshape_ansi',
36
+ 'to_sgr_array',
37
+ ]
38
+ from . import color, data, image
39
+ from ._version import version as __version__
40
+ from .color import (
41
+ Back,
42
+ Color,
43
+ ColorNamespace,
44
+ ColorStr,
45
+ Fore,
46
+ SgrParameter,
47
+ Style,
48
+ ansicolor24Bit,
49
+ ansicolor4Bit,
50
+ ansicolor8Bit,
51
+ colorbytes,
52
+ named_color,
53
+ )
54
+ from .data import register_userfont
55
+ from .image import (
56
+ AnsiImage,
57
+ ansi2img,
58
+ ansi_quantize,
59
+ ansify,
60
+ ascii2img,
61
+ ascii_printable,
62
+ contrast_stretch,
63
+ cp437_printable,
64
+ equalize_white_point,
65
+ get_font_key,
66
+ get_font_object,
67
+ img2ansi,
68
+ img2ascii,
69
+ read_ans,
70
+ render_ans,
71
+ reshape_ansi,
72
+ to_sgr_array,
73
+ )
74
+ from .image._glyph_proc import get_glyph_masks
chromatic/_typing.py CHANGED
@@ -14,12 +14,13 @@ from typing import (
14
14
  Concatenate,
15
15
  Hashable,
16
16
  Iterable,
17
- Literal,
17
+ Literal as L,
18
18
  NamedTuple,
19
19
  Optional,
20
20
  ParamSpec,
21
21
  Protocol,
22
22
  Sequence,
23
+ TYPE_CHECKING,
23
24
  Type,
24
25
  TypeAlias,
25
26
  TypeAliasType,
@@ -40,7 +41,8 @@ from PIL.ImageFont import FreeTypeFont
40
41
  from numpy import dtype, float64, generic, ndarray, number, uint8
41
42
  from numpy._typing import NDArray, _ArrayLike
42
43
 
43
- from chromatic.data import UserFont
44
+ if TYPE_CHECKING:
45
+ from .data import UserFont
44
46
 
45
47
  _P = ParamSpec('_P')
46
48
  _T = TypeVar('_T')
@@ -49,10 +51,10 @@ _T_contra = TypeVar('_T_contra', contravariant=True)
49
51
  _AnyNumber_co = TypeVar('_AnyNumber_co', number, Number, covariant=True)
50
52
 
51
53
  type ArrayReducerFunc[_SCT: generic] = Callable[Concatenate[_ArrayLike[_SCT], _P], NDArray[_SCT]]
52
- type ShapedNDArray[_Shape: tuple[int, ...], _SCT: generic] = ndarray[_Shape, dtype[_SCT]]
54
+ type ShapedNDArray[_Shape: tuple[int, ...], _SCT = generic] = ndarray[_Shape, dtype[_SCT]]
53
55
  type MatrixLike[_SCT: generic] = ShapedNDArray[TupleOf2[int], _SCT]
54
56
  type SquareMatrix[_I: int, _SCT: generic] = ShapedNDArray[TupleOf2[_I], _SCT]
55
- type GlyphArray[_SCT: generic] = SquareMatrix[Literal[24], _SCT]
57
+ type GlyphArray[_SCT: generic] = SquareMatrix[L[24], _SCT]
56
58
  type TupleOf2[_T] = tuple[_T, _T]
57
59
  type TupleOf3[_T] = tuple[_T, _T, _T]
58
60
 
@@ -64,25 +66,41 @@ GlyphBitmask: TypeAlias = GlyphArray[bool]
64
66
  Bitmask: TypeAlias = MatrixLike[bool]
65
67
  GreyscaleGlyphArray: TypeAlias = GlyphArray[float64]
66
68
  GreyscaleArray: TypeAlias = MatrixLike[float64]
67
- RGBArray: TypeAlias = ShapedNDArray[tuple[int, int, Literal[3]], uint8]
68
- RGBPixel: TypeAlias = ShapedNDArray[tuple[Literal[3]], uint8]
69
+ RGBArray: TypeAlias = ShapedNDArray[tuple[int, int, L[3]], uint8]
70
+ RGBPixel: TypeAlias = ShapedNDArray[tuple[L[3]], uint8]
69
71
 
70
72
  RGBImageLike: TypeAlias = Image | RGBArray
71
- RGBVectorLike: TypeAlias = Int3Tuple | IntSequence | RGBPixel
72
- ColorDictKeys = Literal['fg', 'bg']
73
- Ansi4BitAlias = Literal['4b']
74
- Ansi8BitAlias = Literal['8b']
75
- Ansi24BitAlias = Literal['24b']
73
+ RGBVectorLike: TypeAlias = IntSequence | RGBPixel
74
+ ColorDictKeys = L['fg', 'bg']
75
+ Ansi4BitAlias = L['4b']
76
+ Ansi8BitAlias = L['8b']
77
+ Ansi24BitAlias = L['24b']
76
78
  AnsiColorAlias = Ansi4BitAlias | Ansi8BitAlias | Ansi24BitAlias
77
- FontArgType: TypeAlias = FreeTypeFont | UserFont | str | int
79
+ FontArgType: TypeAlias = 'FreeTypeFont | UserFont | str'
80
+
81
+
82
+ def eval_annotation(annotation: str, **kwargs) -> Any:
83
+ globals_ = kwargs.get('globals', {}) | globals()
84
+ locals_ = kwargs.get('locals', {})
85
+ try:
86
+ return subtype(eval(annotation, globals_.copy(), locals_.copy()))
87
+ except NameError as e:
88
+ try:
89
+ import typing
90
+
91
+ globals_[e.name] = getattr(typing, e.name)
92
+ return eval_annotation(annotation, globals=globals_, locals=locals_)
93
+ except AttributeError:
94
+ pass
95
+ raise
78
96
 
79
97
 
80
98
  def type_error_msg(err_obj, *expected, context: str = '', obj_repr=False):
81
99
  n_expected = len(expected)
82
- name_slots = ['{%d.__qualname__!r}' % n for n in range(n_expected)]
83
- if name_slots and n_expected > 1:
100
+ name_slots = ["{%d.__name__!r}" % n for n in range(n_expected)]
101
+ if n_expected > 1:
84
102
  name_slots[-1] = f"or {name_slots[-1]}"
85
- names = (', ' if n_expected > 2 else ' ').join([context.strip()] + name_slots).format(*expected)
103
+ names = (', ' if n_expected > 2 else ' ').join([context.strip(), *name_slots]).format(*expected)
86
104
  if not obj_repr:
87
105
  if not isinstance(err_obj, type):
88
106
  err_obj = type(err_obj)
@@ -100,7 +118,7 @@ def is_matching_type(value, typ):
100
118
  origin, args = deconstruct_type(typ)
101
119
  if origin is Union:
102
120
  return any(is_matching_type(value, arg) for arg in args)
103
- elif origin is Literal:
121
+ elif origin is L:
104
122
  return value in args
105
123
  elif isinstance(typ, TypeVar):
106
124
  if typ.__constraints__:
@@ -188,20 +206,20 @@ def unionize(__iterable: Iterable[SupportsUnion[_T_contra, _T_co]]) -> _T_co:
188
206
 
189
207
  _GenericAlias = type(Type[...]) | types.GenericAlias
190
208
  _UnionGenericType = type(Union[..., None])
191
- _LiteralGenericType = type(Literal[''])
209
+ _LiteralGenericType = type(L[''])
192
210
  _CallableGenericType = type(Callable[[], ...]) | type(ABC_Callable[[], ...])
193
211
  _CallableType = type(Callable) | ABC_Callable
194
212
 
195
213
 
196
214
  class _BoundedDict[_KT, _VT](OrderedDict[_KT, _VT]):
215
+ """Bounded OrderedDict, mimics FIFO behavior of `functools.lru_cache`"""
197
216
 
198
217
  def __init__(self, *, maxsize: Optional[int] = 128):
199
- """Bounded OrderedDict, mimics FIFO behavior of `functools.lru_cache`"""
200
218
  super().__init__()
201
219
  if maxsize is not None:
202
220
  maxsize = max(maxsize, 0)
203
221
 
204
- @wraps(self.__setitem__)
222
+ @wraps(_BoundedDict.__setitem__)
205
223
  def _fifo(_, key, value):
206
224
  if maxsize <= len(self):
207
225
  self.popitem(last=True)
@@ -345,7 +363,7 @@ def subtype[_T](typ: _T) -> _T:
345
363
  return getattr(value, '__args__', ())
346
364
 
347
365
  start = args[literals.pop(0)]
348
- args_list[args_list.index(start)] = Literal[
366
+ args_list[args_list.index(start)] = L[
349
367
  *start.__args__,
350
368
  *dict.fromkeys(arg for idx in literals for arg in _next_args(idx)),
351
369
  ]
chromatic/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.2.2'
21
- __version_tuple__ = version_tuple = (0, 2, 2)
20
+ __version__ = version = '0.3.0'
21
+ __version_tuple__ = version_tuple = (0, 3, 0)
@@ -1,6 +1,3 @@
1
- from . import core, palette
2
- from .colorconv import *
3
- from .core import *
4
- from .palette import *
1
+ import lazy_loader as _lazy
5
2
 
6
- __all__ = list(set(core.__all__) | set(colorconv.__all__) | set(palette.__all__))
3
+ __getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__)
@@ -0,0 +1,86 @@
1
+ __all__ = [
2
+ 'ANSI_4BIT_RGB',
3
+ 'Back',
4
+ 'CSI',
5
+ 'Color',
6
+ 'ColorNamespace',
7
+ 'ColorStr',
8
+ 'Fore',
9
+ 'SGR_RESET',
10
+ 'SgrParameter',
11
+ 'SgrSequence',
12
+ 'Style',
13
+ 'ansi_4bit_to_rgb',
14
+ 'ansi_8bit_to_rgb',
15
+ 'ansicolor24Bit',
16
+ 'ansicolor4Bit',
17
+ 'ansicolor8Bit',
18
+ 'color_chain',
19
+ 'colorbytes',
20
+ 'get_ansi_type',
21
+ 'int2rgb',
22
+ 'hexstr2rgb',
23
+ 'hsl2rgb',
24
+ 'hsv2rgb',
25
+ 'is_u24',
26
+ 'lab2rgb',
27
+ 'lab2xyz',
28
+ 'named_color',
29
+ 'nearest_ansi_4bit_rgb',
30
+ 'nearest_ansi_8bit_rgb',
31
+ 'randcolor',
32
+ 'rgb2ansi_escape',
33
+ 'rgb2int',
34
+ 'rgb2hexstr',
35
+ 'rgb2hsl',
36
+ 'rgb2hsv',
37
+ 'rgb2lab',
38
+ 'rgb2xyz',
39
+ 'rgb_diff',
40
+ 'rgb_dispatch',
41
+ 'rgb_to_ansi_8bit',
42
+ 'xyz2lab',
43
+ 'xyz2rgb',
44
+ ]
45
+ from . import colorconv, core, iterators, palette
46
+ from .colorconv import (
47
+ ANSI_4BIT_RGB,
48
+ ansi_4bit_to_rgb,
49
+ ansi_8bit_to_rgb,
50
+ int2rgb,
51
+ hexstr2rgb,
52
+ hsl2rgb,
53
+ hsv2rgb,
54
+ is_u24,
55
+ lab2rgb,
56
+ lab2xyz,
57
+ nearest_ansi_4bit_rgb,
58
+ nearest_ansi_8bit_rgb,
59
+ rgb2int,
60
+ rgb2hexstr,
61
+ rgb2hsl,
62
+ rgb2hsv,
63
+ rgb2lab,
64
+ rgb2xyz,
65
+ rgb_diff,
66
+ rgb_to_ansi_8bit,
67
+ xyz2lab,
68
+ xyz2rgb,
69
+ )
70
+ from .core import (
71
+ CSI,
72
+ Color,
73
+ ColorStr,
74
+ SGR_RESET,
75
+ SgrParameter,
76
+ SgrSequence,
77
+ ansicolor24Bit,
78
+ ansicolor4Bit,
79
+ ansicolor8Bit,
80
+ color_chain,
81
+ colorbytes,
82
+ get_ansi_type,
83
+ randcolor,
84
+ rgb2ansi_escape,
85
+ )
86
+ from .palette import Back, ColorNamespace, Fore, Style, named_color, rgb_dispatch
@@ -2,16 +2,16 @@ __all__ = [
2
2
  'ANSI_4BIT_RGB',
3
3
  'ansi_4bit_to_rgb',
4
4
  'ansi_8bit_to_rgb',
5
- 'hex2rgb',
5
+ 'int2rgb',
6
6
  'hexstr2rgb',
7
7
  'hsl2rgb',
8
8
  'hsv2rgb',
9
- 'is_hex_rgb',
9
+ 'is_u24',
10
10
  'lab2rgb',
11
11
  'lab2xyz',
12
12
  'nearest_ansi_4bit_rgb',
13
13
  'nearest_ansi_8bit_rgb',
14
- 'rgb2hex',
14
+ 'rgb2int',
15
15
  'rgb2hexstr',
16
16
  'rgb2hsl',
17
17
  'rgb2hsv',
@@ -23,40 +23,69 @@ __all__ = [
23
23
  'xyz2rgb',
24
24
  ]
25
25
 
26
+ from functools import lru_cache
26
27
  from operator import mul, truediv
27
- from typing import Final, Literal, SupportsInt, cast
28
+ from typing import Final, Literal, SupportsInt, TypeGuard
28
29
 
29
30
  import numpy as np
30
31
 
31
32
  from .._typing import Float3Tuple, FloatSequence, Int3Tuple, RGBPixel, RGBVectorLike, ShapedNDArray
32
33
 
33
34
 
34
- def is_hex_rgb(value, *, strict: bool = False):
35
- if issubclass(type(value), SupportsInt):
36
- if 0x0 <= int(value) <= 0xFFFFFF:
35
+ @lru_cache
36
+ def _supports_int(typ: type) -> TypeGuard[type[SupportsInt]]:
37
+ return issubclass(typ, SupportsInt)
38
+
39
+
40
+ def is_u24(value, *, strict: bool = False):
41
+ """Check if value is an unsigned 24-bit integer.
42
+
43
+ Parameters
44
+ ---------
45
+ value
46
+ Input number
47
+ strict : bool
48
+ Whether to return False or raise ValueError on failure
49
+
50
+ Raises
51
+ ------
52
+ ValueError
53
+ Raised when `strict=True` and value is not u24
54
+ """
55
+ if _supports_int(type(value)):
56
+ if 0 <= int(value) <= 0xFFFFFF:
37
57
  return True
38
58
  elif not strict:
39
59
  return False
40
- raise TypeError(f"{value!r} is not a valid RGB color") from None
60
+ raise ValueError(f"{value!r} is not u24")
41
61
 
42
62
 
43
63
  def hexstr2rgb(__str: str) -> Int3Tuple:
44
- if is_hex_rgb(value := int(__str, 16), strict=True):
45
- return hex2rgb(value)
64
+ n = len(__str)
65
+ if n % 4 == 0: # trunc alpha
66
+ n *= 3
67
+ n //= 4
68
+ __str = __str[:n]
69
+ if n == 3: # rgb -> rrggbb
70
+ __str = ''.join(c * 2 for c in __str)
71
+ if is_u24(value := int(__str, 16), strict=True):
72
+ return int2rgb(value)
46
73
 
47
74
 
48
75
  def rgb2hexstr(rgb: RGBVectorLike) -> str:
49
- r, g, b = rgb
50
- return f'{r:02x}{g:02x}{b:02x}'
76
+ return "%02x%02x%02x" % tuple(rgb)
51
77
 
52
78
 
53
- def rgb2hex(rgb: RGBVectorLike) -> int:
79
+ def rgb2int(rgb: RGBVectorLike) -> int:
54
80
  r, g, b = map(int, rgb)
55
81
  return r << 16 | g << 8 | b
56
82
 
57
83
 
58
- def hex2rgb(value: int) -> Int3Tuple:
59
- return (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF
84
+ def int2rgb(__x: int) -> Int3Tuple:
85
+ try:
86
+ return getattr(__x, 'rgb')
87
+ except AttributeError:
88
+ return (__x >> 16) & 0xFF, (__x >> 8) & 0xFF, __x & 0xFF
60
89
 
61
90
 
62
91
  def xyz2lab(xyz: FloatSequence) -> Float3Tuple:
@@ -78,7 +107,7 @@ def lab2xyz(lab: FloatSequence) -> Float3Tuple:
78
107
  x, y, z = map(
79
108
  mul,
80
109
  (95.047, 100.0, 108.883),
81
- map(lambda n: (lambda c: c if c > 0.008856 else (n - 16 / 116) / 7.787)(n**3), (x, y, z)),
110
+ map(lambda i: (lambda j: j if j > 0.008856 else (i - 16 / 116) / 7.787)(i**3), (x, y, z)),
82
111
  )
83
112
  return x, y, z
84
113
 
@@ -104,41 +133,30 @@ def xyz2rgb(xyz: ShapedNDArray[tuple[Literal[3]], np.float64]) -> Int3Tuple:
104
133
 
105
134
  def hsl2rgb(hsl: FloatSequence) -> Int3Tuple:
106
135
  h, s, L = hsl
107
- h = (h / 360) % 1
136
+ h /= 360
137
+ h %= 1
108
138
  if h < 0:
109
139
  h += 1
110
- r = g = b = L
111
140
  v = (L * (1.0 + s)) if L <= 0.5 else (L + s - L * s)
112
- if v > 0:
141
+ if v > 0 and 0 <= (sextant := int(h)) <= 5:
113
142
  m = L + L - v
114
143
  sv = (v - m) / v
115
144
  h *= 6.0
116
- sextant = int(h)
117
- fract = h - sextant
118
- vsf = v * sv * fract
145
+ vsf = v * sv * (h - sextant)
119
146
  mid1 = m + vsf
120
147
  mid2 = v - vsf
121
- if sextant == 0:
122
- r, g, b = v, mid1, m
123
- elif sextant == 1:
124
- r, g, b = mid2, v, m
125
- elif sextant == 2:
126
- r, g, b = m, v, mid1
127
- elif sextant == 3:
128
- r, g, b = m, mid2, v
129
- elif sextant == 4:
130
- r, g, b = mid1, m, v
131
- elif sextant == 5:
132
- r, g, b = v, m, mid2
133
- r, g, b = (round(x * 255) for x in (r, g, b))
148
+ r, g, b = (
149
+ round(x * 0xFF)
150
+ for x in [[v, mid1, m], [mid2, v, m], [m, v, mid1], [m, mid2, v], [mid1, m, v]][sextant]
151
+ )
134
152
  else:
135
- r, g, b = (round(L * 255) for _ in range(3))
153
+ r, g, b = [round(L * 0xFF)] * 3
136
154
  return r, g, b
137
155
 
138
156
 
139
157
  def rgb2hsl(rgb: RGBVectorLike) -> Float3Tuple:
140
158
  r, g, b = (x / 255.0 for x in rgb)
141
- m, v = sorted([r, g, b])[::2]
159
+ m, _, v = sorted([r, g, b])
142
160
  L = (m + v) / 2
143
161
  h = s = 0
144
162
  if L > 0:
@@ -245,7 +263,7 @@ def ansi_4bit_to_rgb(value: int):
245
263
  return ANSI_4BIT_RGB[value]
246
264
 
247
265
 
248
- def _4b_lookup():
266
+ def _4b_lookup() -> dict[Int3Tuple, Int3Tuple]:
249
267
  def rgb_dist(rgb, ansi):
250
268
  r_mean = (rgb[:, 0:1] + ansi[:, 0]) / 2
251
269
  r_diff = (rgb[:, 0:1] - ansi[:, 0]) * (2 + r_mean / 256)
@@ -257,12 +275,11 @@ def _4b_lookup():
257
275
  quants = np.stack(
258
276
  np.meshgrid(*np.repeat(np.arange(32).reshape([1, -1]), 3, 0), indexing='ij'), axis=-1
259
277
  ).reshape([-1, 3])
260
- rgb_colors = quants * 8
261
- nearest_colors = rgb_4b_arr[np.argmin(rgb_dist(rgb_colors, rgb_4b_arr), axis=1)]
262
- table = {
278
+ nearest_colors = rgb_4b_arr[np.argmin(rgb_dist(quants * 8, rgb_4b_arr), axis=1)]
279
+ table: dict = {
263
280
  tuple(map(int, color)): tuple(map(int, nearest_colors[i])) for i, color in enumerate(quants)
264
281
  }
265
- return cast(dict[Int3Tuple, Int3Tuple], table)
282
+ return table
266
283
 
267
284
 
268
285
  ANSI_4BIT_RGB_MAP = _4b_lookup()