chromatic-python 0.2.1__tar.gz → 0.3.0__tar.gz

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 (57) hide show
  1. {chromatic_python-0.2.1 → chromatic_python-0.3.0}/.gitignore +2 -2
  2. {chromatic_python-0.2.1/chromatic_python.egg-info → chromatic_python-0.3.0}/PKG-INFO +9 -8
  3. chromatic_python-0.3.0/chromatic/__init__.py +8 -0
  4. chromatic_python-0.3.0/chromatic/__init__.pyi +74 -0
  5. {chromatic_python-0.2.1 → chromatic_python-0.3.0}/chromatic/_typing.py +45 -24
  6. {chromatic_python-0.2.1 → chromatic_python-0.3.0}/chromatic/_version.py +2 -2
  7. chromatic_python-0.3.0/chromatic/color/__init__.py +3 -0
  8. chromatic_python-0.3.0/chromatic/color/__init__.pyi +86 -0
  9. {chromatic_python-0.2.1 → chromatic_python-0.3.0}/chromatic/color/colorconv.py +59 -42
  10. chromatic_python-0.3.0/chromatic/color/core.py +1652 -0
  11. chromatic_python-0.3.0/chromatic/color/core.pyi +455 -0
  12. {chromatic_python-0.2.1 → chromatic_python-0.3.0}/chromatic/color/iterators.py +3 -3
  13. chromatic_python-0.3.0/chromatic/color/palette.py +561 -0
  14. chromatic_python-0.3.0/chromatic/color/palette.pyi +286 -0
  15. chromatic_python-0.3.0/chromatic/data/__init__.py +26 -0
  16. chromatic_python-0.3.0/chromatic/data/__init__.pyi +9 -0
  17. chromatic_python-0.3.0/chromatic/data/_fetchers.py +55 -0
  18. chromatic_python-0.3.0/chromatic/data/registry.json +5 -0
  19. chromatic_python-0.3.0/chromatic/data/userfont.py +132 -0
  20. chromatic_python-0.3.0/chromatic/data/userfont.pyi +28 -0
  21. chromatic_python-0.3.0/chromatic/data/userfont.schema.json +28 -0
  22. {chromatic_python-0.2.1 → chromatic_python-0.3.0}/chromatic/demo.py +152 -131
  23. chromatic_python-0.3.0/chromatic/image/__init__.pyi +70 -0
  24. {chromatic_python-0.2.1/chromatic/ascii → chromatic_python-0.3.0/chromatic/image}/_array.py +185 -81
  25. {chromatic_python-0.2.1/chromatic/ascii → chromatic_python-0.3.0/chromatic/image}/_curses.py +8 -4
  26. {chromatic_python-0.2.1/chromatic/ascii → chromatic_python-0.3.0/chromatic/image}/_glyph_proc.py +9 -3
  27. {chromatic_python-0.2.1 → chromatic_python-0.3.0/chromatic_python.egg-info}/PKG-INFO +9 -8
  28. {chromatic_python-0.2.1 → chromatic_python-0.3.0}/chromatic_python.egg-info/SOURCES.txt +17 -9
  29. chromatic_python-0.3.0/chromatic_python.egg-info/requires.txt +9 -0
  30. {chromatic_python-0.2.1 → chromatic_python-0.3.0}/pyproject.toml +12 -23
  31. chromatic_python-0.3.0/requirements.txt +9 -0
  32. {chromatic_python-0.2.1 → chromatic_python-0.3.0}/tests/test_color_str.py +32 -43
  33. chromatic_python-0.2.1/chromatic/__init__.py +0 -42
  34. chromatic_python-0.2.1/chromatic/color/__init__.py +0 -6
  35. chromatic_python-0.2.1/chromatic/color/core.py +0 -1737
  36. chromatic_python-0.2.1/chromatic/color/core.pyi +0 -354
  37. chromatic_python-0.2.1/chromatic/color/palette.py +0 -552
  38. chromatic_python-0.2.1/chromatic/color/palette.pyi +0 -315
  39. chromatic_python-0.2.1/chromatic/data/__init__.py +0 -198
  40. chromatic_python-0.2.1/chromatic/data/__init__.pyi +0 -15
  41. chromatic_python-0.2.1/chromatic/data/images/hotdog.jpg +0 -0
  42. chromatic_python-0.2.1/chromatic_python.egg-info/requires.txt +0 -8
  43. {chromatic_python-0.2.1 → chromatic_python-0.3.0}/.gitattributes +0 -0
  44. {chromatic_python-0.2.1 → chromatic_python-0.3.0}/LICENSE +0 -0
  45. {chromatic_python-0.2.1 → chromatic_python-0.3.0}/README.md +0 -0
  46. {chromatic_python-0.2.1/chromatic/data/images → chromatic_python-0.3.0/chromatic/data}/butterfly.jpg +0 -0
  47. {chromatic_python-0.2.1/chromatic/data/images → chromatic_python-0.3.0/chromatic/data}/escher.png +0 -0
  48. {chromatic_python-0.2.1 → chromatic_python-0.3.0}/chromatic/data/fonts/consolas.ttf +0 -0
  49. /chromatic_python-0.2.1/chromatic/data/fonts/IBM_VGA_437_8x16.ttf → /chromatic_python-0.3.0/chromatic/data/fonts/vga437.ttf +0 -0
  50. {chromatic_python-0.2.1/chromatic/data/images → chromatic_python-0.3.0/chromatic/data}/goblin_virus.png +0 -0
  51. {chromatic_python-0.2.1/chromatic/ascii → chromatic_python-0.3.0/chromatic/image}/__init__.py +0 -0
  52. {chromatic_python-0.2.1 → chromatic_python-0.3.0}/chromatic_python.egg-info/dependency_links.txt +0 -0
  53. {chromatic_python-0.2.1 → chromatic_python-0.3.0}/chromatic_python.egg-info/top_level.txt +0 -0
  54. {chromatic_python-0.2.1 → chromatic_python-0.3.0}/logo/logo.ANS +0 -0
  55. {chromatic_python-0.2.1 → chromatic_python-0.3.0}/logo/logo.PNG +0 -0
  56. {chromatic_python-0.2.1 → chromatic_python-0.3.0}/setup.cfg +0 -0
  57. {chromatic_python-0.2.1 → chromatic_python-0.3.0}/tests/__init__.py +0 -0
@@ -5,7 +5,7 @@ __pycache__/
5
5
  /dist/
6
6
  /docs/
7
7
  /tests/_*
8
- /chromatic/data/*.json
9
- /chromatic/data/*.pyi
8
+ /chromatic/data/userfont.json
10
9
  /chromatic/_version.py
11
10
  /*.egg-info/
11
+ /build/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chromatic-python
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: ANSI art image processing and colored terminal text
5
5
  Author: crypt0lith
6
6
  License: MIT License
@@ -33,14 +33,15 @@ Classifier: Typing :: Typed
33
33
  Requires-Python: >=3.12
34
34
  Description-Content-Type: text/markdown
35
35
  License-File: LICENSE
36
- Requires-Dist: fonttools~=4.51.0
37
36
  Requires-Dist: networkx~=3.4.2
38
- Requires-Dist: numpy~=2.1.1
39
- Requires-Dist: opencv-python~=4.10.0.84
40
- Requires-Dist: pillow~=10.4.0
41
- Requires-Dist: scikit-image~=0.25.0rc1
42
- Requires-Dist: scikit-learn~=1.5.2
43
- Requires-Dist: scipy~=1.14.1
37
+ Requires-Dist: numpy~=2.2.6
38
+ Requires-Dist: pillow~=11.2.1
39
+ Requires-Dist: opencv-python~=4.11.0.86
40
+ Requires-Dist: scikit-image~=0.25.2
41
+ Requires-Dist: scikit-learn~=1.6.1
42
+ Requires-Dist: fonttools~=4.58.0
43
+ Requires-Dist: scipy~=1.15.3
44
+ Requires-Dist: lazy_loader~=0.4
44
45
  Dynamic: license-file
45
46
 
46
47
  ![image](/logo/logo.PNG)
@@ -0,0 +1,8 @@
1
+ try:
2
+ from ._version import version as __version__
3
+ except ImportError:
4
+ __version__ = "0.0.0"
5
+
6
+ import lazy_loader as _lazy
7
+
8
+ __getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__)
@@ -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
@@ -1,25 +1,26 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
+ import operator as op
4
5
  import re
5
6
  import types
6
7
  from collections import OrderedDict, namedtuple
7
8
  from collections.abc import Callable as ABC_Callable
8
9
  from functools import reduce, wraps
9
10
  from numbers import Number
10
- from operator import attrgetter, or_ as bitwise_or
11
11
  from typing import (
12
12
  Any,
13
13
  Callable,
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 = [f"{{{n}.__qualname__!r}}" 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__:
@@ -183,25 +201,25 @@ class SupportsUnion(Protocol[_T_contra, _T_co]):
183
201
 
184
202
 
185
203
  def unionize(__iterable: Iterable[SupportsUnion[_T_contra, _T_co]]) -> _T_co:
186
- return reduce(bitwise_or, __iterable)
204
+ return reduce(op.or_, __iterable)
187
205
 
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)
@@ -213,7 +231,7 @@ class _BoundedDict[_KT, _VT](OrderedDict[_KT, _VT]):
213
231
 
214
232
 
215
233
  _SUBTYPE_CACHE: _BoundedDict[int, ...] = _BoundedDict()
216
- _ATTR_GETTERS: _BoundedDict[..., tuple[Callable[[Iterable], NamedTuple], attrgetter]] = (
234
+ _ATTR_GETTERS: _BoundedDict[..., tuple[Callable[[Iterable], NamedTuple], op.attrgetter]] = (
217
235
  _BoundedDict()
218
236
  )
219
237
 
@@ -247,7 +265,10 @@ def _unique_attrs(obj) -> Optional['NamedTuple']:
247
265
  + 'Attrs'
248
266
  ).strip('_')
249
267
  UniqueAttrs = cast(NamedTuple, namedtuple(tup_name, field_names))
250
- _ATTR_GETTERS[tp] = constructor, getter = UniqueAttrs._make, attrgetter(*attr_names) # noqa
268
+ _ATTR_GETTERS[tp] = [constructor, getter] = [
269
+ UniqueAttrs._make,
270
+ op.attrgetter(*attr_names), # noqa
271
+ ]
251
272
  return constructor(getter(obj))
252
273
 
253
274
 
@@ -342,7 +363,7 @@ def subtype[_T](typ: _T) -> _T:
342
363
  return getattr(value, '__args__', ())
343
364
 
344
365
  start = args[literals.pop(0)]
345
- args_list[args_list.index(start)] = Literal[
366
+ args_list[args_list.index(start)] = L[
346
367
  *start.__args__,
347
368
  *dict.fromkeys(arg for idx in literals for arg in _next_args(idx)),
348
369
  ]
@@ -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.1'
21
- __version_tuple__ = version_tuple = (0, 2, 1)
20
+ __version__ = version = '0.3.0'
21
+ __version_tuple__ = version_tuple = (0, 3, 0)
@@ -0,0 +1,3 @@
1
+ import lazy_loader as _lazy
2
+
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()